Skip to content

Commit

Permalink
Improve issue certificate function.
Browse files Browse the repository at this point in the history
TODO: Debug verification of certificate with signature
  • Loading branch information
abhi3700 committed Jun 13, 2024
1 parent 14c7c3c commit 3834b9d
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 73 deletions.
132 changes: 97 additions & 35 deletions packages/auto-id/src/certificateManager.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down Expand Up @@ -165,67 +166,128 @@ 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)
}

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 {
Expand Down
17 changes: 17 additions & 0 deletions packages/auto-id/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ObjectIdentifier } from 'asn1js'
import { randomBytes } from 'crypto'

/**
* Encodes a given string representation of an OID into its DER format.
Expand All @@ -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
}
92 changes: 59 additions & 33 deletions packages/auto-id/tests/certificateManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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)
})
})
32 changes: 27 additions & 5 deletions packages/auto-utils/__test__/crypto.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -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)
})
})
10 changes: 10 additions & 0 deletions packages/auto-utils/src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit 3834b9d

Please sign in to comment.