diff --git a/package-lock.json b/package-lock.json index 38b3d01..d02268a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "good.tools", - "version": "1.19.3", + "version": "1.22.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "good.tools", - "version": "1.19.3", + "version": "1.22.0", "dependencies": { "@algolia/autocomplete-core": "^1.19.4", "@goodtools/jdserialize": "^1.0.0", @@ -15,6 +15,7 @@ "@headlessui/react": "^1.7.7", "@heroicons/react": "^2.0.13", "@monaco-editor/react": "^4.4.6", + "@peculiar/x509": "^1.14.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", @@ -37,7 +38,7 @@ "lru-cache": "^7.14.1", "lucide-react": "^0.562.0", "moment": "^2.29.4", - "node-forge": "^1.3.1", + "node-forge": "^1.3.3", "pako": "^2.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -2064,6 +2065,154 @@ "@octokit/openapi-types": "^27.0.0" } }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.0.tgz", + "integrity": "sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.0.tgz", + "integrity": "sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.0.tgz", + "integrity": "sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.0.tgz", + "integrity": "sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.0.tgz", + "integrity": "sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.0.tgz", + "integrity": "sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pfx": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.0.tgz", + "integrity": "sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.0.tgz", + "integrity": "sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.0.tgz", + "integrity": "sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@pkgr/core": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", @@ -5333,6 +5482,20 @@ "dev": true, "license": "MIT" }, + "node_modules/asn1js": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", + "integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/assert": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", @@ -13627,6 +13790,24 @@ "dev": true, "license": "MIT" }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -14088,6 +14269,12 @@ "node": ">=8" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -16151,6 +16338,24 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/tty-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", diff --git a/package.json b/package.json index c0cba3d..57851eb 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@headlessui/react": "^1.7.7", "@heroicons/react": "^2.0.13", "@monaco-editor/react": "^4.4.6", + "@peculiar/x509": "^1.14.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", @@ -37,7 +38,7 @@ "lru-cache": "^7.14.1", "lucide-react": "^0.562.0", "moment": "^2.29.4", - "node-forge": "^1.3.1", + "node-forge": "^1.3.3", "pako": "^2.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/config/tools.config.tsx b/src/config/tools.config.tsx index ba5f0ba..9b9ab82 100644 --- a/src/config/tools.config.tsx +++ b/src/config/tools.config.tsx @@ -68,8 +68,8 @@ export const tools: Tool[] = [ online: false, dependencies: [ { - name: 'node-forge', - url: 'https://www.npmjs.com/package/node-forge', + name: '@peculiar/x509', + url: 'https://www.npmjs.com/package/@peculiar/x509', }, ], }, diff --git a/src/tools/CertificateDecoder.test.tsx b/src/tools/CertificateDecoder.test.tsx new file mode 100644 index 0000000..f80ad45 --- /dev/null +++ b/src/tools/CertificateDecoder.test.tsx @@ -0,0 +1,94 @@ +import { describe, it, expect } from 'vitest' +import * as x509 from '@peculiar/x509' + +const RSA_CERT = `-----BEGIN CERTIFICATE----- +MIIFzjCCBLagAwIBAgIQCAID3TIok1it+qMlOD7pcTANBgkqhkiG9w0BAQsFADA8 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRwwGgYDVQQDExNBbWF6b24g +UlNBIDIwNDggTTAyMB4XDTIyMTIxNjAwMDAwMFoXDTI0MDExNDIzNTk1OVowFTET +MBEGA1UEAxMKZ29vZC50b29sczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBANe0t96ql19cOVP/fqhqrG6U66hGl8yF+CBX9Scx3wGkD4qtHTmC3uOqbcT1 +9Vc+GGEL0joPboBEiPTqID2trlVxwBe59UZukfUs7ZZfpyWBEpHMEEc+WS7f5s+b +ZT1sOyCgBV0/qllQnQPDF79ckWXCj33Q6Wi51eOoVBrHbp98KCs3tFzel57Sn8aQ +qTX9qCxeLiKoJLWK+lqx7eOUX6VgoYEdmiO8n0WgXIgffAxvS6uukjiY3hdOXrmq +VfH6KdSFSt97nFRoAMiQCc+Chu1wDva/XXsk2b8JQslO6yKPB6KDbuZIKRTEP5eM +22lZsYJW+ufRoPKGVEOzipu8jRsCAwEAAaOCAvEwggLtMB8GA1UdIwQYMBaAFMAx +Us1aUMOCfHRxzsvpnPl664LiMB0GA1UdDgQWBBR1ZHMqohcG+azPAT97VNyiCltN +lTAjBgNVHREEHDAaggpnb29kLnRvb2xzggwqLmdvb2QudG9vbHMwDgYDVR0PAQH/ +BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjA7BgNVHR8ENDAy +MDCgLqAshipodHRwOi8vY3JsLnIybTAyLmFtYXpvbnRydXN0LmNvbS9yMm0wMi5j +cmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwdQYIKwYBBQUHAQEEaTBnMC0GCCsGAQUF +BzABhiFodHRwOi8vb2NzcC5yMm0wMi5hbWF6b250cnVzdC5jb20wNgYIKwYBBQUH +MAKGKmh0dHA6Ly9jcnQucjJtMDIuYW1hem9udHJ1c3QuY29tL3IybTAyLmNlcjAM +BgNVHRMBAf8EAjAAMIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdwDuzdBk1dsa +zsVct520zROiModGfLzs3sNRSFlGcR+1mwAAAYUcWfxKAAAEAwBIMEYCIQCJHdV1 +TycoNjhL2y8OHxixJGiQc+ssiLt53Z/zWhmKUgIhALhWnYHlXt+Tf0vAKxRSfcZf +dxcahQsEqm/jTKSnigNVAHUAc9meiRtMlnigIH1HneayxhzQUV5xGSqMa4AQesF3 +crUAAAGFHFn8sQAABAMARjBEAiBmMG+ZdPgzdfjbM4QGy0/ASsgNQ9RS7A6Y0yOz +ATnwdgIgXvQjpGWid/DKm/ARPlMF1IGjWV7sZNvb872eAdbFq7oAdgBIsONr2qZH +NA/lagL6nTDrHFIBy1bdLIHZu7+rOdiEcwAAAYUcWfxjAAAEAwBHMEUCIAmIjE5A +mj1wDtutyUqcB1XhjkSKGc7YUwRsRZt7RsHzAiEAxHNTkkHVdKGz2ovdOJ/uP532 +1PduHSVxpZTTYIjF8WgwDQYJKoZIhvcNAQELBQADggEBADO9thRd7GhiU09/oY68 +9fHrnPivbvyQrBtwKOtsqa2k+0qf6Zhc2URJziigAa61O2MrQDLMm6H4qZ70ZTPV +IzdQC0ezR9Kewd7TDp5TvrBDZsHhYpEg5xB82/9LGYNjaYg0EfEIS6QaJFRF6mNo +YBoh+bLsodofsWCIogvtpHZmDXK91JDcOr3rSKZtFwL6lg8cYdKXpZ5meDGT6HR6 +4mDtz7sSwPid9n6KUz2plDAwMnYefX5RZoxJAvcB4wua5io6nymaKBtZPoF1rlil +7O6fCTlqFsWaoFeWrhvygP3ZaEu6r8P5n+x99UBJElfg3ybXZ5v29EyzMaDhUv0U +0m0= +-----END CERTIFICATE-----` + +const ED25519_CERT = `-----BEGIN CERTIFICATE----- +MIIBKTCB3KADAgECAhEA9I9+eCQ+zYNPxzW9nxPlMTAFBgMrZXAwEDEOMAwGA1UE +ChMFdGFsb3MwHhcNMjUwMTEyMTYzMjAyWhcNMjYwMTEyMTYzMjAyWjATMREwDwYD +VQQKEwhvczphZG1pbjAqMAUGAytlcAMhAAdeAV/uQQBjdWWdcaYnfsiyfPB//MoP +JQ2I+fz4LCSJo0gwRjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH +AwIwHwYDVR0jBBgwFoAUaMKDnlLRXRuu1HJz4Ei0ksEj+tQwBQYDK2VwA0EAyCoi +LsTiaGzVJlVVK2QZiqLrJCdPmhNfZ6Kj65nos2byRyGA2yNjTIPhV+MiAcKDKCsS +1FZXXO5Fvt8DlJCIAA== +-----END CERTIFICATE-----` + +describe('CertificateDecoder', () => { + describe('X509 Certificate Parsing', () => { + it('should parse RSA certificate', () => { + const cert = new x509.X509Certificate(RSA_CERT) + + expect(cert.subject).toContain('good.tools') + expect(cert.publicKey.algorithm.name).toBe('RSASSA-PKCS1-v1_5') + // eslint-disable-next-line @typescript-eslint/no-base-to-string + expect(cert.publicKey.toString('pem')).toContain('-----BEGIN PUBLIC KEY-----') + }) + + it('should parse Ed25519 certificate', () => { + const cert = new x509.X509Certificate(ED25519_CERT) + + expect(cert.subject).toContain('os:admin') + expect(cert.publicKey.algorithm.name).toBe('Ed25519') + // eslint-disable-next-line @typescript-eslint/no-base-to-string + expect(cert.publicKey.toString('pem')).toContain('-----BEGIN PUBLIC KEY-----') + }) + + it('should extract public key PEM from Ed25519 certificate', () => { + const cert = new x509.X509Certificate(ED25519_CERT) + // eslint-disable-next-line @typescript-eslint/no-base-to-string + const publicKeyPem = cert.publicKey.toString('pem') + + expect(publicKeyPem).toContain('-----BEGIN PUBLIC KEY-----') + expect(publicKeyPem).toContain('-----END PUBLIC KEY-----') + }) + + it('should handle certificate serial number', () => { + const rsaCert = new x509.X509Certificate(RSA_CERT) + const ed25519Cert = new x509.X509Certificate(ED25519_CERT) + + expect(rsaCert.serialNumber).toBeTruthy() + expect(ed25519Cert.serialNumber).toBeTruthy() + }) + + it('should parse validity dates', () => { + const cert = new x509.X509Certificate(ED25519_CERT) + + expect(cert.notBefore).toBeInstanceOf(Date) + expect(cert.notAfter).toBeInstanceOf(Date) + expect(cert.notAfter.getTime()).toBeGreaterThan(cert.notBefore.getTime()) + }) + }) +}) diff --git a/src/tools/CertificateDecoder.tsx b/src/tools/CertificateDecoder.tsx index 851b396..7c28da7 100644 --- a/src/tools/CertificateDecoder.tsx +++ b/src/tools/CertificateDecoder.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react' -import { pki } from 'node-forge' +import * as x509 from '@peculiar/x509' import moment from 'moment' import { CodeGroup } from '@/components/Code' import { Button } from '@/components/ui/button' @@ -42,46 +42,167 @@ YBoh+bLsodofsWCIogvtpHZmDXK91JDcOr3rSKZtFwL6lg8cYdKXpZ5meDGT6HR6 0m0= -----END CERTIFICATE-----` -interface CertificateExtension { - name: string - altNames?: Array<{ value: string }> - [key: string]: unknown +interface DecodedCert { + subject: string + issuer: string + notBefore: Date + notAfter: Date + serialNumber: string + publicKeyAlgorithm: string + publicKeyPem: string + extensions: Array<{ name: string; oid: string; critical: boolean; details: Record }> + subjectAltNames: string[] } -function CertificateDecoder() { - const [encoded, setEncoded] = useState('') - const [decoded, setDecoded] = useState(null) - const [names, setNames] = useState([]) - const encodedRef = useRef(null) +// OID to name mapping for extensions +const EXTENSION_NAMES: Record = { + '2.5.29.14': 'subjectKeyIdentifier', + '2.5.29.15': 'keyUsage', + '2.5.29.17': 'subjectAltName', + '2.5.29.19': 'basicConstraints', + '2.5.29.31': 'cRLDistributionPoints', + '2.5.29.32': 'certificatePolicies', + '2.5.29.35': 'authorityKeyIdentifier', + '2.5.29.37': 'extKeyUsage', + '1.3.6.1.5.5.7.1.1': 'authorityInfoAccess', + '1.3.6.1.4.1.11129.2.4.2': 'timestampList', +} - const decode = () => { - try { - const data = pki.certificateFromPem(encoded) - setDecoded(data) +// OID to name mapping for extended key usage +const EKU_NAMES: Record = { + '1.3.6.1.5.5.7.3.1': 'serverAuth', + '1.3.6.1.5.5.7.3.2': 'clientAuth', + '1.3.6.1.5.5.7.3.3': 'codeSigning', + '1.3.6.1.5.5.7.3.4': 'emailProtection', + '1.3.6.1.5.5.7.3.8': 'timeStamping', + '1.3.6.1.5.5.7.3.9': 'OCSPSigning', +} - const names: string[] = [] +function parseExtensionDetails(ext: x509.Extension): Record { + const details: Record = {} - try { - const cnField = data.subject.getField('CN') as pki.Attribute - if (cnField) { - names.push(cnField.value as string) + try { + switch (ext.type) { + case '2.5.29.14': { + // subjectKeyIdentifier + const ski = new x509.SubjectKeyIdentifierExtension(ext.rawData) + details.subjectKeyIdentifier = ski.keyId + break + } + case '2.5.29.35': { + // authorityKeyIdentifier + const aki = new x509.AuthorityKeyIdentifierExtension(ext.rawData) + if (aki.keyId) details.keyIdentifier = aki.keyId + break + } + case '2.5.29.17': { + // subjectAltName + const san = new x509.SubjectAlternativeNameExtension(ext.rawData) + const names: string[] = [] + san.names.items.forEach((name) => names.push(name.value)) + details.altNames = names.join(', ') + break + } + case '2.5.29.15': { + // keyUsage + const ku = new x509.KeyUsagesExtension(ext.rawData) + details.digitalSignature = !!(ku.usages & x509.KeyUsageFlags.digitalSignature) + details.nonRepudiation = !!(ku.usages & x509.KeyUsageFlags.nonRepudiation) + details.keyEncipherment = !!(ku.usages & x509.KeyUsageFlags.keyEncipherment) + details.dataEncipherment = !!(ku.usages & x509.KeyUsageFlags.dataEncipherment) + details.keyAgreement = !!(ku.usages & x509.KeyUsageFlags.keyAgreement) + details.keyCertSign = !!(ku.usages & x509.KeyUsageFlags.keyCertSign) + details.cRLSign = !!(ku.usages & x509.KeyUsageFlags.cRLSign) + details.encipherOnly = !!(ku.usages & x509.KeyUsageFlags.encipherOnly) + details.decipherOnly = !!(ku.usages & x509.KeyUsageFlags.decipherOnly) + break + } + case '2.5.29.37': { + // extKeyUsage + const eku = new x509.ExtendedKeyUsageExtension(ext.rawData) + eku.usages.forEach((usage) => { + const name = EKU_NAMES[String(usage)] || String(usage) + details[name] = true + }) + break + } + case '2.5.29.19': { + // basicConstraints + const bc = new x509.BasicConstraintsExtension(ext.rawData) + details.cA = bc.ca + if (bc.pathLength !== undefined) { + details.pathLenConstraint = String(bc.pathLength) + } + break + } + case '1.3.6.1.5.5.7.1.1': { + // authorityInfoAccess + const aia = new x509.AuthorityInfoAccessExtension(ext.rawData) + if (aia.ocsp.length > 0) { + details.ocsp = aia.ocsp.map((n) => n.value).join(', ') + } + if (aia.caIssuers.length > 0) { + details.caIssuers = aia.caIssuers.map((n) => n.value).join(', ') } - } catch { - // CN field not found + break } + } + } catch { + // If we can't parse extension details, just return empty + } - const extensions = data.extensions as CertificateExtension[] + return details +} - const san = extensions.find((e: CertificateExtension) => e.name === 'subjectAltName') - if (san && san.altNames) { - san.altNames.forEach((a: { value: string }) => { - names.push(a.value) - }) - } +function parseCertificate(pem: string): DecodedCert { + const cert = new x509.X509Certificate(pem) - setNames(names.filter((v, i, a) => a.indexOf(v) === i)) + // Get subject alt names + const subjectAltNames: string[] = [] + const sanExt = cert.extensions.find((e) => e.type === '2.5.29.17') // subjectAltName OID + if (sanExt) { + const san = new x509.SubjectAlternativeNameExtension(sanExt.rawData) + san.names.items.forEach((name) => { + subjectAltNames.push(name.value) + }) + } + + // Parse extensions with full details + const extensions = cert.extensions.map((ext) => ({ + name: EXTENSION_NAMES[ext.type] || ext.type, + oid: ext.type, + critical: ext.critical, + details: parseExtensionDetails(ext), + })) + + return { + subject: cert.subject, + issuer: cert.issuer, + notBefore: cert.notBefore, + notAfter: cert.notAfter, + serialNumber: cert.serialNumber, + publicKeyAlgorithm: cert.publicKey.algorithm.name, + // eslint-disable-next-line @typescript-eslint/no-base-to-string + publicKeyPem: cert.publicKey.toString('pem'), + extensions, + subjectAltNames, + } +} + +function CertificateDecoder() { + const [encoded, setEncoded] = useState('') + const [decoded, setDecoded] = useState(null) + const [error, setError] = useState(null) + const encodedRef = useRef(null) + + const decode = () => { + try { + setError(null) + const data = parseCertificate(encoded) + setDecoded(data) } catch (err) { console.error('Failed to decode certificate:', err) + setError(err instanceof Error ? err.message : 'Failed to decode certificate') setDecoded(null) } } @@ -94,6 +215,7 @@ function CertificateDecoder() { reader.addEventListener('load', (event) => { setEncoded(event.target?.result as string) setDecoded(null) + setError(null) }) reader.readAsText(f) } @@ -101,17 +223,26 @@ function CertificateDecoder() { const loadExample = () => { setEncoded(EXAMPLE_CERT) setDecoded(null) + setError(null) } const clear = () => { setEncoded('') setDecoded(null) + setError(null) } useEffect(() => { encodedRef.current?.focus() }, []) + // Extract CN from subject for display + const displayName = decoded + ? decoded.subjectAltNames.length > 0 + ? decoded.subjectAltNames.join(', ') + : decoded.subject + : '' + return (