@@ -4,7 +4,9 @@ import * as asn1Ocsp from '@peculiar/asn1-ocsp';
44import * as asn1X509 from '@peculiar/asn1-x509' ;
55import * as asn1Schema from '@peculiar/asn1-schema' ;
66import { createOcspResponse , parseOcspRequest , RevocationReason } from '../src/tls-certificates/ocsp.js' ;
7- import { generateCACertificate } from '../src/tls-certificates/local-ca.js' ;
7+ import { generateCACertificate , LocalCA } from '../src/tls-certificates/local-ca.js' ;
8+ import { extractLeafCertificate } from '../src/tls-certificates/cert-definitions.js' ;
9+ import { certIdHashes , parseSingleResponse , readSignedInteger , toDer } from './ocsp-helpers.js' ;
810
911const crypto = globalThis . crypto ;
1012
@@ -179,7 +181,26 @@ describe("OCSP response generation", () => {
179181
180182 // Convert the serial number to hex for comparison
181183 const serialHex = Buffer . from ( certId . serialNumber ) . toString ( 'hex' ) ;
182- expect ( serialHex . toLowerCase ( ) ) . to . equal ( cert . serialNumber . toLowerCase ( ) . replace ( / \s / g, '' ) ) ;
184+ expect ( serialHex . toLowerCase ( ) . replace ( / ^ 0 0 / , '' ) )
185+ . to . equal ( cert . serialNumber . toLowerCase ( ) . replace ( / \s / g, '' ) ) ;
186+ } ) ;
187+
188+ it ( "encodes a high-bit serial number as a positive integer" , async ( ) => {
189+ // 0xA1... has its top bit set, so without a sign pad this reads back as a negative
190+ // number - i.e. a CertID for a certificate other than the one being served.
191+ expect ( cert . serialNumber . toLowerCase ( ) ) . to . equal ( 'a123456789abcdef' ) ;
192+
193+ const response = await createOcspResponse ( {
194+ cert,
195+ issuerCert : caCert ,
196+ issuerKey : caKey ,
197+ status : 'good'
198+ } ) ;
199+
200+ const { certID } = parseSingleResponse ( response ) ;
201+
202+ expect ( readSignedInteger ( certID . serialNumber ) )
203+ . to . equal ( BigInt ( `0x${ cert . serialNumber } ` ) ) ;
183204 } ) ;
184205
185206 it ( "includes thisUpdate timestamp" , async ( ) => {
@@ -290,3 +311,86 @@ describe("OCSP response generation", () => {
290311 expect ( parsed ! . serialNumber ) . to . equal ( '010203' ) ;
291312 } ) ;
292313} ) ;
314+
315+ describe ( "OCSP responses from the local CA" , ( ) => {
316+
317+ let localCA : LocalCA ;
318+ let otherCA : LocalCA ;
319+
320+ before ( async ( ) => {
321+ localCA = await LocalCA . create ( await generateCACertificate ( ) ) ;
322+ otherCA = await LocalCA . create ( await generateCACertificate ( { commonName : 'Other CA' } ) ) ;
323+ } ) ;
324+
325+ it ( "answers for a certificate it issued, naming the issuing intermediate" , async ( ) => {
326+ const generated = await localCA . generateCertificate ( 'example.testserver.host' , { } ) ;
327+ const leafPem = extractLeafCertificate ( generated . cert ) ;
328+
329+ const response = await localCA . getOcspResponse ( toDer ( leafPem ) ) ;
330+ expect ( response ) . to . not . be . null ;
331+
332+ const { certID } = parseSingleResponse ( response ! ) ;
333+ const leaf = new x509 . X509Certificate ( leafPem ) ;
334+ const intermediate = new x509 . X509Certificate ( await localCA . getIntermediateCertificatePem ( ) ) ;
335+ const expectedHashes = await certIdHashes ( intermediate ) ;
336+
337+ expect ( leaf . issuer ) . to . equal ( intermediate . subject ) ;
338+ expect ( Buffer . from ( certID . issuerNameHash . buffer ) . toString ( 'hex' ) ) . to . equal ( expectedHashes . nameHash ) ;
339+ expect ( Buffer . from ( certID . issuerKeyHash . buffer ) . toString ( 'hex' ) ) . to . equal ( expectedHashes . keyHash ) ;
340+ expect ( readSignedInteger ( certID . serialNumber ) ) . to . equal ( BigInt ( `0x${ leaf . serialNumber } ` ) ) ;
341+ } ) ;
342+
343+ it ( "refuses to answer for a certificate issued by another CA" , async ( ) => {
344+ // This is what a real ACME-issued cert looks like to us: we have no standing to say
345+ // anything about it, so we must not staple a response naming one of our own CAs.
346+ const foreignCert = await otherCA . generateCertificate ( 'example.testserver.host' , { } ) ;
347+
348+ const response = await localCA . getOcspResponse ( toDer ( extractLeafCertificate ( foreignCert . cert ) ) ) ;
349+ expect ( response ) . to . be . null ;
350+ } ) ;
351+
352+ it ( "answers for a self-signed certificate it generated, naming the cert itself" , async ( ) => {
353+ // A self-signed cert is its own issuer, so signing with its own key is exactly what
354+ // RFC 6960 asks for - and we still hold that key.
355+ const generated = await localCA . generateCertificate ( 'example.testserver.host' , {
356+ selfSigned : true
357+ } ) ;
358+ const leafPem = extractLeafCertificate ( generated . cert ) ;
359+
360+ const response = await localCA . getOcspResponse ( toDer ( leafPem ) ) ;
361+ expect ( response ) . to . not . be . null ;
362+
363+ const { certID } = parseSingleResponse ( response ! ) ;
364+ const leaf = new x509 . X509Certificate ( leafPem ) ;
365+ const expectedHashes = await certIdHashes ( leaf ) ;
366+
367+ expect ( leaf . issuer ) . to . equal ( leaf . subject ) ;
368+ expect ( Buffer . from ( certID . issuerNameHash . buffer ) . toString ( 'hex' ) ) . to . equal ( expectedHashes . nameHash ) ;
369+ expect ( Buffer . from ( certID . issuerKeyHash . buffer ) . toString ( 'hex' ) ) . to . equal ( expectedHashes . keyHash ) ;
370+ expect ( readSignedInteger ( certID . serialNumber ) ) . to . equal ( BigInt ( `0x${ leaf . serialNumber } ` ) ) ;
371+ } ) ;
372+
373+ it ( "reports a self-signed revoked certificate as revoked" , async ( ) => {
374+ const generated = await localCA . generateCertificate ( 'revoked.testserver.host' , {
375+ selfSigned : true
376+ } ) ;
377+
378+ const response = await localCA . getOcspResponse ( toDer ( extractLeafCertificate ( generated . cert ) ) ) ;
379+ expect ( response ) . to . not . be . null ;
380+
381+ expect ( parseSingleResponse ( response ! ) . certStatus . revoked ) . to . exist ;
382+ } ) ;
383+
384+ it ( "refuses to answer for a self-signed certificate generated elsewhere" , async ( ) => {
385+ const foreignCert = await otherCA . generateCertificate ( 'example.testserver.host' , {
386+ selfSigned : true
387+ } ) ;
388+
389+ const response = await localCA . getOcspResponse ( toDer ( extractLeafCertificate ( foreignCert . cert ) ) ) ;
390+ expect ( response ) . to . be . null ;
391+ } ) ;
392+
393+ it ( "refuses to answer for data that isn't a certificate" , async ( ) => {
394+ expect ( await localCA . getOcspResponse ( Buffer . from ( 'not a certificate' ) ) ) . to . be . null ;
395+ } ) ;
396+ } ) ;
0 commit comments