test_vp_certificate_rotation.py 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. """Tests for CertificateService.ensure_certificates' CA-rotation guard.
  2. When the shared CA is regenerated (e.g. its expiry crossed
  3. ``CA_EXPIRY_THRESHOLD_DAYS``), any per-VP printer certificate that was
  4. signed by the OLD CA becomes orphaned: it still exists on disk and the
  5. old fallback ``cert_path.exists()`` check would happily reuse it. A
  6. slicer that imported the NEW CA then fails the TLS handshake because
  7. the printer cert's issuer doesn't match anything in its trust store.
  8. ``_cert_matches_current_ca`` is the guard. It compares the on-disk
  9. printer cert's issuer against the on-disk CA cert's subject; on
  10. mismatch ``ensure_certificates`` regenerates the per-VP cert under the
  11. current CA.
  12. """
  13. from backend.app.services.virtual_printer.certificate import CertificateService
  14. def test_ensure_certificates_reuses_cert_when_issuer_matches_ca(tmp_path):
  15. """Happy path: a freshly-generated CA + per-VP cert pair shares
  16. issuer/subject. ``ensure_certificates`` reads them back without
  17. regenerating."""
  18. svc = CertificateService(cert_dir=tmp_path, serial="01P00A391800001")
  19. # First call: generates the CA + per-VP cert from scratch.
  20. first_cert, first_key = svc.ensure_certificates()
  21. first_cert_bytes = first_cert.read_bytes()
  22. # Second call: cert + CA exist and the issuer matches. Should reuse.
  23. second_cert, _ = svc.ensure_certificates()
  24. assert second_cert.read_bytes() == first_cert_bytes
  25. def test_ensure_certificates_regenerates_when_ca_rotated(tmp_path):
  26. """CA rotation scenario: the CA file is replaced with a different one
  27. (e.g. previous expired and was regenerated). The per-VP cert on disk
  28. was signed by the old CA, so its issuer no longer matches the new CA's
  29. subject. ``ensure_certificates`` must regenerate the per-VP cert."""
  30. # Build the first pair.
  31. svc1 = CertificateService(cert_dir=tmp_path, serial="01P00A391800001")
  32. orig_cert_bytes = svc1.ensure_certificates()[0].read_bytes()
  33. orig_ca_bytes = svc1.ca_cert_path.read_bytes()
  34. # Simulate CA rotation: build a SECOND CA in a different dir, then
  35. # swap that CA's files into the original CA path. The per-VP cert
  36. # still on disk was signed by the original CA — issuer mismatch now.
  37. rotated_dir = tmp_path / "rotated"
  38. rotated_dir.mkdir()
  39. svc_rotated = CertificateService(cert_dir=rotated_dir, serial="01P00A391800002")
  40. svc_rotated.ensure_certificates()
  41. # Overwrite the original CA on disk with the rotated one.
  42. svc1.ca_cert_path.write_bytes(svc_rotated.ca_cert_path.read_bytes())
  43. svc1.ca_key_path.write_bytes(svc_rotated.ca_key_path.read_bytes())
  44. assert svc1.ca_cert_path.read_bytes() != orig_ca_bytes # confirm rotation
  45. # Build a fresh service against the rotated CA, then ensure_certificates
  46. # should detect the mismatch and regenerate the per-VP cert.
  47. svc2 = CertificateService(cert_dir=tmp_path, serial="01P00A391800001")
  48. new_cert, _ = svc2.ensure_certificates()
  49. new_cert_bytes = new_cert.read_bytes()
  50. # New per-VP cert must differ from the old (signed by a different CA now).
  51. assert new_cert_bytes != orig_cert_bytes
  52. def test_cert_matches_current_ca_returns_false_when_no_ca(tmp_path):
  53. """If the CA file is missing entirely, the match check must return
  54. False so ``ensure_certificates`` falls through to ``generate_certificates``
  55. instead of returning a per-VP cert that nothing can validate."""
  56. svc = CertificateService(cert_dir=tmp_path, serial="01P00A391800001")
  57. # Write a per-VP cert without a CA.
  58. svc.cert_path.write_bytes(b"fake cert content")
  59. svc.key_path.write_bytes(b"fake key content")
  60. # No bbl_ca.crt on disk → match fails safely.
  61. assert svc._cert_matches_current_ca() is False