From 90c10fe7dde89ce30838b6bc89619893e7ce46e0 Mon Sep 17 00:00:00 2001 From: Abhijit Roy Date: Mon, 17 Jun 2024 18:56:29 +0530 Subject: [PATCH] Rewrite CertificateManager class using @peculiar/x509 library TODO: tests pending --- packages/auto-id/package.json | 2 + packages/auto-id/src/certificateManager.ts | 502 ++++++++++++------ packages/auto-id/src/keyManagement.ts | 23 +- .../auto-id/tests/certificateManager.test.ts | 240 ++++++--- yarn.lock | 192 ++++++- 5 files changed, 703 insertions(+), 256 deletions(-) diff --git a/packages/auto-id/package.json b/packages/auto-id/package.json index cbe56e52..39f78a58 100644 --- a/packages/auto-id/package.json +++ b/packages/auto-id/package.json @@ -12,6 +12,8 @@ "@autonomys/auto-utils": "workspace:*", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", + "@peculiar/webcrypto": "^1.5.0", + "@peculiar/x509": "^1.11.0", "asn1js": "^3.0.5", "node-forge": "^1.3.1" }, diff --git a/packages/auto-id/src/certificateManager.ts b/packages/auto-id/src/certificateManager.ts index 24d404af..8b8dea4b 100644 --- a/packages/auto-id/src/certificateManager.ts +++ b/packages/auto-id/src/certificateManager.ts @@ -1,39 +1,45 @@ -//! For key generation, management, `keyManagement.ts` file is used i.e. "crypto" library. -//! And for certificate related, used "node-forge" library. +//! For key generation, management, `keyManagement.ts` file is used using "crypto" library. +//! And for certificate related, used "@peculiar/x509" library. import { blake2b_256, concatenateUint8Arrays, stringToUint8Array } from '@autonomys/auto-utils' -import { KeyObject, createPublicKey, createSign } from 'crypto' +import { AsnConvert } from '@peculiar/asn1-schema' +import { AttributeTypeAndValue } from '@peculiar/asn1-x509' +import { Crypto } from '@peculiar/webcrypto' +import * as x509 from '@peculiar/x509' +import { KeyObject, createPublicKey } from 'crypto' import fs from 'fs' -import forge from 'node-forge' -import { doPublicKeysMatch, keyToPem, pemToPublicKey } from './keyManagement' -import { addDaysToCurrentDate, randomSerialNumber } from './utils' - -interface CustomCertificateExtension { - altNames: { - type: number - value: string - }[] -} +import { doPublicKeysMatch } from './keyManagement' +import { randomSerialNumber } from './utils' + +const crypto = new Crypto() +x509.cryptoProvider.set(crypto) interface SigningParams { privateKey: KeyObject algorithm: 'sha256' | null // Only 'sha256' or null for Ed25519 } -class CertificateManager { - private certificate: forge.pki.Certificate | null +const OID_COMMON_NAME = '2.5.4.3' // OID for Common Name, not available in the library. +const OID_SUBJECT_ALT_NAME = '2.5.29.17' // OID for Subject Alternative Name, not available in the library. + +export class CertificateManager { + private certificate: x509.X509Certificate | null private privateKey: KeyObject | null + private publicKey: KeyObject | null constructor( - certificate: forge.pki.Certificate | null = null, + certificate: x509.X509Certificate | null = null, privateKey: KeyObject | null = null, + publicKey: KeyObject | null = null, ) { this.certificate = certificate this.privateKey = privateKey + this.publicKey = publicKey } protected prepareSigningParams(): SigningParams { const privateKey = this.privateKey + if (!privateKey) { throw new Error('Private key is not set.') } @@ -41,6 +47,7 @@ class CertificateManager { if (privateKey.asymmetricKeyType === 'ed25519') { return { privateKey: privateKey, algorithm: null } } + if (privateKey.asymmetricKeyType === 'rsa') { return { privateKey: privateKey, algorithm: 'sha256' } } @@ -48,50 +55,104 @@ class CertificateManager { throw new Error('Unsupported key type for signing.') } - static toCommonName(subjectName: string): forge.pki.CertificateField[] { - return [{ name: 'commonName', value: subjectName }] + protected static toCommonName(subjectName: string): x509.Name { + const commonNameAttr = new AttributeTypeAndValue({ + type: OID_COMMON_NAME, + value: subjectName, + }) + return new x509.Name([[commonNameAttr]]) } - static prettyPrintCertificate(cert: forge.pki.Certificate): void { + // protected static toCommonName(subjectName: string): JsonName { + // return [{ '2.5.4.3': [subjectName] }] // OID for commonName + // } + + private static async keyObjectToCryptoKey(keyObject: KeyObject): Promise { + const keyType = keyObject.type + const keyPem = keyObject + .export({ type: keyType === 'private' ? 'pkcs8' : 'spki', format: 'pem' }) + .toString() + const keyData = Buffer.from(keyPem.split('\n').slice(1, -1).join(''), 'base64') + + if (keyObject.asymmetricKeyType === 'ed25519') { + if (keyType === 'private') { + return crypto.subtle.importKey('pkcs8', keyData, { name: 'Ed25519' }, true, ['sign']) + } else { + return crypto.subtle.importKey('spki', keyData, { name: 'Ed25519' }, true, ['verify']) + } + } else if (keyObject.asymmetricKeyType === 'rsa') { + if (keyType === 'private') { + return crypto.subtle.importKey( + 'pkcs8', + keyData, + { + name: 'RSASSA-PKCS1-v1_5', + hash: { name: 'SHA-256' }, + }, + true, + ['sign'], + ) + } else { + return crypto.subtle.importKey( + 'spki', + keyData, + { + name: 'RSASSA-PKCS1-v1_5', + hash: { name: 'SHA-256' }, + }, + true, + ['verify'], + ) + } + } else { + throw new Error('Unsupported key type') + } + } + + static prettyPrintCertificate(cert: x509.X509Certificate): void { console.log('Certificate:') console.log('============') - console.log( - `Subject: ${cert.subject.attributes.map((attr) => `${attr.name}=${attr.value}`).join(', ')}`, - ) - console.log( - `Issuer: ${cert.issuer.attributes.map((attr) => `${attr.name}=${attr.value}`).join(', ')}`, - ) + console.log(`Subject: ${cert.subject}`) + console.log(`Issuer: ${cert.issuer}`) console.log(`Serial Number: ${cert.serialNumber}`) - console.log(`Not Valid Before: ${cert.validity.notBefore.toISOString()}`) - console.log(`Not Valid After: ${cert.validity.notAfter.toISOString()}`) + console.log(`Not Valid Before: ${cert.notBefore}`) + console.log(`Not Valid After: ${cert.notAfter}`) + console.log('\nExtensions:') cert.extensions.forEach((ext) => { - console.log(` - ${ext.name} (${ext.id}): ${JSON.stringify(ext.value)}`) + console.log(` - ${ext.type}: ${JSON.stringify(ext.value)}`) }) console.log('\nPublic Key:') console.log(cert.publicKey) } - static certificateToPem(cert: forge.pki.Certificate): string { - return forge.pki.certificateToPem(cert) + static certificateToPem(cert: x509.X509Certificate): string { + return cert.toString('pem') } - static pemToCertificate(pem: string): forge.pki.Certificate { - return forge.pki.certificateFromPem(pem) + static pemToCertificate(pem: string): x509.X509Certificate { + return new x509.X509Certificate(pem) } - static getSubjectCommonName(subjectFields: forge.pki.CertificateField[]): string | undefined { - const cnField = subjectFields.find((field) => field.name === 'commonName') - if (cnField && typeof cnField.value === 'string') { - return cnField.value - } - return undefined + static getSubjectCommonName(subject: x509.Name): string | undefined { + const commonNames = subject.getField(OID_COMMON_NAME) // OID for commonName + return commonNames.length > 0 ? commonNames[0] : undefined } - static getCertificateAutoId(certificate: forge.pki.Certificate): string | undefined { - const sanExtension = certificate.getExtension('subjectAltName') - if (sanExtension) { - const san = sanExtension as CustomCertificateExtension + // static async pemPublicFromPrivateKey(privateKey: CryptoKey): Promise { + // const publicKey = await crypto.subtle.exportKey('spki', privateKey) + // return x509.PemConverter.encode(publicKey, 'PUBLIC KEY') + // } + + // static async derPublicFromPrivateKey(privateKey: CryptoKey): Promise { + // const publicKey = await crypto.subtle.exportKey('spki', privateKey) + // return Buffer.from(publicKey).toString('hex') + // } + + static getCertificateAutoId(certificate: x509.X509Certificate): string | undefined { + const sanExtension = certificate.extensions.find((ext) => ext.type === OID_SUBJECT_ALT_NAME) // OID for subjectAltName + if (sanExtension && sanExtension.value) { + const san = sanExtension.value as any // Adjust this cast as needed based on actual SAN structure for (const name of san.altNames) { if (name.type === 6 && name.value.startsWith('autoid:auto:')) { return name.value.split(':').pop() @@ -101,98 +162,115 @@ class CertificateManager { return undefined } - static pemPublicFromPrivateKey(privateKey: KeyObject): string { - const publicKey = createPublicKey(privateKey) - return publicKey.export({ type: 'spki', format: 'pem' }).toString() - } - - static derPublicFromPrivateKey(privateKey: KeyObject): string { - const publicKey = createPublicKey(privateKey) - return publicKey.export({ type: 'spki', format: 'der' }).toString() - } - - createCSR(subjectName: string): forge.pki.CertificateSigningRequest { + async createCSR(subjectName: string): Promise { const privateKey = this.privateKey - if (!privateKey) { - throw new Error('Private key is not set.') + const publicKey = this.publicKey + + if (!privateKey || !publicKey) { + throw new Error('Private or public key is not set.') } - let csr = forge.pki.createCertificationRequest() - csr.setSubject(CertificateManager.toCommonName(subjectName)) + // Set the signing algorithm based on the key type + let signingAlgorithm: Algorithm | EcdsaParams if (privateKey.asymmetricKeyType === 'ed25519') { - // Manually handle Ed25519 due to possible forge limitations - const publicKeyDer = CertificateManager.derPublicFromPrivateKey(privateKey) - - // Directly assign the public key in DER format - csr.publicKey = forge.pki.publicKeyFromAsn1(forge.asn1.fromDer(publicKeyDer)) - // csr.publicKey = forge.pki.publicKeyFromPem( - // CertificateManager.pemPublicFromPrivateKey(privateKey), - // ) + signingAlgorithm = { name: 'Ed25519' } + } else if (privateKey.asymmetricKeyType === 'rsa') { + signingAlgorithm = { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } } } else { - csr.publicKey = forge.pki.publicKeyFromPem( - CertificateManager.pemPublicFromPrivateKey(privateKey), - ) + throw new Error('Unsupported key type for signing') } + + const csr = await x509.Pkcs10CertificateRequestGenerator.create({ + name: `CN=${subjectName}`, + keys: { + privateKey: await CertificateManager.keyObjectToCryptoKey(privateKey), + publicKey: await CertificateManager.keyObjectToCryptoKey(publicKey), + }, + signingAlgorithm: signingAlgorithm, + }) + return csr } - signCSR(csr: forge.pki.CertificateSigningRequest): forge.pki.CertificateSigningRequest { - const signingParams = this.prepareSigningParams() - if (this.privateKey?.asymmetricKeyType === 'ed25519') { - // Ensure cryptographic algorithm is set to sign - // if (!csr.siginfo.algorithmOid) { - // throw new Error('Signature algorithm OID must be set before signing the CSR.') - // } + async signCSR(csr: x509.Pkcs10CertificateRequest): Promise { + const privateKey = this.privateKey + if (!privateKey) { + throw new Error('Private key is not set.') + } - // console.log('Inspecting CSR before converting to ASN.1:', csr) - const asn1 = forge.pki.certificationRequestToAsn1(csr) - const derBuffer = forge.asn1.toDer(asn1).getBytes() + const privateKeyConverted = await CertificateManager.keyObjectToCryptoKey(privateKey) - const sign = createSign('SHA256') - sign.update(derBuffer, 'binary') // Make sure the update is called with 'binary' encoding - sign.end() + const _signingParams = this.prepareSigningParams() - const signature = sign.sign(signingParams.privateKey, 'binary') - csr.signature = Buffer.from(signature, 'binary') - } else { - if (signingParams.algorithm) { - const digestMethod = forge.md[signingParams.algorithm].create() - csr.sign(forge.pki.privateKeyFromPem(keyToPem(signingParams.privateKey)), digestMethod) - } else { - throw new Error('Unsupported key type or missing algorithm.') - } - } + // FIXME: check during testing + // const derBuffer = csr.rawData + const asn1 = csr.rawData + const derBuffer = AsnConvert.serialize(asn1) + const signature = await crypto.subtle.sign( + privateKeyConverted.algorithm.name, + privateKeyConverted, + derBuffer, + ) + csr.signature = new Uint8Array(signature) return csr } - createAndSignCSR(subject_name: string): forge.pki.CertificateSigningRequest { - const csr = this.createCSR(subject_name) + async createAndSignCSR(subjectName: string): Promise { + const csr = await this.createCSR(subjectName) return this.signCSR(csr) } - issueCertificate( - csr: forge.pki.CertificateSigningRequest, + // TODO: later on move to "keyManagement.ts" + private static publicKeyToKeyObject(publicKey: x509.PublicKey): KeyObject { + // Export the key data to ArrayBuffer + const keyData = publicKey.rawData // DER format + + // Create a KeyObject from the key data + const keyObject = createPublicKey({ + key: Buffer.from(keyData), + format: 'der', + type: 'spki', + }) + + return keyObject + } + + // TODO: later on move to utils. + private static stringToArrayBuffer(str: string): ArrayBuffer { + const buffer = new ArrayBuffer(str.length) + const view = new Uint8Array(buffer) + for (let i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i) + } + return buffer + } + + async issueCertificate( + csr: x509.Pkcs10CertificateRequest, validityPeriodDays: number = 365, - ): forge.pki.Certificate { + ): Promise { const privateKey = this.privateKey - const certificate = this.certificate - if (!privateKey) { - throw new Error('Private key is not set.') + const publicKey = this.publicKey + if (!privateKey || !publicKey) { + throw new Error('Private or public key is not set.') } - let issuerName: any + let issuerName: x509.Name let autoId: string + const certificate = this.certificate if (!certificate) { - issuerName = csr.subject.attributes - autoId = blake2b_256( - stringToUint8Array(CertificateManager.getSubjectCommonName(csr.subject.attributes) || ''), - ) + issuerName = csr.subjectName + const subjectCommonName = CertificateManager.getSubjectCommonName(issuerName) + if (!subjectCommonName) { + throw new Error('Subject common name not found in CSR.') + } + autoId = blake2b_256(stringToUint8Array(subjectCommonName)) } else { if ( !doPublicKeysMatch( - createPublicKey(forge.pki.publicKeyToPem(certificate.publicKey)), - pemToPublicKey(CertificateManager.pemPublicFromPrivateKey(privateKey)), + CertificateManager.publicKeyToKeyObject(certificate.publicKey), + publicKey, ) ) { throw new Error( @@ -200,10 +278,10 @@ class CertificateManager { ) } - issuerName = certificate.subject.attributes + issuerName = certificate.subjectName const certificateAutoId = CertificateManager.getCertificateAutoId(certificate) || '' const certificateSubjectCommonName = - CertificateManager.getSubjectCommonName(certificate.subject.attributes) || '' + CertificateManager.getSubjectCommonName(certificate.subjectName) || '' if (certificateAutoId === '' || certificateSubjectCommonName === '') { throw new Error( 'Issuer certificate does not have either an auto ID or a subject common name or both.', @@ -218,86 +296,184 @@ class CertificateManager { } // Prepare the certificate builder with information from the CSR - const cert = forge.pki.createCertificate() - if (!csr.publicKey) - throw new Error('CSR does not have a public key. Please provide a CSR with a public key.') - cert.setSubject(csr.subject.attributes) - cert.setIssuer(issuerName) - cert.publicKey = csr.publicKey - cert.serialNumber = randomSerialNumber().toString() - cert.validity.notBefore = new Date() - cert.validity.notAfter = addDaysToCurrentDate(validityPeriodDays) + const notBefore = new Date() + const notAfter = new Date() + notAfter.setDate(notBefore.getDate() + validityPeriodDays) + + const privateKeyConverted = await CertificateManager.keyObjectToCryptoKey(privateKey) + + let certificateBuilder = await x509.X509CertificateGenerator.create({ + serialNumber: randomSerialNumber().toString(), + issuer: csr.subject, + subject: csr.subject, + notBefore, + notAfter, + signingAlgorithm: privateKeyConverted.algorithm, + publicKey: await CertificateManager.keyObjectToCryptoKey(publicKey), + signingKey: privateKeyConverted, + }) const autoIdSan = `autoid:auto:${Buffer.from(autoId).toString('hex')}` - let sanExtensionFound = false + // FIXME: check testing // Check for existing SAN extension - const extensions = csr.getAttribute({ name: 'extensionRequest' })?.extensions - if (extensions) { - for (const ext of extensions) { - if (ext.name === 'subjectAltName') { - sanExtensionFound = true - ext.altNames = ext.altNames || [] // Ensure altNames is initialized - ext.altNames.push({ - type: 6, // URI - value: autoIdSan, - }) - break - } - } - } + // const sanExtensions = csr.extensions + const sanExtensions = csr.extensions.filter((ext) => ext.type === OID_SUBJECT_ALT_NAME) // OID for subjectAltName + if (sanExtensions) { + // const existingSan = sanExtensions[0].value + const existingSan = sanExtensions[0] as x509.SubjectAlternativeNameExtension + const generalNames = existingSan.names.toJSON() + + // Add autoIdSan to generalNames + generalNames.push({ + type: 'uniformResourceIdentifier' as x509.GeneralNameType, + value: autoIdSan, + }) + + // const newSanExtension = existingSan + CertificateManager.stringToArrayBuffer(autoIdSan) + const newSanExtension = new x509.SubjectAlternativeNameExtension( + generalNames, + existingSan.critical, + ) + certificateBuilder.extensions.push(newSanExtension) + } else { + // certificateBuilder.extensions.push( + // new x509.SubjectAlternativeNameExtension([autoIdSan]), + // false, + // ) - // If no existing SAN extension, create one - if (!sanExtensionFound) { - cert.setExtensions([ - ...cert.extensions, - { - name: 'subjectAltName', - altNames: [{ type: 6, value: autoIdSan }], - }, - ]) + certificateBuilder.extensions.push( + new x509.SubjectAlternativeNameExtension([ + { type: 'uniformResourceIdentifier' as x509.GeneralNameType, value: autoIdSan }, + ]), + ) } // Copy all extensions from the CSR to the certificate - if (extensions) { - cert.setExtensions([...cert.extensions, ...extensions]) + for (const ext of csr.extensions) { + // certificateBuilder.extensions.push(new x509.Extension(ext.value, ext.critical)) + certificateBuilder.extensions.push(ext) } - // Sign the certificate with private key - cert.sign(forge.pki.privateKeyFromPem(keyToPem(privateKey)), forge.md.sha256.create()) + const certificateSigned = await x509.X509CertificateGenerator.create({ + serialNumber: certificateBuilder.serialNumber, + issuer: certificateBuilder.issuer, + subject: certificateBuilder.subject, + notBefore: certificateBuilder.notBefore, + notAfter: certificateBuilder.notAfter, + extensions: certificateBuilder.extensions, + publicKey: certificateBuilder.publicKey, + signingAlgorithm: certificateBuilder.signatureAlgorithm, + signingKey: privateKeyConverted, + }) - return cert + return certificateSigned } - /** - * Issues a self-signed certificate for the identity. - * - * @param subjectName Subject name for the certificate(common name). - * @param validityPeriodDays Number of days the certificate is valid. Defaults to 365. - * @returns Created X.509 certificate. - */ - selfIssueCertificate( + async selfIssueCertificate( subjectName: string, validityPeriodDays: number = 365, - ): forge.pki.Certificate { + ): Promise { + if (!this.privateKey || !this.publicKey) { + throw new Error('Private or public key is not set.') + } + + const csr = await this.createAndSignCSR(subjectName) + const certificate = await this.issueCertificate(csr, validityPeriodDays) + + this.certificate = certificate + return certificate + } + + saveCertificate(filePath: string): void { + if (!this.certificate) { + throw new Error('No certificate available to save.') + } + + const certificatePem = CertificateManager.certificateToPem(this.certificate) + fs.writeFileSync(filePath, certificatePem, 'utf8') + } +} + +export class Ed25519CertificateManager { + private certificate: x509.X509Certificate | null + private privateKey: CryptoKey | null + + constructor( + certificate: x509.X509Certificate | null = null, + privateKey: CryptoKey | null = null, + ) { + this.certificate = certificate + this.privateKey = privateKey + } + + async createAndSignCSR(subjectName: string): Promise { + if (!this.privateKey) { + throw new Error('Private key is not set.') + } + + // Export the public key (ArrayBuffer) from the private key + const publicKeyArrayBuffer = await crypto.subtle.exportKey('spki', this.privateKey) + // Create a public key (CryptoKey) from the exported public key + const publicKey = await crypto.subtle.importKey( + 'spki', + publicKeyArrayBuffer, + { + name: 'Ed25519', + }, + true, + ['verify'], + ) + + const csr = await x509.Pkcs10CertificateRequestGenerator.create({ + name: `CN=${subjectName}`, + keys: { privateKey: this.privateKey, publicKey }, + signingAlgorithm: { name: 'Ed25519' }, + }) + + return csr + } + + async issueCertificate( + csr: x509.Pkcs10CertificateRequest, + validityPeriodDays: number = 365, + ): Promise { if (!this.privateKey) { throw new Error('Private key is not set.') } - const csr = this.signCSR(this.createCSR(subjectName)) - const certificate = this.issueCertificate(csr, validityPeriodDays) + + const notBefore = new Date() + const notAfter = new Date() + notAfter.setDate(notBefore.getDate() + validityPeriodDays) + + const certificate = await x509.X509CertificateGenerator.create({ + serialNumber: randomSerialNumber().toString(), + issuer: csr.subject, + subject: csr.subject, + notBefore, + notAfter, + signingAlgorithm: { name: 'Ed25519' }, + signingKey: this.privateKey, + publicKey: csr.publicKey, + }) this.certificate = certificate - return this.certificate + return certificate + } + + async selfIssueCertificate( + subjectName: string, + validityPeriodDays: number = 365, + ): Promise { + const csr = await this.createAndSignCSR(subjectName) + return this.issueCertificate(csr, validityPeriodDays) } saveCertificate(filePath: string): void { - const certificate = this.certificate - if (!certificate) { + if (!this.certificate) { throw new Error('No certificate available to save.') } - const certificatePem = CertificateManager.certificateToPem(certificate) + const certificatePem = this.certificate.toString('pem') fs.writeFileSync(filePath, certificatePem, 'utf8') } } - -export default CertificateManager diff --git a/packages/auto-id/src/keyManagement.ts b/packages/auto-id/src/keyManagement.ts index 3530b1d4..7718d40e 100644 --- a/packages/auto-id/src/keyManagement.ts +++ b/packages/auto-id/src/keyManagement.ts @@ -19,11 +19,26 @@ export function generateRsaKeyPair(keySize: number = 2048): [string, string] { return [privateKey, publicKey] } -// export function generateRsaKeyPair(keySize: number = 2048): [string, string] { -// const { publicKey, privateKey } = forge.pki.rsa.generateKeyPair({ bits: keySize, e: 0x10001 }) +import { Crypto } from '@peculiar/webcrypto' +const crypto = new Crypto() -// return [privateKey.toString(), publicKey.toString()] -// } +export async function generateEd25519KeyPair2(): Promise<[CryptoKey, CryptoKey]> { + const keyPair = await crypto.subtle.generateKey( + { + name: 'Ed25519', + namedCurve: 'Ed25519', + }, + true, + ['sign', 'verify'], + ) + + return [keyPair.privateKey, keyPair.publicKey] +} + +export async function pemToEd25519PrivateKey(pem: string): Promise { + const binaryDer = Buffer.from(pem, 'base64') + return crypto.subtle.importKey('pkcs8', binaryDer, { name: 'Ed25519' }, false, ['sign']) +} /** * Generates an Ed25519 key pair. diff --git a/packages/auto-id/tests/certificateManager.test.ts b/packages/auto-id/tests/certificateManager.test.ts index b890c29d..fa52b161 100644 --- a/packages/auto-id/tests/certificateManager.test.ts +++ b/packages/auto-id/tests/certificateManager.test.ts @@ -1,6 +1,7 @@ +import * as x509 from '@peculiar/x509' import { createPublicKey } from 'crypto' import * as forge from 'node-forge' -import CertificateManager from '../src/certificateManager' +import { CertificateManager } from '../src/certificateManager' import { doPublicKeysMatch, generateEd25519KeyPair, @@ -9,102 +10,173 @@ import { pemToPublicKey, } from '../src/keyManagement' -describe('CertificateManager', () => { - it('create and sign CSR', () => { - // Generate a key pair - const [privateKey, _] = generateRsaKeyPair() - // TODO: Enable when Ed25519 key pair is supported by CertificateManager - // const [privateKey, _] = generateEd25519KeyPair() // Fails ❌ with error: "Cannot read public key. Unknown OID." +// describe('CertificateManager', () => { +// it('create and sign CSR', () => { +// // Generate a key pair +// const [privateKey, _] = generateRsaKeyPair() +// // TODO: Enable when Ed25519 key pair is supported by CertificateManager +// // const [privateKey, _] = generateEd25519KeyPair() // Fails ❌ with error: "Cannot read public key. Unknown OID." + +// // Instantiate CertificateManager with the generated private key +// const certificateManager = new CertificateManager(null, pemToPrivateKey(privateKey)) - // Instantiate CertificateManager with the generated private key - const certificateManager = new CertificateManager(null, pemToPrivateKey(privateKey)) +// // Create and sign a CSR +// const subjectName = 'Test' +// const csr = certificateManager.createAndSignCSR(subjectName) + +// expect(csr).not.toBeNull() + +// // NOTE: static type-checking is already done in TypeScript at compile time unlike Python. So, ignored this assertion. +// // Assert that the CSR is of type x509.CertificateSigningRequest +// // assert isinstance(csr, x509.CertificateSigningRequest) + +// // Assert that the CSR subject name matches the provided subject name +// const commonNameField = csr.subject.attributes.find((attr) => attr.name === 'commonName') +// expect(commonNameField?.value).toEqual(subjectName) + +// // Get the derived public key (in forge) from original private key. +// // private key (PEM) -> private key(KeyObject) -> public key(PEM) +// const derivedPublicKeyObj = pemToPublicKey( +// CertificateManager.pemPublicFromPrivateKey(pemToPrivateKey(privateKey)), +// ) - // Create and sign a CSR - const subjectName = 'Test' - const csr = certificateManager.createAndSignCSR(subjectName) - - expect(csr).not.toBeNull() - - // NOTE: static type-checking is already done in TypeScript at compile time unlike Python. So, ignored this assertion. - // Assert that the CSR is of type x509.CertificateSigningRequest - // assert isinstance(csr, x509.CertificateSigningRequest) - - // Assert that the CSR subject name matches the provided subject name - const commonNameField = csr.subject.attributes.find((attr) => attr.name === 'commonName') - expect(commonNameField?.value).toEqual(subjectName) - - // Get the derived public key (in forge) from original private key. - // private key (PEM) -> private key(KeyObject) -> public key(PEM) - const derivedPublicKeyObj = pemToPublicKey( - CertificateManager.pemPublicFromPrivateKey(pemToPrivateKey(privateKey)), - ) - - // Assert that the CSR public key matches the public key from the key pair - if (csr.publicKey) { - // Convert forge.PublicKey format to crypto.KeyObject - const csrPublicKeyObj = createPublicKey(forge.pki.publicKeyToPem(csr.publicKey)) - - expect(doPublicKeysMatch(csrPublicKeyObj, derivedPublicKeyObj)).toBe(true) - } else { - throw new Error('CSR does not have a public key.') - } - }) - - it('issue certificate', () => { - const [subjectPrivateKey, subjectPublicKey] = generateRsaKeyPair() - const [issuerPrivateKey, issuerPublicKey] = generateRsaKeyPair() +// // Assert that the CSR public key matches the public key from the key pair +// if (csr.publicKey) { +// // Convert forge.PublicKey format to crypto.KeyObject +// const csrPublicKeyObj = createPublicKey(forge.pki.publicKeyToPem(csr.publicKey)) + +// expect(doPublicKeysMatch(csrPublicKeyObj, derivedPublicKeyObj)).toBe(true) +// } else { +// throw new Error('CSR does not have a public key.') +// } +// }) + +// it('issue certificate', () => { +// const [subjectPrivateKey, subjectPublicKey] = generateRsaKeyPair() +// const [issuerPrivateKey, issuerPublicKey] = generateRsaKeyPair() + +// // TODO: Enable when Ed25519 key pair is supported by CertificateManager +// // const [subjectPrivateKey, subjectPublicKey] = generateRsaKeyPair() +// // const [issuerPrivateKey, issuerPublicKey] = generateRsaKeyPair() + +// const issuer = new CertificateManager(null, pemToPrivateKey(issuerPrivateKey)) +// const _issuerCertificate = issuer.selfIssueCertificate('issuer') + +// // Define the subject name for the certificate +// const subjectName = 'Test' + +// const csrCreator = new CertificateManager(null, pemToPrivateKey(subjectPrivateKey)) +// // Call the createCSR function to generate a CSR +// const csr = csrCreator.createAndSignCSR(subjectName) + +// // Issue a certificate using the CSR +// const certificate = issuer.issueCertificate(csr) + +// // Assert that the certificate is not null +// expect(certificate).not.toBeNull() + +// // NOTE: static type-checking is already done in TypeScript at compile time unlike Python. So, ignored this assertion. +// // Assert that the certificate is of type x509.Certificate +// // assert isinstance(certificate, x509.Certificate) + +// // Assert that the certificate subject name matches the provided subject name +// const commonNameField = csr.subject.attributes.find((attr) => attr.name === 'commonName') +// expect(commonNameField?.value).toEqual(subjectName) + +// // Assert that the certificate public key matches the private key's public key +// if (certificate.publicKey) { +// const certificatePublicKeyObj = createPublicKey( +// forge.pki.publicKeyToPem(certificate.publicKey), +// ) +// const subjectPublicKeyObj = createPublicKey(subjectPublicKey) + +// expect(doPublicKeysMatch(certificatePublicKeyObj, subjectPublicKeyObj)).toBe(true) +// } else { +// throw new Error('Certificate does not have a public key.') +// } + +// const certBytes = certificate.tbsCertificate +// const signature = certificate.signature +// // FIXME: Verify the certificate signature +// // issuer_public_key.verify(signature, cert_bytes) + +// // Convert the issuer's public key from PEM to a forge public key object +// // const issuerPublicKeyObj = forge.pki.publicKeyFromPem(issuerPublicKey) + +// // const tbsDer = forge.asn1.toDer(certBytes).getBytes() +// // const isValidSignature = issuerPublicKeyObj.verify(tbsDer, signature) + +// // expect(isValidSignature).toBe(true) +// }) +// }) + +// describe('Using @peculiar/x509 + forge', () => { +// // Using `@peculiar/x509` library instead of `node-forge` +// test('create and sign CSR with Ed25519', async () => { +// const keypair = await generateEd25519KeyPair2() +// const certificateManager = new CertificateManager(null, pemToPrivateKey(keypair.privateKey)) - // TODO: Enable when Ed25519 key pair is supported by CertificateManager - // const [subjectPrivateKey, subjectPublicKey] = generateRsaKeyPair() - // const [issuerPrivateKey, issuerPublicKey] = generateRsaKeyPair() +// const subjectName = 'Test' +// const csr = await certificateManager.createCSR(subjectName) +// await certificateManager.signCSR(csr) - const issuer = new CertificateManager(null, pemToPrivateKey(issuerPrivateKey)) - const _issuerCertificate = issuer.selfIssueCertificate('issuer') +// expect(csr).not.toBeNull() +// const commonNameField = csr.subject.attributes.find((attr) => attr.name === 'commonName') +// expect(commonNameField?.value).toEqual(subjectName) +// }) + +// test('issue certificate and verify signature', async () => { +// const [subjectPrivateKey, subjectPublicKey] = generateRsaKeyPair() +// const [issuerPrivateKey, issuerPublicKey] = generateRsaKeyPair() + +// const issuer = new CertificateManager(null, pemToPrivateKey(issuerPrivateKey)) +// await issuer.selfIssueCertificate('issuer') + +// const subjectName = 'Test' +// const csrCreator = new CertificateManager(null, pemToPrivateKey(subjectPrivateKey)) +// const csr = await csrCreator.createCSR(subjectName) +// await csrCreator.signCSR(csr) + +// const certificate = await issuer.issueCertificate(csr) - // Define the subject name for the certificate - const subjectName = 'Test' +// expect(certificate).not.toBeNull() +// const commonNameField = certificate.subject.attributes.find( +// (attr) => attr.name === 'commonName', +// ) +// expect(commonNameField?.value).toEqual(subjectName) - const csrCreator = new CertificateManager(null, pemToPrivateKey(subjectPrivateKey)) - // Call the createCSR function to generate a CSR - const csr = csrCreator.createAndSignCSR(subjectName) +// const issuerCert = new x509.X509Certificate(issuerPublicKey) +// const certBytes = certificate +// const isValidSignature = await issuerCert.verify(certBytes) - // Issue a certificate using the CSR - const certificate = issuer.issueCertificate(csr) +// expect(isValidSignature).toBe(true) +// }) +// }) - // Assert that the certificate is not null - expect(certificate).not.toBeNull() +import { Ed25519CertificateManager } from '../src/certificateManager' +import { generateEd25519KeyPair2 } from '../src/keyManagement' - // NOTE: static type-checking is already done in TypeScript at compile time unlike Python. So, ignored this assertion. - // Assert that the certificate is of type x509.Certificate - // assert isinstance(certificate, x509.Certificate) +describe('Ed25519CertificateManager using @peculiar/x509 only', () => { + it('create and sign CSR with Ed25519', async () => { + const [privateKey, _] = await generateEd25519KeyPair2() + const certificateManager = new Ed25519CertificateManager(null, privateKey) - // Assert that the certificate subject name matches the provided subject name - const commonNameField = csr.subject.attributes.find((attr) => attr.name === 'commonName') - expect(commonNameField?.value).toEqual(subjectName) - - // Assert that the certificate public key matches the private key's public key - if (certificate.publicKey) { - const certificatePublicKeyObj = createPublicKey( - forge.pki.publicKeyToPem(certificate.publicKey), - ) - const subjectPublicKeyObj = createPublicKey(subjectPublicKey) - - expect(doPublicKeysMatch(certificatePublicKeyObj, subjectPublicKeyObj)).toBe(true) - } else { - throw new Error('Certificate does not have a public key.') - } + const subjectName = 'Test' + const csr = await certificateManager.createAndSignCSR(subjectName) - const certBytes = certificate.tbsCertificate - const signature = certificate.signature - // FIXME: Verify the certificate signature - // issuer_public_key.verify(signature, cert_bytes) + expect(csr).not.toBeNull() + const commonNameField = csr.attributes.find((attr) => attr.type === '2.5.4.3') + expect(commonNameField?.values).toContain(subjectName) + }) - // Convert the issuer's public key from PEM to a forge public key object - const issuerPublicKeyObj = forge.pki.publicKeyFromPem(issuerPublicKey) + it('self-issue certificate with Ed25519', async () => { + const [privateKey, publicKey] = await generateEd25519KeyPair2() + const certificateManager = new Ed25519CertificateManager(null, privateKey) - const tbsDer = forge.asn1.toDer(certBytes).getBytes() - const isValidSignature = issuerPublicKeyObj.verify(tbsDer, signature) + const subjectName = 'Test' + const certificate = await certificateManager.selfIssueCertificate(subjectName) - expect(isValidSignature).toBe(true) + expect(certificate).not.toBeNull() + expect(certificate.subject).toContain(`CN=${subjectName}`) }) }) diff --git a/yarn.lock b/yarn.lock index e7562e43..35438c16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,6 +34,8 @@ __metadata: "@autonomys/auto-utils": "workspace:*" "@peculiar/asn1-schema": "npm:^2.3.8" "@peculiar/asn1-x509": "npm:^2.3.8" + "@peculiar/webcrypto": "npm:^1.5.0" + "@peculiar/x509": "npm:^1.11.0" "@types/jest": "npm:^29.5.12" "@types/node": "npm:^20.12.12" "@types/node-forge": "npm:^1" @@ -919,6 +921,97 @@ __metadata: languageName: node linkType: hard +"@peculiar/asn1-cms@npm:^2.3.8": + version: 2.3.8 + resolution: "@peculiar/asn1-cms@npm:2.3.8" + dependencies: + "@peculiar/asn1-schema": "npm:^2.3.8" + "@peculiar/asn1-x509": "npm:^2.3.8" + "@peculiar/asn1-x509-attr": "npm:^2.3.8" + asn1js: "npm:^3.0.5" + tslib: "npm:^2.6.2" + checksum: 10c0/e330c8f23df45f8bacd05a314ea62b54a375e1c3bf05875bf69a4c971ad8e4f1f389e0c0c261a770d3841dad71c8a15141ec488d8ddce74ebcfb8e63f479f02f + languageName: node + linkType: hard + +"@peculiar/asn1-csr@npm:^2.3.8": + version: 2.3.8 + resolution: "@peculiar/asn1-csr@npm:2.3.8" + dependencies: + "@peculiar/asn1-schema": "npm:^2.3.8" + "@peculiar/asn1-x509": "npm:^2.3.8" + asn1js: "npm:^3.0.5" + tslib: "npm:^2.6.2" + checksum: 10c0/1ba2414ba0679277f691e857de84b8543310af5a8e6385e2f27d672fc9179d143f9ed98cd752b775fd3d9aa4d54c2d120b157136aea0d00b1d7343cdabe74310 + languageName: node + linkType: hard + +"@peculiar/asn1-ecc@npm:^2.3.8": + version: 2.3.8 + resolution: "@peculiar/asn1-ecc@npm:2.3.8" + dependencies: + "@peculiar/asn1-schema": "npm:^2.3.8" + "@peculiar/asn1-x509": "npm:^2.3.8" + asn1js: "npm:^3.0.5" + tslib: "npm:^2.6.2" + checksum: 10c0/88a1e34e3afc9f60ec89ae6d716e7e9fe92ae841e2758fc07b997c7325e0770cf605170031113c055086b93ed3eb8d12c8f64425bad576aafe74474410bb0ae9 + languageName: node + linkType: hard + +"@peculiar/asn1-pfx@npm:^2.3.8": + version: 2.3.8 + resolution: "@peculiar/asn1-pfx@npm:2.3.8" + dependencies: + "@peculiar/asn1-cms": "npm:^2.3.8" + "@peculiar/asn1-pkcs8": "npm:^2.3.8" + "@peculiar/asn1-rsa": "npm:^2.3.8" + "@peculiar/asn1-schema": "npm:^2.3.8" + asn1js: "npm:^3.0.5" + tslib: "npm:^2.6.2" + checksum: 10c0/8591281d081e3f22aa5575908188ff0560ea218f9e30af7ed9c4d24bf34d682f3dc8091b1f78bb44cceb4b0ad3198e83bc938a0e0b56be4e274bcd7c13eaa334 + languageName: node + linkType: hard + +"@peculiar/asn1-pkcs8@npm:^2.3.8": + version: 2.3.8 + resolution: "@peculiar/asn1-pkcs8@npm:2.3.8" + dependencies: + "@peculiar/asn1-schema": "npm:^2.3.8" + "@peculiar/asn1-x509": "npm:^2.3.8" + asn1js: "npm:^3.0.5" + tslib: "npm:^2.6.2" + checksum: 10c0/33db038165f3a96a6bcbb20a2774b816a772652c4b6d67a814ecf68d3806743b75c4b2f758fecc2f5d134d18bee2864e0f42afee962e3052e4742e50e238a237 + languageName: node + linkType: hard + +"@peculiar/asn1-pkcs9@npm:^2.3.8": + version: 2.3.8 + resolution: "@peculiar/asn1-pkcs9@npm:2.3.8" + dependencies: + "@peculiar/asn1-cms": "npm:^2.3.8" + "@peculiar/asn1-pfx": "npm:^2.3.8" + "@peculiar/asn1-pkcs8": "npm:^2.3.8" + "@peculiar/asn1-schema": "npm:^2.3.8" + "@peculiar/asn1-x509": "npm:^2.3.8" + "@peculiar/asn1-x509-attr": "npm:^2.3.8" + asn1js: "npm:^3.0.5" + tslib: "npm:^2.6.2" + checksum: 10c0/6a500939366169540223570b4ff48eca4b718e40616c3cc9086bffd66ac2f6a37041a9577f029040797ad42520c3dd5e29d7e2fd35d440d5171a4f5c48a86a8d + languageName: node + linkType: hard + +"@peculiar/asn1-rsa@npm:^2.3.8": + version: 2.3.8 + resolution: "@peculiar/asn1-rsa@npm:2.3.8" + dependencies: + "@peculiar/asn1-schema": "npm:^2.3.8" + "@peculiar/asn1-x509": "npm:^2.3.8" + asn1js: "npm:^3.0.5" + tslib: "npm:^2.6.2" + checksum: 10c0/39b1a7597a210acd07c4d78bb2d55b9aedf05e3ab28fccde86ca6978322dcba35a483f2d84c155e18953ad15b728184e379010d11b01245ac2a2cbf1457a0dba + languageName: node + linkType: hard + "@peculiar/asn1-schema@npm:^2.3.8": version: 2.3.8 resolution: "@peculiar/asn1-schema@npm:2.3.8" @@ -930,6 +1023,18 @@ __metadata: languageName: node linkType: hard +"@peculiar/asn1-x509-attr@npm:^2.3.8": + version: 2.3.8 + resolution: "@peculiar/asn1-x509-attr@npm:2.3.8" + dependencies: + "@peculiar/asn1-schema": "npm:^2.3.8" + "@peculiar/asn1-x509": "npm:^2.3.8" + asn1js: "npm:^3.0.5" + tslib: "npm:^2.6.2" + checksum: 10c0/0a7c92024c326267ed16a3eb3d3956061546838acf6d8ca54bdc5c105b4139ee4cf2bffe01c3685012d1cfcf5a48c06feb7a664bd88cbeda2d61dc990dcf90d0 + languageName: node + linkType: hard + "@peculiar/asn1-x509@npm:^2.3.8": version: 2.3.8 resolution: "@peculiar/asn1-x509@npm:2.3.8" @@ -943,6 +1048,47 @@ __metadata: languageName: node linkType: hard +"@peculiar/json-schema@npm:^1.1.12": + version: 1.1.12 + resolution: "@peculiar/json-schema@npm:1.1.12" + dependencies: + tslib: "npm:^2.0.0" + checksum: 10c0/202132c66dcc6b6aca5d0af971c015be2e163da2f7f992910783c5d39c8a7db59b6ec4f4ce419459a1f954b7e1d17b6b253f0e60072c1b3d254079f4eaebc311 + languageName: node + linkType: hard + +"@peculiar/webcrypto@npm:^1.5.0": + version: 1.5.0 + resolution: "@peculiar/webcrypto@npm:1.5.0" + dependencies: + "@peculiar/asn1-schema": "npm:^2.3.8" + "@peculiar/json-schema": "npm:^1.1.12" + pvtsutils: "npm:^1.3.5" + tslib: "npm:^2.6.2" + webcrypto-core: "npm:^1.8.0" + checksum: 10c0/4f6f24b2c52c2155b9c569b6eb1d57954cb5f7bd2764a50cdaed7aea17a6dcf304b75b87b57ba318756ffec8179a07d9a76534aaf77855912b838543e5ff8983 + languageName: node + linkType: hard + +"@peculiar/x509@npm:^1.11.0": + version: 1.11.0 + resolution: "@peculiar/x509@npm:1.11.0" + dependencies: + "@peculiar/asn1-cms": "npm:^2.3.8" + "@peculiar/asn1-csr": "npm:^2.3.8" + "@peculiar/asn1-ecc": "npm:^2.3.8" + "@peculiar/asn1-pkcs9": "npm:^2.3.8" + "@peculiar/asn1-rsa": "npm:^2.3.8" + "@peculiar/asn1-schema": "npm:^2.3.8" + "@peculiar/asn1-x509": "npm:^2.3.8" + pvtsutils: "npm:^1.3.5" + reflect-metadata: "npm:^0.2.2" + tslib: "npm:^2.6.2" + tsyringe: "npm:^4.8.0" + checksum: 10c0/528b37ccc291b7d78d6ba912946fbb045cf65da325d69c8910ef5ef8e32f0eca26f52db2c8e0d36092774b3ee2ca7cf4cf677f0b0c56c9e029737388c08ca527 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -1836,7 +1982,7 @@ __metadata: languageName: node linkType: hard -"asn1js@npm:^3.0.5": +"asn1js@npm:^3.0.1, asn1js@npm:^3.0.5": version: 3.0.5 resolution: "asn1js@npm:3.0.5" dependencies: @@ -4366,6 +4512,13 @@ __metadata: languageName: node linkType: hard +"reflect-metadata@npm:^0.2.2": + version: 0.2.2 + resolution: "reflect-metadata@npm:0.2.2" + checksum: 10c0/1cd93a15ea291e420204955544637c264c216e7aac527470e393d54b4bb075f10a17e60d8168ec96600c7e0b9fcc0cb0bb6e91c3fbf5b0d8c9056f04e6ac1ec2 + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -4868,6 +5021,20 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^1.9.3": + version: 1.14.1 + resolution: "tslib@npm:1.14.1" + checksum: 10c0/69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 + languageName: node + linkType: hard + +"tslib@npm:^2.0.0, tslib@npm:^2.4.0, tslib@npm:^2.6.1": + version: 2.6.3 + resolution: "tslib@npm:2.6.3" + checksum: 10c0/2598aef53d9dbe711af75522464b2104724d6467b26a60f2bdac8297d2b5f1f6b86a71f61717384aa8fd897240467aaa7bcc36a0700a0faf751293d1331db39a + languageName: node + linkType: hard + "tslib@npm:^2.1.0, tslib@npm:^2.6.2": version: 2.6.2 resolution: "tslib@npm:2.6.2" @@ -4875,10 +5042,12 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.4.0, tslib@npm:^2.6.1": - version: 2.6.3 - resolution: "tslib@npm:2.6.3" - checksum: 10c0/2598aef53d9dbe711af75522464b2104724d6467b26a60f2bdac8297d2b5f1f6b86a71f61717384aa8fd897240467aaa7bcc36a0700a0faf751293d1331db39a +"tsyringe@npm:^4.8.0": + version: 4.8.0 + resolution: "tsyringe@npm:4.8.0" + dependencies: + tslib: "npm:^1.9.3" + checksum: 10c0/e13810e8ff39c4093acd0649bc5db3c164825827631e1522cd9d5ca8694a018447fa1c24f059ea54e93b1020767b1131b9dc9ce598dabfc9aa41c11544bbfe19 languageName: node linkType: hard @@ -5014,6 +5183,19 @@ __metadata: languageName: node linkType: hard +"webcrypto-core@npm:^1.8.0": + version: 1.8.0 + resolution: "webcrypto-core@npm:1.8.0" + dependencies: + "@peculiar/asn1-schema": "npm:^2.3.8" + "@peculiar/json-schema": "npm:^1.1.12" + asn1js: "npm:^3.0.1" + pvtsutils: "npm:^1.3.5" + tslib: "npm:^2.6.2" + checksum: 10c0/d4158af402500eb26d0de6e088baa0fbef41c43a3e3b5f53b8326c8c517e55037b3d8a17672cf48bdccfd13526599857544ea8485e2172bb14c9ee4561d706a5 + languageName: node + linkType: hard + "which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2"