Skip to content

Commit

Permalink
Create CSR passes for RSA, but not Ed25519
Browse files Browse the repository at this point in the history
Need to debug the issue.
  • Loading branch information
abhi3700 committed Jun 13, 2024
1 parent e525d7a commit 14c7c3c
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 3 deletions.
4 changes: 3 additions & 1 deletion packages/auto-id/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"@autonomys/auto-utils": "workspace:*",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/asn1-x509": "^2.3.8",
"asn1js": "^3.0.5"
"asn1js": "^3.0.5",
"node-forge": "^1.3.1"
},
"files": [
"dist",
Expand All @@ -21,6 +22,7 @@
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^20.12.12",
"@types/node-forge": "^1",
"jest": "^29.7.0",
"ts-jest": "^29.1.4",
"ts-node": "^10.9.2",
Expand Down
241 changes: 241 additions & 0 deletions packages/auto-id/src/certificateManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
//! 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 { KeyObject, createPublicKey, createSign } from 'crypto'
import fs from 'fs'
import forge from 'node-forge'
import { keyToPem } from './keyManagement'

interface CustomCertificateExtension {
altNames: {
type: number
value: string
}[]
}

interface SigningParams {
privateKey: KeyObject
algorithm: 'sha256' | null // Only 'sha256' or null for Ed25519
}

class CertificateManager {
private certificate: forge.pki.Certificate | null
private privateKey: KeyObject | null

constructor(
certificate: forge.pki.Certificate | null = null,
privateKey: KeyObject | null = null,
) {
this.certificate = certificate
this.privateKey = privateKey
}

protected prepareSigningParams(): SigningParams {
const privateKey = this.privateKey
if (!privateKey) {
throw new Error('Private key is not set.')
}

if (privateKey.asymmetricKeyType === 'ed25519') {
return { privateKey: privateKey, algorithm: null }
}
if (privateKey.asymmetricKeyType === 'rsa') {
return { privateKey: privateKey, algorithm: 'sha256' }
}

throw new Error('Unsupported key type for signing.')
}

static toCommonName(subjectName: string): forge.pki.CertificateField[] {
return [{ name: 'commonName', value: subjectName }]
}

static prettyPrintCertificate(cert: forge.pki.Certificate): 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(`Serial Number: ${cert.serialNumber}`)
console.log(`Not Valid Before: ${cert.validity.notBefore.toISOString()}`)
console.log(`Not Valid After: ${cert.validity.notAfter.toISOString()}`)
console.log('\nExtensions:')
cert.extensions.forEach((ext) => {
console.log(` - ${ext.name} (${ext.id}): ${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 pemToCertificate(pem: string): forge.pki.Certificate {
return forge.pki.certificateFromPem(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 getCertificateAutoId(certificate: forge.pki.Certificate): string | undefined {
const sanExtension = certificate.getExtension('subjectAltName')
if (sanExtension) {
const san = sanExtension as CustomCertificateExtension
for (const name of san.altNames) {
if (name.type === 6 && name.value.startsWith('autoid:auto:')) {
return name.value.split(':').pop()
}
}
}
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 {
const privateKey = this.privateKey
if (!privateKey) {
throw new Error('Private key is not set.')
}
let csr = forge.pki.createCertificationRequest()
csr.setSubject(CertificateManager.toCommonName(subjectName))

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),
// )
} else {
csr.publicKey = forge.pki.publicKeyFromPem(
CertificateManager.pemPublicFromPrivateKey(privateKey),
)
}
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.')
// }

// console.log('Inspecting CSR before converting to ASN.1:', csr)
const asn1 = forge.pki.certificationRequestToAsn1(csr)
const derBuffer = forge.asn1.toDer(asn1).getBytes()

const sign = createSign('SHA256')
sign.update(derBuffer, 'binary') // Make sure the update is called with 'binary' encoding
sign.end()

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.')
}
}

return csr
}

create_and_sign_csr(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) {
throw new Error('Private key is not set.')
}
let issuerName
let autoId: string
if (!this.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) || ''
autoId = blake2b_256(
stringToUint8Array(
autoIdPrefix +
(CertificateManager.getSubjectCommonName(this.certificate.subject.attributes) || ''),
),
)
}
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.')
}
extensions.push({
name: 'subjectAltName',
altNames: [
{
type: 6, // URI
value: `autoid:auto:${autoId}`,
},
],
})
cert.setExtensions(extensions)

cert.sign(forge.pki.privateKeyFromPem(keyToPem(this.privateKey)), forge.md.sha256.create())
return forge.pki.certificateToPem(cert)
}

saveCertificate(filePath: string): void {
const certificate = this.certificate
if (!certificate) {
throw new Error('No certificate available to save.')
}
const certificatePem = CertificateManager.certificateToPem(certificate)
fs.writeFileSync(filePath, certificatePem, 'utf8')
}
}

export default CertificateManager
1 change: 1 addition & 0 deletions packages/auto-id/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './certificateManager'
export * from './keyManagement'
export * from './utils'
16 changes: 14 additions & 2 deletions packages/auto-id/src/keyManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ 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 })

// return [privateKey.toString(), publicKey.toString()]
// }

/**
* Generates an Ed25519 key pair.
* @returns A tuple containing the Ed25519 private key and public key.
Expand All @@ -32,6 +38,12 @@ export function generateEd25519KeyPair(): [string, string] {
return [privateKey, publicKey]
}

// export function generateEd25519KeyPair(): [string, string] {
// const { privateKey, publicKey } = forge.pki.ed25519.generateKeyPair()

// return [privateKey.toString(), publicKey.toString()]
// }

/**
* Converts a cryptographic key object into a PEM formatted string.
* This function can handle both private and public key objects.
Expand Down Expand Up @@ -167,7 +179,7 @@ export async function loadPrivateKey(filePath: string, password?: string): Promi
try {
const keyData = await read(filePath)
const privateKey = pemToPrivateKey(keyData, password)
return privateKey;
return privateKey
} catch (error: any) {
throw new Error(`Failed to load private key: ${error.message}`)
}
Expand Down Expand Up @@ -286,4 +298,4 @@ export function doPublicKeysMatch(publicKey1: KeyObject, publicKey2: KeyObject):

// Compare the serialized public key data
return publicKey1Der.equals(publicKey2Der)
}
}
84 changes: 84 additions & 0 deletions packages/auto-id/tests/certificateManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { createPublicKey } from 'crypto'
import * as forge from 'node-forge'
import CertificateManager from '../src/certificateManager'
import {
doPublicKeysMatch,
generateEd25519KeyPair,
generateRsaKeyPair,
pemToPrivateKey,
pemToPublicKey,
} 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'

// Instantiate CertificateManager with the generated private key
const manager = new CertificateManager(null, pemToPrivateKey(privateKey))

// Create and sign CSR
const csr = manager.create_and_sign_csr(subjectName)

// Assert that the CSR is not null
expect(csr).toBeDefined()

// 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('create and sign CSR with RSA key', () => {
// Generate a RSA key pair
const [privateKey, _] = generateRsaKeyPair()

// 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.create_and_sign_csr(subjectName)

expect(csr).toBeDefined()

// 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.')
}
})
})
Loading

0 comments on commit 14c7c3c

Please sign in to comment.