Browse Source

VP certificates start a day back

- A virtual printer's certificate was valid only from the second it was
  made, and it is made anew at every start. A slicer whose clock ran a
  minute behind refused it and reported only "-1" when binding, which
  looks like a missing certificate
- New certificates start a day back; a printer certificate never before
  its CA. Existing CAs are kept, nothing to import again
maziggy 2 days ago
parent
commit
e50c562fa4

+ 1 - 0
CHANGELOG.md

@@ -19,6 +19,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **"Any model" jobs go to the printer with the fewest print hours (#3480, sponsor vote Q4 2026)** — When several printers of a model can take a job, the queue used to pick the first one in its list, so on a farm the same printer got most of the work. It now picks the one with the fewest print hours (the Maintenance counter, including baseline hours entered there), so jobs and wear spread across the farm. Preferred colours still come first, and the queue powers on the least used printer too when it has to switch one on with an Auto Power On plug.
 - **"Any model" jobs go to the printer with the fewest print hours (#3480, sponsor vote Q4 2026)** — When several printers of a model can take a job, the queue used to pick the first one in its list, so on a farm the same printer got most of the work. It now picks the one with the fewest print hours (the Maintenance counter, including baseline hours entered there), so jobs and wear spread across the farm. Preferred colours still come first, and the queue powers on the least used printer too when it has to switch one on with an Auto Power On plug.
 
 
 ### Fixed
 ### Fixed
+- **A slicer whose clock is a little behind can connect to a virtual printer again** — A virtual printer makes a new certificate every time Bambuddy starts, and it was valid only from that second on. Bambu Studio on a computer whose clock ran even a minute behind saw a certificate that was not valid yet, refused it, and reported only "-1" when connecting with the access code, which looks exactly like a missing certificate. New certificates now start a day back (a printer certificate never before its CA), so a restart no longer locks out such a computer. Nothing needs to be imported again.
 - **A printer prompt answered on the printer leaves the HMS list (#3487)** — Some faults, such as the A1 mini's "is the filament extruded?" prompt while loading filament, come in the printer's print_error field rather than its HMS list. Bambuddy showed them with the HMS errors but did not notice when the printer set that field back to 0, so the entry stayed on the printer card until "Clear errors" was clicked. It now goes as soon as the printer clears it, and a newer code replaces the old one. An HMS list update that arrives while such a fault is still active no longer drops it either.
 - **A printer prompt answered on the printer leaves the HMS list (#3487)** — Some faults, such as the A1 mini's "is the filament extruded?" prompt while loading filament, come in the printer's print_error field rather than its HMS list. Bambuddy showed them with the HMS errors but did not notice when the printer set that field back to 0, so the entry stayed on the printer card until "Clear errors" was clicked. It now goes as soon as the printer clears it, and a newer code replaces the old one. An HMS list update that arrives while such a fault is still active no longer drops it either.
 - **Failed and cancelled prints get their timelapse too (#3481)** — The printer saves the timelapse of a print that fails or is cancelled, up to where it stopped, and that video is the best way to see what went wrong. Bambuddy fetched timelapses only for prints that completed, so a stopped print's video stayed on the printer until it was attached by hand with **Scan for Timelapse**. It is now fetched and attached to that print's run like any other. Wiki updated.
 - **Failed and cancelled prints get their timelapse too (#3481)** — The printer saves the timelapse of a print that fails or is cancelled, up to where it stopped, and that video is the best way to see what went wrong. Bambuddy fetched timelapses only for prints that completed, so a stopped print's video stayed on the printer until it was attached by hand with **Scan for Timelapse**. It is now fetched and attached to that print's run like any other. Wiki updated.
 - **A printer named after its model no longer hides the model in the support package (#3442, reported by @daltonclift)** — The support package replaces printer names with [PRINTER]. A printer named "H3C" turned every "H3C" in the package into [PRINTER], including the model itself. A name that is only a model name or model code is not private, so it now stays; any other name is still replaced.
 - **A printer named after its model no longer hides the model in the support package (#3442, reported by @daltonclift)** — The support package replaces printer names with [PRINTER]. A printer named "H3C" turned every "H3C" in the package into [PRINTER], including the model itself. A name that is only a model name or model code is not private, so it now stays; any other name is still replaced.

+ 9 - 2
backend/app/services/virtual_printer/certificate.py

@@ -22,6 +22,12 @@ from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
+# How far back a new certificate's validity starts. A certificate valid from
+# the second it was made is refused, as not valid yet, by a client whose clock
+# is behind; the slicer then reports only "-1" when binding. The printer
+# certificate is made anew at every start, so this hit right after a restart.
+CLOCK_SKEW_ALLOWANCE = timedelta(days=1)
+
 # Default serial number for virtual printer (matches SSDP/MQTT config)
 # Default serial number for virtual printer (matches SSDP/MQTT config)
 DEFAULT_SERIAL = "00M09A391800001"
 DEFAULT_SERIAL = "00M09A391800001"
 
 
@@ -191,7 +197,7 @@ class CertificateService:
             .issuer_name(ca_name)
             .issuer_name(ca_name)
             .public_key(ca_key.public_key())
             .public_key(ca_key.public_key())
             .serial_number(x509.random_serial_number())
             .serial_number(x509.random_serial_number())
-            .not_valid_before(now)
+            .not_valid_before(now - CLOCK_SKEW_ALLOWANCE)
             .not_valid_after(now + timedelta(days=7300))  # 20 years
             .not_valid_after(now + timedelta(days=7300))  # 20 years
             .add_extension(
             .add_extension(
                 x509.BasicConstraints(ca=True, path_length=0),
                 x509.BasicConstraints(ca=True, path_length=0),
@@ -299,7 +305,8 @@ class CertificateService:
             .issuer_name(issuer)
             .issuer_name(issuer)
             .public_key(printer_key.public_key())
             .public_key(printer_key.public_key())
             .serial_number(x509.random_serial_number())
             .serial_number(x509.random_serial_number())
-            .not_valid_before(now)
+            # Never before its CA's own start: a fresh CA is backdated as much.
+            .not_valid_before(max(now - CLOCK_SKEW_ALLOWANCE, ca_cert.not_valid_before_utc))
             .not_valid_after(now + timedelta(days=3650))  # 10 years
             .not_valid_after(now + timedelta(days=3650))  # 10 years
             .add_extension(
             .add_extension(
                 x509.BasicConstraints(ca=False, path_length=None),
                 x509.BasicConstraints(ca=False, path_length=None),

+ 26 - 0
backend/tests/unit/services/test_virtual_printer.py

@@ -3799,6 +3799,32 @@ class TestCertificateService:
         leaf_akid = leaf.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier).value
         leaf_akid = leaf.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier).value
         assert leaf_akid.key_identifier == ca_skid.digest
         assert leaf_akid.key_identifier == ca_skid.digest
 
 
+    def test_certificates_tolerate_a_client_clock_that_is_behind(self, cert_service):
+        """Both certificates start a day back: one valid from the second it was
+        made was refused by a slicer whose clock was a minute behind ("-1")."""
+        from datetime import datetime, timedelta, timezone
+
+        from cryptography import x509
+
+        cert_path, _ = cert_service.generate_certificates()
+        now = datetime.now(timezone.utc)
+        ca = x509.load_pem_x509_certificate(cert_service.ca_cert_path.read_bytes())
+        leaf = x509.load_pem_x509_certificate(cert_path.read_bytes())
+        for cert in (ca, leaf):
+            assert cert.not_valid_before_utc <= now - timedelta(hours=23)
+        assert leaf.not_valid_before_utc >= ca.not_valid_before_utc
+
+    def test_printer_certificate_never_starts_before_an_existing_ca(self, tmp_path):
+        """A CA made less than a day ago, without the backdating, still bounds its leaf."""
+        from cryptography import x509
+
+        planted = self._plant_legacy_ca(tmp_path)
+        service = self._install(tmp_path, "TEST123")
+        cert_path, _ = service.generate_certificates()
+
+        leaf = x509.load_pem_x509_certificate(cert_path.read_bytes())
+        assert leaf.not_valid_before_utc == planted.not_valid_before_utc
+
     def test_existing_ca_is_reused_and_its_certificate_shape_is_unchanged(self, tmp_path):
     def test_existing_ca_is_reused_and_its_certificate_shape_is_unchanged(self, tmp_path):
         """An install that already has a CA keeps it — nothing to re-import.
         """An install that already has a CA keeps it — nothing to re-import.