From 3834b9dcac72fb38add00cfa53d82b28f806f5a3 Mon Sep 17 00:00:00 2001 From: Abhijit Roy Date: Fri, 14 Jun 2024 03:25:54 +0530 Subject: [PATCH] Improve issue certificate function. TODO: Debug verification of certificate with signature --- packages/auto-id/src/certificateManager.ts | 132 +++++++++++++----- packages/auto-id/src/utils.ts | 17 +++ .../auto-id/tests/certificateManager.test.ts | 92 +++++++----- packages/auto-utils/__test__/crypto.test.ts | 32 ++++- packages/auto-utils/src/crypto.ts | 10 ++ 5 files changed, 210 insertions(+), 73 deletions(-) diff --git a/packages/auto-id/src/certificateManager.ts b/packages/auto-id/src/certificateManager.ts index 705f9eb8..24d404af 100644 --- a/packages/auto-id/src/certificateManager.ts +++ b/packages/auto-id/src/certificateManager.ts @@ -1,11 +1,12 @@ //! For key generation, management, `keyManagement.ts` file is used i.e. "crypto" library. //! And for certificate related, used "node-forge" library. -import { blake2b_256, stringToUint8Array } from '@autonomys/auto-utils' +import { blake2b_256, concatenateUint8Arrays, stringToUint8Array } from '@autonomys/auto-utils' import { KeyObject, createPublicKey, createSign } from 'crypto' import fs from 'fs' import forge from 'node-forge' -import { keyToPem } from './keyManagement' +import { doPublicKeysMatch, keyToPem, pemToPublicKey } from './keyManagement' +import { addDaysToCurrentDate, randomSerialNumber } from './utils' interface CustomCertificateExtension { altNames: { @@ -165,7 +166,7 @@ class CertificateManager { return csr } - create_and_sign_csr(subject_name: string): forge.pki.CertificateSigningRequest { + createAndSignCSR(subject_name: string): forge.pki.CertificateSigningRequest { const csr = this.createCSR(subject_name) return this.signCSR(csr) } @@ -173,59 +174,120 @@ class CertificateManager { issueCertificate( csr: forge.pki.CertificateSigningRequest, validityPeriodDays: number = 365, - ): string { - if (!this.privateKey) { + ): forge.pki.Certificate { + const privateKey = this.privateKey + const certificate = this.certificate + if (!privateKey) { throw new Error('Private key is not set.') } - let issuerName + + let issuerName: any let autoId: string - if (!this.certificate) { + if (!certificate) { issuerName = csr.subject.attributes autoId = blake2b_256( stringToUint8Array(CertificateManager.getSubjectCommonName(csr.subject.attributes) || ''), ) } else { - issuerName = this.certificate.subject.attributes - const autoIdPrefix = CertificateManager.getCertificateAutoId(this.certificate) || '' + if ( + !doPublicKeysMatch( + createPublicKey(forge.pki.publicKeyToPem(certificate.publicKey)), + pemToPublicKey(CertificateManager.pemPublicFromPrivateKey(privateKey)), + ) + ) { + throw new Error( + 'Issuer certificate public key does not match the private key used for signing.', + ) + } + + issuerName = certificate.subject.attributes + const certificateAutoId = CertificateManager.getCertificateAutoId(certificate) || '' + const certificateSubjectCommonName = + CertificateManager.getSubjectCommonName(certificate.subject.attributes) || '' + if (certificateAutoId === '' || certificateSubjectCommonName === '') { + throw new Error( + 'Issuer certificate does not have either an auto ID or a subject common name or both.', + ) + } autoId = blake2b_256( - stringToUint8Array( - autoIdPrefix + - (CertificateManager.getSubjectCommonName(this.certificate.subject.attributes) || ''), + concatenateUint8Arrays( + stringToUint8Array(certificateAutoId), + stringToUint8Array(certificateSubjectCommonName), ), ) } + + // 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.publicKey = csr.publicKey - cert.publicKey = csr.publicKey - cert.serialNumber = new Date().getTime().toString(16) - cert.validity.notBefore = new Date() - cert.validity.notAfter = new Date() - cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + validityPeriodDays) cert.setSubject(csr.subject.attributes) cert.setIssuer(issuerName) - const attribute = csr.getAttribute({ name: 'extensionRequest' }) - if (!attribute || !attribute.extensions) { - throw new Error('CSR does not have extensions.') - } - const extensions = attribute.extensions - if (!extensions) { - throw new Error('CSR does not have extensions. Please provide a CSR with extensions.') + cert.publicKey = csr.publicKey + cert.serialNumber = randomSerialNumber().toString() + cert.validity.notBefore = new Date() + cert.validity.notAfter = addDaysToCurrentDate(validityPeriodDays) + + const autoIdSan = `autoid:auto:${Buffer.from(autoId).toString('hex')}` + let sanExtensionFound = false + + // 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 + } + } } - extensions.push({ - name: 'subjectAltName', - altNames: [ + + // If no existing SAN extension, create one + if (!sanExtensionFound) { + cert.setExtensions([ + ...cert.extensions, { - type: 6, // URI - value: `autoid:auto:${autoId}`, + name: 'subjectAltName', + altNames: [{ type: 6, value: autoIdSan }], }, - ], - }) - cert.setExtensions(extensions) + ]) + } - cert.sign(forge.pki.privateKeyFromPem(keyToPem(this.privateKey)), forge.md.sha256.create()) - return forge.pki.certificateToPem(cert) + // Copy all extensions from the CSR to the certificate + if (extensions) { + cert.setExtensions([...cert.extensions, ...extensions]) + } + + // Sign the certificate with private key + cert.sign(forge.pki.privateKeyFromPem(keyToPem(privateKey)), forge.md.sha256.create()) + + return cert + } + + /** + * 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( + subjectName: string, + validityPeriodDays: number = 365, + ): forge.pki.Certificate { + if (!this.privateKey) { + throw new Error('Private key is not set.') + } + const csr = this.signCSR(this.createCSR(subjectName)) + const certificate = this.issueCertificate(csr, validityPeriodDays) + + this.certificate = certificate + return this.certificate } saveCertificate(filePath: string): void { diff --git a/packages/auto-id/src/utils.ts b/packages/auto-id/src/utils.ts index 62093fe2..b9630f1a 100644 --- a/packages/auto-id/src/utils.ts +++ b/packages/auto-id/src/utils.ts @@ -1,4 +1,5 @@ import { ObjectIdentifier } from 'asn1js' +import { randomBytes } from 'crypto' /** * Encodes a given string representation of an OID into its DER format. @@ -21,3 +22,19 @@ export function derEncodeSignatureAlgorithmOID(oid: string): Uint8Array { return new Uint8Array([...sequenceHeader, ...new Uint8Array(berArrayBuffer), ...nullParameter]) } + +export function randomSerialNumber(): bigint { + // Generate 20 random bytes + const bytes = randomBytes(20) + // Convert bytes to a BigInt + let serial = BigInt('0x' + bytes.toString('hex')) + // Shift right by 1 to ensure the number is positive + serial = serial >> BigInt(1) + return serial +} + +export function addDaysToCurrentDate(days: number): Date { + const currentDate = new Date() // This gives you the current date and time + currentDate.setUTCDate(currentDate.getUTCDate() + days) // Adds the specified number of days + return currentDate +} diff --git a/packages/auto-id/tests/certificateManager.test.ts b/packages/auto-id/tests/certificateManager.test.ts index cc9ce4e1..b890c29d 100644 --- a/packages/auto-id/tests/certificateManager.test.ts +++ b/packages/auto-id/tests/certificateManager.test.ts @@ -10,22 +10,24 @@ import { } from '../src/keyManagement' describe('CertificateManager', () => { - it('creates and signs a CSR with an Ed25519 key', () => { - // Generate an Ed25519 key pair - const [privateKey, _] = generateEd25519KeyPair() - // const keypair = forge.pki.ed25519.generateKeyPair() - - // Define the subject name for the CSR - const subjectName = 'Test' + 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 manager = new CertificateManager(null, pemToPrivateKey(privateKey)) + const certificateManager = new CertificateManager(null, pemToPrivateKey(privateKey)) - // Create and sign CSR - const csr = manager.create_and_sign_csr(subjectName) + // Create and sign a CSR + const subjectName = 'Test' + const csr = certificateManager.createAndSignCSR(subjectName) - // Assert that the CSR is not null - expect(csr).toBeDefined() + 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') @@ -48,37 +50,61 @@ describe('CertificateManager', () => { } }) - it('create and sign CSR with RSA key', () => { - // Generate a RSA key pair - const [privateKey, _] = generateRsaKeyPair() + it('issue certificate', () => { + const [subjectPrivateKey, subjectPublicKey] = generateRsaKeyPair() + const [issuerPrivateKey, issuerPublicKey] = generateRsaKeyPair() - // Instantiate CertificateManager with the generated private key - const certificateManager = new CertificateManager(null, pemToPrivateKey(privateKey)) + // TODO: Enable when Ed25519 key pair is supported by CertificateManager + // const [subjectPrivateKey, subjectPublicKey] = generateRsaKeyPair() + // const [issuerPrivateKey, issuerPublicKey] = generateRsaKeyPair() - // Create and sign a CSR + const issuer = new CertificateManager(null, pemToPrivateKey(issuerPrivateKey)) + const _issuerCertificate = issuer.selfIssueCertificate('issuer') + + // Define the subject name for the certificate const subjectName = 'Test' - const csr = certificateManager.create_and_sign_csr(subjectName) - expect(csr).toBeDefined() + const csrCreator = new CertificateManager(null, pemToPrivateKey(subjectPrivateKey)) + // Call the createCSR function to generate a CSR + const csr = csrCreator.createAndSignCSR(subjectName) - // Assert that the CSR subject name matches the provided subject name + // 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) - // 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)) + // 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(csrPublicKeyObj, derivedPublicKeyObj)).toBe(true) + expect(doPublicKeysMatch(certificatePublicKeyObj, subjectPublicKeyObj)).toBe(true) } else { - throw new Error('CSR does not have a public key.') + 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) }) }) diff --git a/packages/auto-utils/__test__/crypto.test.ts b/packages/auto-utils/__test__/crypto.test.ts index 636aa132..31c83385 100644 --- a/packages/auto-utils/__test__/crypto.test.ts +++ b/packages/auto-utils/__test__/crypto.test.ts @@ -1,4 +1,4 @@ -import { blake2b_256, stringToUint8Array } from '../src/crypto' +import { blake2b_256, concatenateUint8Arrays, stringToUint8Array } from '../src/crypto' describe('Verify crypto functions', () => { test('Check blake2b_256 return the hash of the data', async () => { @@ -8,9 +8,31 @@ describe('Verify crypto functions', () => { expect(hash).toEqual('0xb5da441cfe72ae042ef4d2b17742907f675de4da57462d4c3609c2e2ed755970') }) - test('Check stringToUint8Array return the byte array of the string', async () => { - const message = 'Hello, world!' - const byteArray = stringToUint8Array(message) - expect(byteArray).toBeInstanceOf(Uint8Array) + test('should encode strings to Uint8Arrays and concatenate them correctly', () => { + // Define test strings + const string1 = 'Hello' + const string2 = 'World' + + // Encode strings to Uint8Arrays + const encodedString1 = stringToUint8Array(string1) + const encodedString2 = stringToUint8Array(string2) + + // Manually create expected encoded arrays if known (for illustration) + const expectedEncoded1 = new Uint8Array([72, 101, 108, 108, 111]) // ASCII values for "Hello" + const expectedEncoded2 = new Uint8Array([87, 111, 114, 108, 100]) // ASCII values for "World" + + // Test individual encoding + expect(encodedString1).toEqual(expectedEncoded1) + expect(encodedString2).toEqual(expectedEncoded2) + + // Concatenate encoded arrays + const concatenatedArrays = concatenateUint8Arrays(encodedString1, encodedString2) + + // Manually create the expected concatenated result + const expectedConcatenation = new Uint8Array([72, 101, 108, 108, 111, 87, 111, 114, 108, 100]) // Combined ASCII + + // Test concatenation result + expect(concatenatedArrays).toEqual(expectedConcatenation) + expect(concatenatedArrays.length).toBe(encodedString1.length + encodedString2.length) }) }) diff --git a/packages/auto-utils/src/crypto.ts b/packages/auto-utils/src/crypto.ts index 1dce5c74..ae513c9f 100644 --- a/packages/auto-utils/src/crypto.ts +++ b/packages/auto-utils/src/crypto.ts @@ -29,3 +29,13 @@ export function stringToUint8Array(text: string): Uint8Array { const encoder = new TextEncoder() // Create a new TextEncoder instance return encoder.encode(text) // Encode the string to a Uint8Array using UTF-8 encoding } + +/** + * Concatenates two Uint8Array instances into a single Uint8Array. + */ +export function concatenateUint8Arrays(array1: Uint8Array, array2: Uint8Array): Uint8Array { + const combined = new Uint8Array(array1.length + array2.length) + combined.set(array1) + combined.set(array2, array1.length) + return combined +}