Skip to content

Commit 0578bd5

Browse files
authored
Merge pull request #28 from subspace/fix-issue-16-peculiar-x509
2 parents 6aef6f0 + 441c732 commit 0578bd5

File tree

10 files changed

+779
-15
lines changed

10 files changed

+779
-15
lines changed

packages/auto-id/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"@autonomys/auto-utils": "workspace:*",
1313
"@peculiar/asn1-schema": "^2.3.8",
1414
"@peculiar/asn1-x509": "^2.3.8",
15+
"@peculiar/webcrypto": "^1.5.0",
16+
"@peculiar/x509": "^1.11.0",
1517
"asn1js": "^3.0.5"
1618
},
1719
"files": [
@@ -26,4 +28,4 @@
2628
"ts-node": "^10.9.2",
2729
"typescript": "^5.4.5"
2830
}
29-
}
31+
}
+344
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
//! For key generation, management, `keyManagement.ts` file is used using "crypto" library.
2+
//! And for certificate related, used "@peculiar/x509" library.
3+
4+
import {
5+
blake2b_256,
6+
concatenateUint8Arrays,
7+
save,
8+
stringToUint8Array,
9+
} from '@autonomys/auto-utils'
10+
import { AsnConvert } from '@peculiar/asn1-schema'
11+
import { AttributeTypeAndValue, GeneralNames } from '@peculiar/asn1-x509'
12+
import { Crypto } from '@peculiar/webcrypto'
13+
import * as x509 from '@peculiar/x509'
14+
import { KeyObject, createPublicKey } from 'crypto'
15+
import { doPublicKeysMatch, pemToPublicKey } from './keyManagement'
16+
17+
const crypto = new Crypto()
18+
x509.cryptoProvider.set(crypto)
19+
20+
interface SigningParams {
21+
privateKey: CryptoKey
22+
algorithm: 'sha256' | null // Only 'sha256' or null for Ed25519
23+
}
24+
25+
export const OID_COMMON_NAME = '2.5.4.3' // OID for Common Name, not available in the library.
26+
const OID_SUBJECT_ALT_NAME = '2.5.29.17' // OID for Subject Alternative Name, not available in the library.
27+
28+
export class CertificateManager {
29+
private certificate: x509.X509Certificate | null
30+
private privateKey: CryptoKey | null
31+
private publicKey: CryptoKey | null
32+
33+
constructor(
34+
certificate: x509.X509Certificate | null = null,
35+
privateKey: CryptoKey | null = null,
36+
publicKey: CryptoKey | null = null,
37+
) {
38+
this.certificate = certificate
39+
this.privateKey = privateKey
40+
this.publicKey = publicKey
41+
}
42+
43+
protected prepareSigningParams(): SigningParams {
44+
const privateKey = this.privateKey
45+
46+
if (!privateKey) {
47+
throw new Error('Private key is not set.')
48+
}
49+
50+
if (privateKey.algorithm.name === 'Ed25519') {
51+
return { privateKey: privateKey, algorithm: null }
52+
}
53+
54+
if (privateKey.algorithm.name === 'rsa') {
55+
return { privateKey: privateKey, algorithm: 'sha256' }
56+
}
57+
58+
throw new Error('Unsupported key type for signing.')
59+
}
60+
61+
protected static toCommonName(subjectName: string): x509.Name {
62+
const commonNameAttr = new AttributeTypeAndValue({
63+
type: OID_COMMON_NAME,
64+
value: subjectName,
65+
})
66+
return new x509.Name([[commonNameAttr]])
67+
}
68+
69+
static prettyPrintCertificate(cert: x509.X509Certificate): void {
70+
console.log('Certificate:')
71+
console.log('============')
72+
console.log(`Subject: ${cert.subject}`)
73+
console.log(`Issuer: ${cert.issuer}`)
74+
console.log(`Serial Number: ${cert.serialNumber}`)
75+
console.log(`Not Valid Before: ${cert.notBefore}`)
76+
console.log(`Not Valid After: ${cert.notAfter}`)
77+
78+
console.log('\nExtensions:')
79+
cert.extensions.forEach((ext) => {
80+
console.log(` - ${ext.type}: ${JSON.stringify(ext.value)}`)
81+
})
82+
console.log('\nPublic Key:')
83+
console.log(cert.publicKey)
84+
}
85+
86+
static certificateToPem(cert: x509.X509Certificate): string {
87+
return cert.toString('pem')
88+
}
89+
90+
static pemToCertificate(pem: string): x509.X509Certificate {
91+
return new x509.X509Certificate(pem)
92+
}
93+
94+
static getSubjectCommonName(subject: x509.Name): string | undefined {
95+
const commonNames = subject.getField(OID_COMMON_NAME) // OID for commonName
96+
return commonNames.length > 0 ? commonNames[0] : undefined
97+
}
98+
99+
static getCertificateAutoId(certificate: x509.X509Certificate): string | undefined {
100+
const sanExtension = certificate.extensions.find((ext) => ext.type === OID_SUBJECT_ALT_NAME)
101+
102+
if (sanExtension && sanExtension.value) {
103+
// Deserialize the ArrayBuffer to GeneralNames ASN.1 object
104+
const san = AsnConvert.parse(sanExtension.value, GeneralNames)
105+
106+
for (const name of san) {
107+
if (
108+
name.uniformResourceIdentifier &&
109+
name.uniformResourceIdentifier.startsWith('autoid:auto:')
110+
) {
111+
return name.uniformResourceIdentifier.split(':').pop()
112+
}
113+
}
114+
}
115+
return undefined
116+
}
117+
118+
async createCSR(subjectName: string): Promise<x509.Pkcs10CertificateRequest> {
119+
const privateKey = this.privateKey
120+
const publicKey = this.publicKey
121+
122+
if (!privateKey || !publicKey) {
123+
throw new Error('Private or public key is not set.')
124+
}
125+
126+
// Set the signing algorithm based on the key type
127+
let signingAlgorithm: Algorithm | EcdsaParams
128+
if (privateKey.algorithm.name === 'Ed25519') {
129+
signingAlgorithm = { name: 'Ed25519' }
130+
} else if (privateKey.algorithm.name === 'rsa') {
131+
signingAlgorithm = { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } }
132+
} else {
133+
throw new Error('Unsupported key type for signing')
134+
}
135+
136+
const csr = await x509.Pkcs10CertificateRequestGenerator.create({
137+
name: `CN=${subjectName}`,
138+
keys: {
139+
privateKey: privateKey,
140+
publicKey: publicKey,
141+
},
142+
signingAlgorithm: signingAlgorithm,
143+
})
144+
145+
return csr
146+
}
147+
148+
async signCSR(csr: x509.Pkcs10CertificateRequest): Promise<x509.Pkcs10CertificateRequest> {
149+
const privateKey = this.privateKey
150+
if (!privateKey) {
151+
throw new Error('Private key is not set.')
152+
}
153+
154+
const _signingParams = this.prepareSigningParams()
155+
156+
const derBuffer = csr.rawData
157+
const signature = await crypto.subtle.sign(privateKey.algorithm.name, privateKey, derBuffer)
158+
csr.signature = new Uint8Array(signature)
159+
160+
return csr
161+
}
162+
163+
async createAndSignCSR(subjectName: string): Promise<x509.Pkcs10CertificateRequest> {
164+
const csr = await this.createCSR(subjectName)
165+
return this.signCSR(csr)
166+
}
167+
168+
// TODO: later on move to "keyManagement.ts"
169+
private static publicKeyToKeyObject(publicKey: x509.PublicKey): KeyObject {
170+
// Export the key data to ArrayBuffer
171+
const keyData = publicKey.rawData // DER format
172+
173+
// Create a KeyObject from the key data
174+
const keyObject = createPublicKey({
175+
key: Buffer.from(keyData),
176+
format: 'der',
177+
type: 'spki',
178+
})
179+
180+
return keyObject
181+
}
182+
183+
async issueCertificate(
184+
csr: x509.Pkcs10CertificateRequest,
185+
validityPeriodDays: number = 365,
186+
): Promise<x509.X509Certificate> {
187+
const privateKey = this.privateKey
188+
const publicKey = this.publicKey
189+
if (!privateKey || !publicKey) {
190+
throw new Error('Private or public key is not set.')
191+
}
192+
193+
let issuerName: x509.Name
194+
let autoId: string
195+
const certificate = this.certificate
196+
if (!certificate) {
197+
issuerName = csr.subjectName
198+
const subjectCommonName = CertificateManager.getSubjectCommonName(issuerName)
199+
if (!subjectCommonName) {
200+
throw new Error('Subject common name not found in CSR.')
201+
}
202+
autoId = blake2b_256(stringToUint8Array(subjectCommonName))
203+
} else {
204+
if (
205+
// FIXME: modify
206+
!doPublicKeysMatch(
207+
CertificateManager.publicKeyToKeyObject(certificate.publicKey),
208+
pemToPublicKey(await cryptoKeyToPem(publicKey)),
209+
)
210+
) {
211+
throw new Error(
212+
'Issuer certificate public key does not match the private key used for signing.',
213+
)
214+
}
215+
216+
issuerName = certificate.subjectName
217+
const certificateAutoId = CertificateManager.getCertificateAutoId(certificate) || ''
218+
const certificateSubjectCommonName =
219+
CertificateManager.getSubjectCommonName(certificate.subjectName) || ''
220+
if (certificateAutoId === '' || certificateSubjectCommonName === '') {
221+
throw new Error(
222+
'Issuer certificate does not have either an auto ID or a subject common name or both.',
223+
)
224+
}
225+
autoId = blake2b_256(
226+
concatenateUint8Arrays(
227+
stringToUint8Array(certificateAutoId),
228+
stringToUint8Array(certificateSubjectCommonName),
229+
),
230+
)
231+
}
232+
233+
// Prepare the certificate builder with information from the CSR
234+
const notBefore = new Date()
235+
const notAfter = new Date()
236+
notAfter.setDate(notBefore.getDate() + validityPeriodDays)
237+
238+
let certificateBuilder = await x509.X509CertificateGenerator.create({
239+
issuer: csr.subject,
240+
subject: csr.subject,
241+
notBefore,
242+
notAfter,
243+
signingAlgorithm: privateKey.algorithm,
244+
publicKey: publicKey,
245+
signingKey: privateKey,
246+
})
247+
248+
const autoIdSan = `autoid:auto:${Buffer.from(autoId).toString('hex')}`
249+
250+
const sanExtensions = csr.extensions.filter((ext) => ext.type === OID_SUBJECT_ALT_NAME) // OID for subjectAltName
251+
if (sanExtensions.length) {
252+
// const existingSan = sanExtensions[0].value
253+
const existingSan = sanExtensions[0] as x509.SubjectAlternativeNameExtension
254+
255+
const generalNames = existingSan.names.toJSON()
256+
257+
// Add autoIdSan to generalNames
258+
generalNames.push({
259+
type: 'url' as x509.GeneralNameType,
260+
value: autoIdSan,
261+
})
262+
263+
// const newSanExtension = existingSan + CertificateManager.stringToArrayBuffer(autoIdSan)
264+
const newSanExtension = new x509.SubjectAlternativeNameExtension(
265+
generalNames,
266+
existingSan.critical,
267+
)
268+
certificateBuilder.extensions.push(newSanExtension)
269+
} else {
270+
// certificateBuilder.extensions.push(
271+
// new x509.SubjectAlternativeNameExtension([autoIdSan]),
272+
// false,
273+
// )
274+
275+
certificateBuilder.extensions.push(
276+
new x509.SubjectAlternativeNameExtension([
277+
{ type: 'url' /* as x509.GeneralNameType */, value: autoIdSan },
278+
]),
279+
)
280+
}
281+
282+
// Copy all extensions from the CSR to the certificate
283+
for (const ext of csr.extensions) {
284+
// certificateBuilder.extensions.push(new x509.Extension(ext.value, ext.critical))
285+
certificateBuilder.extensions.push(ext)
286+
}
287+
288+
const certificateSigned = await x509.X509CertificateGenerator.create({
289+
serialNumber: certificateBuilder.serialNumber,
290+
issuer: certificateBuilder.issuer,
291+
subject: certificateBuilder.subject,
292+
notBefore: certificateBuilder.notBefore,
293+
notAfter: certificateBuilder.notAfter,
294+
extensions: certificateBuilder.extensions,
295+
publicKey: certificateBuilder.publicKey,
296+
signingAlgorithm: certificateBuilder.signatureAlgorithm,
297+
signingKey: privateKey,
298+
})
299+
300+
return certificateSigned
301+
}
302+
303+
async selfIssueCertificate(
304+
subjectName: string,
305+
validityPeriodDays: number = 365,
306+
): Promise<x509.X509Certificate> {
307+
if (!this.privateKey || !this.publicKey) {
308+
throw new Error('Private or public key is not set.')
309+
}
310+
311+
const csr = await this.createAndSignCSR(subjectName)
312+
const certificate = await this.issueCertificate(csr, validityPeriodDays)
313+
314+
this.certificate = certificate
315+
return certificate
316+
}
317+
318+
async saveCertificate(filePath: string): Promise<void> {
319+
if (!this.certificate) {
320+
throw new Error('No certificate available to save.')
321+
}
322+
323+
const certificatePem = CertificateManager.certificateToPem(this.certificate)
324+
await save(filePath, certificatePem)
325+
}
326+
}
327+
328+
function arrayBufferToBase64(buffer: ArrayBuffer): string {
329+
let binary = ''
330+
const bytes = new Uint8Array(buffer)
331+
const len = bytes.byteLength
332+
for (let i = 0; i < len; i++) {
333+
binary += String.fromCharCode(bytes[i])
334+
}
335+
return btoa(binary)
336+
}
337+
338+
async function cryptoKeyToPem(key: CryptoKey): Promise<string> {
339+
const exported = await crypto.subtle.exportKey(key.type === 'private' ? 'pkcs8' : 'spki', key)
340+
const base64 = arrayBufferToBase64(exported)
341+
const type = key.type === 'private' ? 'PRIVATE KEY' : 'PUBLIC KEY'
342+
const pem = `-----BEGIN ${type}-----\n${base64.match(/.{1,64}/g)?.join('\n')}\n-----END ${type}-----`
343+
return pem
344+
}

packages/auto-id/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
export * from './certificateManager'
12
export * from './keyManagement'
23
export * from './utils'

0 commit comments

Comments
 (0)