@@ -265,8 +265,34 @@ var _ = Describe("Auth Middleware", func() {
265265 // ── Revoked client cert ────────────────────────────────────────────────────
266266
267267 Context ("revoked client cert" , func () {
268- It ("returns 403 even though the cert is CA-signed" , func () {
269- // Register the CN in the CA so Revoke can find it in inventory.
268+ It ("returns 403 when the presented cert's serial is in the CRL" , func () {
269+ // Sign a cert through the CA so its serial is tracked.
270+ csrPEM , err := testutil .GenerateCSR ("revoked-client" )
271+ Expect (err ).NotTo (HaveOccurred ())
272+ _ , err = myCA .SaveRequest ("revoked-client" , csrPEM )
273+ Expect (err ).NotTo (HaveOccurred ())
274+ certPEM , err := myCA .Sign ("revoked-client" )
275+ Expect (err ).NotTo (HaveOccurred ())
276+
277+ // Parse the issued cert so we can present it in the TLS request.
278+ block , _ := pem .Decode (certPEM )
279+ issuedCert , err := x509 .ParseCertificate (block .Bytes )
280+ Expect (err ).NotTo (HaveOccurred ())
281+
282+ // Revoke the cert — its serial is now in the CRL.
283+ Expect (myCA .Revoke ("revoked-client" )).To (Succeed ())
284+
285+ // Present the revoked cert; the middleware checks its serial
286+ // directly against the CRL and must deny access.
287+ req := httptest .NewRequest ("GET" , "/certificate_request/revoked-client" , nil )
288+ req = withClientCert (req , issuedCert )
289+ rr := httptest .NewRecorder ()
290+ mux .ServeHTTP (rr , req )
291+ Expect (rr .Code ).To (Equal (http .StatusForbidden ))
292+ })
293+
294+ It ("allows a cert whose serial is NOT in the CRL even when another cert for the same CN was revoked" , func () {
295+ // Sign and revoke "revoked-client".
270296 csrPEM , err := testutil .GenerateCSR ("revoked-client" )
271297 Expect (err ).NotTo (HaveOccurred ())
272298 _ , err = myCA .SaveRequest ("revoked-client" , csrPEM )
@@ -275,14 +301,90 @@ var _ = Describe("Auth Middleware", func() {
275301 Expect (err ).NotTo (HaveOccurred ())
276302 Expect (myCA .Revoke ("revoked-client" )).To (Succeed ())
277303
278- // Issue a fresh TLS cert with the revoked CN.
279- // IsRevoked looks up the on-disk cert for the CN, reads its serial
280- // number, and checks whether that serial is in the CRL. The TLS-
281- // presented cert's serial is not consulted; only the CN is used to
282- // locate the revoked record on disk.
283- clientCert := issueClientCert ("revoked-client" , caCert , caKey )
304+ // A separately-issued cert with the same CN but a different serial
305+ // (not in the CRL) must pass the revocation check.
306+ freshCert := issueClientCert ("revoked-client" , caCert , caKey )
284307 req := httptest .NewRequest ("GET" , "/certificate_request/revoked-client" , nil )
285- req = withClientCert (req , clientCert )
308+ req = withClientCert (req , freshCert )
309+ rr := httptest .NewRecorder ()
310+ mux .ServeHTTP (rr , req )
311+ // The cert is not revoked — access is denied only if it also
312+ // fails the tier check (self-or-admin: CN matches path subject).
313+ Expect (rr .Code ).NotTo (Equal (http .StatusForbidden ))
314+ })
315+ })
316+
317+ // ── Revocation bypass prevention (re-issuance regression) ─────────────────
318+ // Before the fix, IsRevoked looked up the cert *on disk* for the CN and
319+ // checked that cert's serial. After a revocation + re-issuance the disk
320+ // cert had a new (clean) serial, so the old revoked cert would pass.
321+ // IsRevokedSerial checks the serial of the PRESENTED cert, closing the gap.
322+
323+ Context ("revocation bypass prevention after re-issuance" , func () {
324+ It ("denies an old revoked cert even after the same CN has been re-issued" , func () {
325+ // Step 1: issue the first cert for "puppet-server" (admin CN).
326+ csrPEM1 , err := testutil .GenerateCSR ("puppet-server" )
327+ Expect (err ).NotTo (HaveOccurred ())
328+ _ , err = myCA .SaveRequest ("puppet-server" , csrPEM1 )
329+ Expect (err ).NotTo (HaveOccurred ())
330+ certPEM1 , err := myCA .Sign ("puppet-server" )
331+ Expect (err ).NotTo (HaveOccurred ())
332+ block1 , _ := pem .Decode (certPEM1 )
333+ oldCert , err := x509 .ParseCertificate (block1 .Bytes )
334+ Expect (err ).NotTo (HaveOccurred ())
335+
336+ // Step 2: revoke it — serial1 is now in the CRL.
337+ Expect (myCA .Revoke ("puppet-server" )).To (Succeed ())
338+
339+ // Step 3: re-register and sign a new cert for the same CN.
340+ csrPEM2 , err := testutil .GenerateCSR ("puppet-server" )
341+ Expect (err ).NotTo (HaveOccurred ())
342+ _ , err = myCA .SaveRequest ("puppet-server" , csrPEM2 ) // evicts the revoked cert
343+ Expect (err ).NotTo (HaveOccurred ())
344+ certPEM2 , err := myCA .Sign ("puppet-server" )
345+ Expect (err ).NotTo (HaveOccurred ())
346+ block2 , _ := pem .Decode (certPEM2 )
347+ newCert , err := x509 .ParseCertificate (block2 .Bytes )
348+ Expect (err ).NotTo (HaveOccurred ())
349+
350+ // OLD cert (revoked serial) must be denied — regression test.
351+ req := httptest .NewRequest ("POST" , "/sign/all" , nil )
352+ req = withClientCert (req , oldCert )
353+ rr := httptest .NewRecorder ()
354+ mux .ServeHTTP (rr , req )
355+ Expect (rr .Code ).To (Equal (http .StatusForbidden ))
356+
357+ // NEW cert (clean serial, same admin CN) must be allowed.
358+ req2 := httptest .NewRequest ("POST" , "/sign/all" , nil )
359+ req2 = withClientCert (req2 , newCert )
360+ rr2 := httptest .NewRecorder ()
361+ mux .ServeHTTP (rr2 , req2 )
362+ Expect (rr2 .Code ).NotTo (Equal (http .StatusForbidden ))
363+ })
364+ })
365+
366+ // ── CRL unavailable → fail-closed ─────────────────────────────────────────
367+
368+ Context ("CRL unavailable" , func () {
369+ It ("returns 403 when the CRL file cannot be read (fail-closed)" , func () {
370+ // Sign a cert through the CA so it is a valid client cert.
371+ csrPEM , err := testutil .GenerateCSR ("crl-test-node" )
372+ Expect (err ).NotTo (HaveOccurred ())
373+ _ , err = myCA .SaveRequest ("crl-test-node" , csrPEM )
374+ Expect (err ).NotTo (HaveOccurred ())
375+ certPEM , err := myCA .Sign ("crl-test-node" )
376+ Expect (err ).NotTo (HaveOccurred ())
377+ block , _ := pem .Decode (certPEM )
378+ issuedCert , err := x509 .ParseCertificate (block .Bytes )
379+ Expect (err ).NotTo (HaveOccurred ())
380+
381+ // Remove the CRL file to simulate a disk fault.
382+ Expect (os .Remove (store .CRLPath ())).To (Succeed ())
383+
384+ // The middleware must deny the request (fail-closed) rather than
385+ // allowing access because it cannot check revocation status.
386+ req := httptest .NewRequest ("GET" , "/certificate_request/crl-test-node" , nil )
387+ req = withClientCert (req , issuedCert )
286388 rr := httptest .NewRecorder ()
287389 mux .ServeHTTP (rr , req )
288390 Expect (rr .Code ).To (Equal (http .StatusForbidden ))
0 commit comments