From 0006133b73e6a5c8edab94e55ee92cbdfd6e22be Mon Sep 17 00:00:00 2001 From: peppelinux Date: Thu, 11 Jun 2026 14:37:36 +0200 Subject: [PATCH] fix: datetime verification evaluation about UTC in issuance and expiration --- pymdoccbor/mso/issuer.py | 21 +++++++++++----- pymdoccbor/tests/test_06_mso_issuer.py | 33 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index ae6fb8a..05be268 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -164,6 +164,19 @@ def format_datetime_repr(self, dt: datetime.datetime) -> str: """ return dt.isoformat().split(".")[0] + "Z" + @staticmethod + def parse_date(date_str: str, *, end_of_day: bool = False) -> datetime.datetime: + """ + Parse a date-only string (YYYY-MM-DD) into a datetime. + + Date-only expiry values are interpreted as end-of-day so that + validityInfo.signed remains within [validFrom, validUntil]. + """ + dt = datetime.datetime.strptime(date_str, "%Y-%m-%d") + if end_of_day: + dt = dt.replace(hour=23, minute=59, second=59) + return dt + def sign( self, device_key: dict | None = None, @@ -181,16 +194,12 @@ def sign( """ utcnow = datetime.datetime.utcnow() - valid_from = datetime.datetime.strptime( - self.validity["issuance_date"], "%Y-%m-%d" - ) + valid_from = self.parse_date(self.validity["issuance_date"]) if settings.PYMDOC_EXP_DELTA_HOURS: exp = utcnow + datetime.timedelta(hours=settings.PYMDOC_EXP_DELTA_HOURS) else: - # five years - exp = datetime.datetime.strptime(self.validity["expiry_date"], "%Y-%m-%d") - # exp = utcnow + datetime.timedelta(hours=(24 * 365) * 5) + exp = self.parse_date(self.validity["expiry_date"], end_of_day=True) if utcnow > valid_from: valid_from = utcnow diff --git a/pymdoccbor/tests/test_06_mso_issuer.py b/pymdoccbor/tests/test_06_mso_issuer.py index 66066d1..0e43642 100644 --- a/pymdoccbor/tests/test_06_mso_issuer.py +++ b/pymdoccbor/tests/test_06_mso_issuer.py @@ -1,3 +1,6 @@ +import datetime + +import cbor2 from pycose.messages import CoseMessage from pymdoccbor.mso.issuer import MsoIssuer @@ -46,3 +49,33 @@ def test_mso_issuer_sign(): mso = msoi.sign() assert isinstance(mso, CoseMessage) + + +def test_mso_issuer_validity_same_day(): + today = datetime.datetime.utcnow().strftime("%Y-%m-%d") + msoi = MsoIssuer( + data=MICOV_DATA, + private_key=PKEY, + validity={"issuance_date": today, "expiry_date": today}, + alg="ES256", + cert_info=CERT_DATA, + ) + + mso = msoi.sign() + payload = cbor2.loads(mso.payload) + mso_body = cbor2.loads(payload.value) + validity = mso_body["validityInfo"] + + def _as_utc(dt): + if isinstance(dt, cbor2.CBORTag): + dt = dt.value + if isinstance(dt, str): + return datetime.datetime.fromisoformat(dt.replace("Z", "+00:00")) + return dt.replace(tzinfo=datetime.timezone.utc) + + signed = _as_utc(validity["signed"]) + valid_from = _as_utc(validity["validFrom"]) + valid_until = _as_utc(validity["validUntil"]) + + assert valid_from <= signed <= valid_until + assert valid_until.hour == 23 and valid_until.minute == 59 and valid_until.second == 59