|
| 1 | +import * as x509 from '@peculiar/x509'; |
| 2 | +import * as asn1Ocsp from '@peculiar/asn1-ocsp'; |
| 3 | +import * as asn1X509 from '@peculiar/asn1-x509'; |
| 4 | +import * as asn1Schema from '@peculiar/asn1-schema'; |
| 5 | + |
| 6 | +const crypto = globalThis.crypto; |
| 7 | + |
| 8 | +// OIDs |
| 9 | +const OID_SHA1 = '1.3.14.3.2.26'; |
| 10 | +const OID_SHA256_WITH_RSA = '1.2.840.113549.1.1.11'; |
| 11 | + |
| 12 | +// CRL Reasons (RFC 5280) |
| 13 | +export enum RevocationReason { |
| 14 | + unspecified = 0, |
| 15 | + keyCompromise = 1, |
| 16 | + cACompromise = 2, |
| 17 | + affiliationChanged = 3, |
| 18 | + superseded = 4, |
| 19 | + cessationOfOperation = 5, |
| 20 | + certificateHold = 6, |
| 21 | + removeFromCRL = 8, |
| 22 | + privilegeWithdrawn = 9, |
| 23 | + aACompromise = 10 |
| 24 | +} |
| 25 | + |
| 26 | +interface OcspResponseOptions { |
| 27 | + cert: x509.X509Certificate; |
| 28 | + issuerCert: x509.X509Certificate; |
| 29 | + issuerKey: CryptoKey; |
| 30 | + status: 'good' | 'revoked' | 'unknown'; |
| 31 | + revocationTime?: Date; |
| 32 | + revocationReason?: RevocationReason; |
| 33 | + thisUpdate?: Date; |
| 34 | + nextUpdate?: Date; |
| 35 | +} |
| 36 | + |
| 37 | +async function sha1Hash(data: BufferSource): Promise<ArrayBuffer> { |
| 38 | + return await crypto.subtle.digest('SHA-1', data); |
| 39 | +} |
| 40 | + |
| 41 | +function createCertId( |
| 42 | + cert: x509.X509Certificate, |
| 43 | + issuerCert: x509.X509Certificate, |
| 44 | + issuerNameHash: ArrayBuffer, |
| 45 | + issuerKeyHash: ArrayBuffer |
| 46 | +): asn1Ocsp.CertID { |
| 47 | + // Get serial number as hex string and convert to bytes |
| 48 | + const serialHex = cert.serialNumber; |
| 49 | + // Remove any spaces and ensure even length |
| 50 | + const cleanSerial = serialHex.replace(/\s/g, ''); |
| 51 | + const serialBytes = new Uint8Array( |
| 52 | + cleanSerial.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)) |
| 53 | + ); |
| 54 | + |
| 55 | + return new asn1Ocsp.CertID({ |
| 56 | + hashAlgorithm: new asn1X509.AlgorithmIdentifier({ |
| 57 | + algorithm: OID_SHA1, |
| 58 | + parameters: null |
| 59 | + }), |
| 60 | + issuerNameHash: new asn1Schema.OctetString(issuerNameHash), |
| 61 | + issuerKeyHash: new asn1Schema.OctetString(issuerKeyHash), |
| 62 | + serialNumber: serialBytes |
| 63 | + }); |
| 64 | +} |
| 65 | + |
| 66 | +function createCertStatus( |
| 67 | + status: 'good' | 'revoked' | 'unknown', |
| 68 | + revocationTime?: Date, |
| 69 | + revocationReason?: RevocationReason |
| 70 | +): asn1Ocsp.CertStatus { |
| 71 | + if (status === 'good') { |
| 72 | + // For CHOICE types, pass the selected option in the constructor |
| 73 | + return new asn1Ocsp.CertStatus({ good: null }); |
| 74 | + } else if (status === 'revoked') { |
| 75 | + // Don't set revocationReason for now - simplify to debug |
| 76 | + const revoked = new asn1Ocsp.RevokedInfo({ |
| 77 | + revocationTime: revocationTime || new Date() |
| 78 | + }); |
| 79 | + return new asn1Ocsp.CertStatus({ revoked }); |
| 80 | + } else { |
| 81 | + return new asn1Ocsp.CertStatus({ unknown: null }); |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +async function createSingleResponse( |
| 86 | + options: OcspResponseOptions, |
| 87 | + issuerNameHash: ArrayBuffer, |
| 88 | + issuerKeyHash: ArrayBuffer |
| 89 | +): Promise<asn1Ocsp.SingleResponse> { |
| 90 | + const thisUpdate = options.thisUpdate || new Date(); |
| 91 | + |
| 92 | + const singleResponse = new asn1Ocsp.SingleResponse({ |
| 93 | + certID: createCertId(options.cert, options.issuerCert, issuerNameHash, issuerKeyHash), |
| 94 | + certStatus: createCertStatus(options.status, options.revocationTime, options.revocationReason), |
| 95 | + thisUpdate |
| 96 | + }); |
| 97 | + |
| 98 | + if (options.nextUpdate) { |
| 99 | + singleResponse.nextUpdate = options.nextUpdate; |
| 100 | + } |
| 101 | + |
| 102 | + return singleResponse; |
| 103 | +} |
| 104 | + |
| 105 | +async function createResponseData( |
| 106 | + issuerCert: x509.X509Certificate, |
| 107 | + responses: asn1Ocsp.SingleResponse[] |
| 108 | +): Promise<asn1Ocsp.ResponseData> { |
| 109 | + // Parse the issuer's name from its certificate |
| 110 | + const issuerCertAsn1 = asn1Schema.AsnConvert.parse(issuerCert.rawData, asn1X509.Certificate); |
| 111 | + const issuerName = issuerCertAsn1.tbsCertificate.subject; |
| 112 | + |
| 113 | + return new asn1Ocsp.ResponseData({ |
| 114 | + responderID: new asn1Ocsp.ResponderID({ byName: issuerName }), |
| 115 | + producedAt: new Date(), |
| 116 | + responses |
| 117 | + }); |
| 118 | +} |
| 119 | + |
| 120 | +async function signResponseData( |
| 121 | + responseData: asn1Ocsp.ResponseData, |
| 122 | + issuerKey: CryptoKey |
| 123 | +): Promise<ArrayBuffer> { |
| 124 | + // Serialize the response data |
| 125 | + const responseDataDer = asn1Schema.AsnConvert.serialize(responseData); |
| 126 | + |
| 127 | + // Sign with SHA-256 + RSA |
| 128 | + const signature = await crypto.subtle.sign( |
| 129 | + { |
| 130 | + name: "RSASSA-PKCS1-v1_5", |
| 131 | + hash: "SHA-256" |
| 132 | + }, |
| 133 | + issuerKey, |
| 134 | + responseDataDer |
| 135 | + ); |
| 136 | + |
| 137 | + return signature; |
| 138 | +} |
| 139 | + |
| 140 | +async function createBasicOcspResponse( |
| 141 | + responseData: asn1Ocsp.ResponseData, |
| 142 | + issuerKey: CryptoKey, |
| 143 | + issuerCert: x509.X509Certificate |
| 144 | +): Promise<asn1Ocsp.BasicOCSPResponse> { |
| 145 | + const signature = await signResponseData(responseData, issuerKey); |
| 146 | + |
| 147 | + // Create bit string from signature ArrayBuffer |
| 148 | + const signatureBitString = new Uint8Array(signature); |
| 149 | + |
| 150 | + const basicResponse = new asn1Ocsp.BasicOCSPResponse({ |
| 151 | + tbsResponseData: responseData, |
| 152 | + signatureAlgorithm: new asn1X509.AlgorithmIdentifier({ |
| 153 | + algorithm: OID_SHA256_WITH_RSA, |
| 154 | + parameters: null |
| 155 | + }), |
| 156 | + signature: signatureBitString |
| 157 | + }); |
| 158 | + |
| 159 | + // Include the issuer certificate in the response |
| 160 | + const issuerCertDer = issuerCert.rawData; |
| 161 | + basicResponse.certs = [asn1Schema.AsnConvert.parse(issuerCertDer, asn1X509.Certificate)]; |
| 162 | + |
| 163 | + return basicResponse; |
| 164 | +} |
| 165 | + |
| 166 | +export async function createOcspResponse(options: OcspResponseOptions): Promise<Buffer> { |
| 167 | + // Hash the issuer's distinguished name |
| 168 | + const issuerCertAsn1 = asn1Schema.AsnConvert.parse(options.issuerCert.rawData, asn1X509.Certificate); |
| 169 | + const issuerNameDer = asn1Schema.AsnConvert.serialize(issuerCertAsn1.tbsCertificate.subject); |
| 170 | + const issuerNameHash = await sha1Hash(issuerNameDer); |
| 171 | + |
| 172 | + // Hash the issuer's public key |
| 173 | + const issuerPublicKeyInfo = issuerCertAsn1.tbsCertificate.subjectPublicKeyInfo; |
| 174 | + const issuerKeyBytes = new Uint8Array(issuerPublicKeyInfo.subjectPublicKey); |
| 175 | + const issuerKeyHash = await sha1Hash(issuerKeyBytes); |
| 176 | + |
| 177 | + const singleResponse = await createSingleResponse(options, issuerNameHash, issuerKeyHash); |
| 178 | + const responseData = await createResponseData(options.issuerCert, [singleResponse]); |
| 179 | + const basicResponse = await createBasicOcspResponse(responseData, options.issuerKey, options.issuerCert); |
| 180 | + |
| 181 | + // Wrap in OCSPResponse |
| 182 | + const basicResponseDer = asn1Schema.AsnConvert.serialize(basicResponse); |
| 183 | + const ocspResponse = new asn1Ocsp.OCSPResponse({ |
| 184 | + responseStatus: asn1Ocsp.OCSPResponseStatus.successful, |
| 185 | + responseBytes: new asn1Ocsp.ResponseBytes({ |
| 186 | + responseType: asn1Ocsp.id_pkix_ocsp_basic, |
| 187 | + response: new asn1Schema.OctetString(basicResponseDer) |
| 188 | + }) |
| 189 | + }); |
| 190 | + |
| 191 | + const der = asn1Schema.AsnConvert.serialize(ocspResponse); |
| 192 | + return Buffer.from(der); |
| 193 | +} |
| 194 | + |
| 195 | +// Parse an OCSP request to extract the CertID |
| 196 | +export function parseOcspRequest(requestDer: Buffer): { |
| 197 | + issuerNameHash: string; |
| 198 | + issuerKeyHash: string; |
| 199 | + serialNumber: string; |
| 200 | +} | null { |
| 201 | + try { |
| 202 | + const request = asn1Schema.AsnConvert.parse(requestDer, asn1Ocsp.OCSPRequest); |
| 203 | + const firstRequest = request.tbsRequest.requestList[0]; |
| 204 | + const certId = firstRequest.reqCert; |
| 205 | + |
| 206 | + // OctetString has a buffer property that contains the actual data |
| 207 | + const issuerNameHash = Buffer.from(certId.issuerNameHash.buffer).toString('hex'); |
| 208 | + const issuerKeyHash = Buffer.from(certId.issuerKeyHash.buffer).toString('hex'); |
| 209 | + const serialNumber = Buffer.from(certId.serialNumber).toString('hex'); |
| 210 | + |
| 211 | + return { issuerNameHash, issuerKeyHash, serialNumber }; |
| 212 | + } catch (e) { |
| 213 | + console.error('Failed to parse OCSP request:', e); |
| 214 | + return null; |
| 215 | + } |
| 216 | +} |
0 commit comments