From 53ac05d5d322f5004afda72f1cda51e965f81c5f Mon Sep 17 00:00:00 2001 From: Nurgaleev_Mansur_908 Date: Mon, 26 Feb 2024 23:27:38 +0300 Subject: [PATCH 1/5] change utcnow() to now(timezone.utc) Signed-off-by: Nurgaleev_Mansur_908 --- examples/manual_repo/basic_repo.py | 4 ++-- examples/manual_repo/hashed_bin_delegation.py | 4 ++-- .../succinct_hash_bin_delegations.py | 4 ++-- examples/repository/_simplerepo.py | 4 ++-- examples/uploader/_localrepo.py | 4 ++-- tests/repository_simulator.py | 2 +- tests/test_api.py | 6 +++--- tests/test_updater_top_level_update.py | 20 +++++++++---------- tuf/api/_payload.py | 8 ++++---- tuf/api/metadata.py | 2 +- .../_internal/trusted_metadata_set.py | 2 +- 11 files changed, 30 insertions(+), 30 deletions(-) diff --git a/examples/manual_repo/basic_repo.py b/examples/manual_repo/basic_repo.py index 129bbc39fa..2e7d25ccc6 100644 --- a/examples/manual_repo/basic_repo.py +++ b/examples/manual_repo/basic_repo.py @@ -23,7 +23,7 @@ import os import tempfile -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Dict @@ -47,7 +47,7 @@ def _in(days: float) -> datetime: """Adds 'days' to now and returns datetime object w/o microseconds.""" - return datetime.utcnow().replace(microsecond=0) + timedelta(days=days) + return datetime.now(timezone.utc).replace(microsecond=0) + timedelta(days=days) # Create top-level metadata diff --git a/examples/manual_repo/hashed_bin_delegation.py b/examples/manual_repo/hashed_bin_delegation.py index 94ce93cdef..da209ae2cc 100644 --- a/examples/manual_repo/hashed_bin_delegation.py +++ b/examples/manual_repo/hashed_bin_delegation.py @@ -19,7 +19,7 @@ import hashlib import os import tempfile -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Dict, Iterator, List, Tuple @@ -38,7 +38,7 @@ def _in(days: float) -> datetime: """Adds 'days' to now and returns datetime object w/o microseconds.""" - return datetime.utcnow().replace(microsecond=0) + timedelta(days=days) + return datetime.now(timezone.utc).replace(microsecond=0) + timedelta(days=days) roles: Dict[str, Metadata[Targets]] = {} diff --git a/examples/manual_repo/succinct_hash_bin_delegations.py b/examples/manual_repo/succinct_hash_bin_delegations.py index 4c4ffdb9ec..cc27437203 100644 --- a/examples/manual_repo/succinct_hash_bin_delegations.py +++ b/examples/manual_repo/succinct_hash_bin_delegations.py @@ -20,7 +20,7 @@ import math import os import tempfile -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Dict, Tuple @@ -99,7 +99,7 @@ def create_key() -> Tuple[Key, SSlibSigner]: # NOTE: See "Targets" and "Targets delegation" paragraphs in 'basic_repo.py' # example for more details about the Targets object. -expiration_date = datetime.utcnow().replace(microsecond=0) + timedelta(days=7) +expiration_date = datetime.now(timezone.utc).replace(microsecond=0) + timedelta(days=7) targets = Metadata(Targets(expires=expiration_date)) succinct_roles = SuccinctRoles( diff --git a/examples/repository/_simplerepo.py b/examples/repository/_simplerepo.py index 1ed9cb55f6..a9ddaa8731 100644 --- a/examples/repository/_simplerepo.py +++ b/examples/repository/_simplerepo.py @@ -7,7 +7,7 @@ import json import logging from collections import defaultdict -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Dict, List, Union from securesystemslib import keys @@ -130,7 +130,7 @@ def open(self, role: str) -> Metadata: def close(self, role: str, md: Metadata) -> None: """Store a version of metadata. Handle version bumps, expiry, signing""" md.signed.version += 1 - md.signed.expires = datetime.utcnow() + self.expiry_period + md.signed.expires = datetime.now(timezone.utc) + self.expiry_period md.signatures.clear() for signer in self.signer_cache[role]: diff --git a/examples/uploader/_localrepo.py b/examples/uploader/_localrepo.py index 616fcb2096..e469fc69c7 100644 --- a/examples/uploader/_localrepo.py +++ b/examples/uploader/_localrepo.py @@ -7,7 +7,7 @@ import json import logging import os -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Dict import requests @@ -77,7 +77,7 @@ def open(self, role: str) -> Metadata: def close(self, role: str, md: Metadata) -> None: """Store a version of metadata. Handle version bumps, expiry, signing""" md.signed.version += 1 - md.signed.expires = datetime.utcnow() + self.expiry_period + md.signed.expires = datetime.now(timezone.utc) + self.expiry_period with open(f"{self.key_dir}/{role}", "rt", encoding="utf-8") as f: signer = SSlibSigner(json.loads(f.read())) diff --git a/tests/repository_simulator.py b/tests/repository_simulator.py index f51b0f984d..6927354d2e 100644 --- a/tests/repository_simulator.py +++ b/tests/repository_simulator.py @@ -124,7 +124,7 @@ def __init__(self) -> None: self.fetch_tracker = FetchTracker() - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) self.safe_expiry = now.replace(microsecond=0) + datetime.timedelta( days=30 ) diff --git a/tests/test_api.py b/tests/test_api.py index c1076353dc..c3b14fe59e 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -14,7 +14,7 @@ import tempfile import unittest from copy import copy, deepcopy -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, ClassVar, Dict, Optional @@ -326,10 +326,10 @@ def test_metadata_signed_is_expired(self) -> None: # Test is_expired without reference_time, # manipulating md.signed.expires expires = md.signed.expires - md.signed.expires = datetime.utcnow() + md.signed.expires = datetime.now(timezone.utc) is_expired = md.signed.is_expired() self.assertTrue(is_expired) - md.signed.expires = datetime.utcnow() + timedelta(days=1) + md.signed.expires = datetime.now(timezone.utc) + timedelta(days=1) is_expired = md.signed.is_expired() self.assertFalse(is_expired) md.signed.expires = expires diff --git a/tests/test_updater_top_level_update.py b/tests/test_updater_top_level_update.py index 89c8ea7482..cafef868c9 100644 --- a/tests/test_updater_top_level_update.py +++ b/tests/test_updater_top_level_update.py @@ -43,7 +43,7 @@ class TestRefresh(unittest.TestCase): # set dump_dir to trigger repository state dumps dump_dir: Optional[str] = None - past_datetime = datetime.datetime.utcnow().replace( + past_datetime = datetime.datetime.now(datetime.timezone.utc).replace( microsecond=0 ) - datetime.timedelta(days=5) @@ -320,7 +320,7 @@ def test_expired_timestamp_version_rollback(self, mock_time: Mock) -> None: - Second updater refresh performed on day 18: assert that rollback check uses expired timestamp v1""" - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) self.sim.timestamp.expires = now + datetime.timedelta(days=7) self.sim.timestamp.version = 2 @@ -332,8 +332,8 @@ def test_expired_timestamp_version_rollback(self, mock_time: Mock) -> None: self.sim.timestamp.version = 1 - mock_time.utcnow.return_value = ( - datetime.datetime.utcnow() + datetime.timedelta(days=18) + mock_time.now.return_value = ( + datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=18) ) with patch("datetime.datetime", mock_time): # Check that a rollback protection is performed even if @@ -356,7 +356,7 @@ def test_expired_timestamp_snapshot_rollback(self, mock_time: Mock) -> None: - Second updater refresh performed on day 18: assert that rollback protection is done with expired timestamp v1""" - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) self.sim.timestamp.expires = now + datetime.timedelta(days=7) # Bump the snapshot version number to 3 @@ -371,8 +371,8 @@ def test_expired_timestamp_snapshot_rollback(self, mock_time: Mock) -> None: self.sim.update_snapshot() self.sim.timestamp.expires = now + datetime.timedelta(days=21) - mock_time.utcnow.return_value = ( - datetime.datetime.utcnow() + datetime.timedelta(days=18) + mock_time.now.return_value = ( + datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=18) ) with patch("datetime.datetime", mock_time): # Assert that rollback protection is done even if @@ -750,7 +750,7 @@ def test_expired_metadata(self, mock_time: Mock) -> None: - Second updater refresh performed on day 18, it is successful and timestamp/snaphot final versions are v2""" - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) self.sim.timestamp.expires = now + datetime.timedelta(days=7) # Make a successful update of valid metadata which stores it in cache @@ -762,8 +762,8 @@ def test_expired_metadata(self, mock_time: Mock) -> None: # Mocking time so that local timestam has expired # but the new timestamp has not - mock_time.utcnow.return_value = ( - datetime.datetime.utcnow() + datetime.timedelta(days=18) + mock_time.now.return_value = ( + datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=18) ) with patch("datetime.datetime", mock_time): self._run_refresh() diff --git a/tuf/api/_payload.py b/tuf/api/_payload.py index 84097b6557..6e61ff9759 100644 --- a/tuf/api/_payload.py +++ b/tuf/api/_payload.py @@ -10,7 +10,7 @@ import io import logging from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timezone from typing import ( IO, Any, @@ -82,7 +82,7 @@ def expires(self) -> datetime: """Get the metadata expiry date. # Use 'datetime' module to e.g. expire in seven days from now - obj.expires = utcnow() + timedelta(days=7) + obj.expires = now(timezone.utc) + timedelta(days=7) """ return self._expires @@ -115,7 +115,7 @@ def __init__( self.spec_version = spec_version - self.expires = expires or datetime.utcnow() + self.expires = expires or datetime.now(timezone.utc) if version is None: version = 1 @@ -205,7 +205,7 @@ def is_expired(self, reference_time: Optional[datetime] = None) -> bool: ``True`` if expiration time is less than the reference time. """ if reference_time is None: - reference_time = datetime.utcnow() + reference_time = datetime.now(timezone.utc) return reference_time >= self.expires diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index ae42a3f539..c0d812a5cc 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -98,7 +98,7 @@ class Metadata(Generic[T]): New Metadata instances can be created from scratch with:: - one_day = datetime.utcnow() + timedelta(days=1) + one_day = datetime.now(timezone.utc) + timedelta(days=1) timestamp = Metadata(Timestamp(expires=one_day)) Apart from ``expires`` all of the arguments to the inner constructors have diff --git a/tuf/ngclient/_internal/trusted_metadata_set.py b/tuf/ngclient/_internal/trusted_metadata_set.py index 87c42b05bf..36e1aab1c3 100644 --- a/tuf/ngclient/_internal/trusted_metadata_set.py +++ b/tuf/ngclient/_internal/trusted_metadata_set.py @@ -110,7 +110,7 @@ def __init__(self, root_data: bytes, envelope_type: EnvelopeType): error type and content will contain more details. """ self._trusted_set: Dict[str, Signed] = {} - self.reference_time = datetime.datetime.utcnow() + self.reference_time = datetime.datetime.now(datetime.timezone.utc) if envelope_type is EnvelopeType.SIMPLE: self._load_data = _load_from_simple_envelope From 93d9f8fa2d03a94a13fe54a36c8ae4789378914c Mon Sep 17 00:00:00 2001 From: Nurgaleev_Mansur_908 Date: Tue, 27 Feb 2024 22:13:17 +0300 Subject: [PATCH 2/5] fix lint Signed-off-by: Nurgaleev_Mansur_908 --- examples/manual_repo/basic_repo.py | 3 ++- examples/manual_repo/hashed_bin_delegation.py | 3 ++- examples/manual_repo/succinct_hash_bin_delegations.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/manual_repo/basic_repo.py b/examples/manual_repo/basic_repo.py index 2e7d25ccc6..68a719a300 100644 --- a/examples/manual_repo/basic_repo.py +++ b/examples/manual_repo/basic_repo.py @@ -47,7 +47,8 @@ def _in(days: float) -> datetime: """Adds 'days' to now and returns datetime object w/o microseconds.""" - return datetime.now(timezone.utc).replace(microsecond=0) + timedelta(days=days) + return (datetime.now(timezone.utc).replace(microsecond=0) + + timedelta(days=days)) # Create top-level metadata diff --git a/examples/manual_repo/hashed_bin_delegation.py b/examples/manual_repo/hashed_bin_delegation.py index da209ae2cc..1f302d4404 100644 --- a/examples/manual_repo/hashed_bin_delegation.py +++ b/examples/manual_repo/hashed_bin_delegation.py @@ -38,7 +38,8 @@ def _in(days: float) -> datetime: """Adds 'days' to now and returns datetime object w/o microseconds.""" - return datetime.now(timezone.utc).replace(microsecond=0) + timedelta(days=days) + return (datetime.now(timezone.utc).replace(microsecond=0) + + timedelta(days=days)) roles: Dict[str, Metadata[Targets]] = {} diff --git a/examples/manual_repo/succinct_hash_bin_delegations.py b/examples/manual_repo/succinct_hash_bin_delegations.py index cc27437203..d11370dd36 100644 --- a/examples/manual_repo/succinct_hash_bin_delegations.py +++ b/examples/manual_repo/succinct_hash_bin_delegations.py @@ -99,7 +99,8 @@ def create_key() -> Tuple[Key, SSlibSigner]: # NOTE: See "Targets" and "Targets delegation" paragraphs in 'basic_repo.py' # example for more details about the Targets object. -expiration_date = datetime.now(timezone.utc).replace(microsecond=0) + timedelta(days=7) +expiration_date = (datetime.now(timezone.utc).replace(microsecond=0) + + timedelta(days=7)) targets = Metadata(Targets(expires=expiration_date)) succinct_roles = SuccinctRoles( From 048d3b343bc8444178631361f7487d7e34b34549 Mon Sep 17 00:00:00 2001 From: Nurgaleev_Mansur_908 Date: Wed, 28 Feb 2024 15:29:44 +0300 Subject: [PATCH 3/5] fix lint Signed-off-by: Nurgaleev_Mansur_908 --- examples/manual_repo/basic_repo.py | 5 ++-- examples/manual_repo/hashed_bin_delegation.py | 5 ++-- .../succinct_hash_bin_delegations.py | 5 ++-- tests/test_updater_top_level_update.py | 27 ++++++++++--------- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/examples/manual_repo/basic_repo.py b/examples/manual_repo/basic_repo.py index 68a719a300..eb32523b23 100644 --- a/examples/manual_repo/basic_repo.py +++ b/examples/manual_repo/basic_repo.py @@ -47,8 +47,9 @@ def _in(days: float) -> datetime: """Adds 'days' to now and returns datetime object w/o microseconds.""" - return (datetime.now(timezone.utc).replace(microsecond=0) + - timedelta(days=days)) + return datetime.now(timezone.utc).replace(microsecond=0) + timedelta( + days=days + ) # Create top-level metadata diff --git a/examples/manual_repo/hashed_bin_delegation.py b/examples/manual_repo/hashed_bin_delegation.py index 1f302d4404..df476e091f 100644 --- a/examples/manual_repo/hashed_bin_delegation.py +++ b/examples/manual_repo/hashed_bin_delegation.py @@ -38,8 +38,9 @@ def _in(days: float) -> datetime: """Adds 'days' to now and returns datetime object w/o microseconds.""" - return (datetime.now(timezone.utc).replace(microsecond=0) + - timedelta(days=days)) + return datetime.now(timezone.utc).replace(microsecond=0) + timedelta( + days=days + ) roles: Dict[str, Metadata[Targets]] = {} diff --git a/examples/manual_repo/succinct_hash_bin_delegations.py b/examples/manual_repo/succinct_hash_bin_delegations.py index d11370dd36..cbeaaa92d6 100644 --- a/examples/manual_repo/succinct_hash_bin_delegations.py +++ b/examples/manual_repo/succinct_hash_bin_delegations.py @@ -99,8 +99,9 @@ def create_key() -> Tuple[Key, SSlibSigner]: # NOTE: See "Targets" and "Targets delegation" paragraphs in 'basic_repo.py' # example for more details about the Targets object. -expiration_date = (datetime.now(timezone.utc).replace(microsecond=0) + - timedelta(days=7)) +expiration_date = datetime.now(timezone.utc).replace(microsecond=0) + timedelta( + days=7 +) targets = Metadata(Targets(expires=expiration_date)) succinct_roles = SuccinctRoles( diff --git a/tests/test_updater_top_level_update.py b/tests/test_updater_top_level_update.py index cafef868c9..af14642c42 100644 --- a/tests/test_updater_top_level_update.py +++ b/tests/test_updater_top_level_update.py @@ -11,6 +11,7 @@ import sys import tempfile import unittest +from datetime import timezone from typing import Iterable, Optional from unittest.mock import MagicMock, Mock, call, patch @@ -43,7 +44,7 @@ class TestRefresh(unittest.TestCase): # set dump_dir to trigger repository state dumps dump_dir: Optional[str] = None - past_datetime = datetime.datetime.now(datetime.timezone.utc).replace( + past_datetime = datetime.datetime.now(timezone.utc).replace( microsecond=0 ) - datetime.timedelta(days=5) @@ -320,7 +321,7 @@ def test_expired_timestamp_version_rollback(self, mock_time: Mock) -> None: - Second updater refresh performed on day 18: assert that rollback check uses expired timestamp v1""" - now = datetime.datetime.now(datetime.timezone.utc) + now = datetime.datetime.now(timezone.utc) self.sim.timestamp.expires = now + datetime.timedelta(days=7) self.sim.timestamp.version = 2 @@ -332,9 +333,9 @@ def test_expired_timestamp_version_rollback(self, mock_time: Mock) -> None: self.sim.timestamp.version = 1 - mock_time.now.return_value = ( - datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=18) - ) + mock_time.now.return_value = datetime.datetime.now( + timezone.utc + ) + datetime.timedelta(days=18) with patch("datetime.datetime", mock_time): # Check that a rollback protection is performed even if # local timestamp has expired @@ -356,7 +357,7 @@ def test_expired_timestamp_snapshot_rollback(self, mock_time: Mock) -> None: - Second updater refresh performed on day 18: assert that rollback protection is done with expired timestamp v1""" - now = datetime.datetime.now(datetime.timezone.utc) + now = datetime.datetime.now(timezone.utc) self.sim.timestamp.expires = now + datetime.timedelta(days=7) # Bump the snapshot version number to 3 @@ -371,9 +372,9 @@ def test_expired_timestamp_snapshot_rollback(self, mock_time: Mock) -> None: self.sim.update_snapshot() self.sim.timestamp.expires = now + datetime.timedelta(days=21) - mock_time.now.return_value = ( - datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=18) - ) + mock_time.now.return_value = datetime.datetime.now( + timezone.utc + ) + datetime.timedelta(days=18) with patch("datetime.datetime", mock_time): # Assert that rollback protection is done even if # local timestamp has expired @@ -750,7 +751,7 @@ def test_expired_metadata(self, mock_time: Mock) -> None: - Second updater refresh performed on day 18, it is successful and timestamp/snaphot final versions are v2""" - now = datetime.datetime.now(datetime.timezone.utc) + now = datetime.datetime.now(timezone.utc) self.sim.timestamp.expires = now + datetime.timedelta(days=7) # Make a successful update of valid metadata which stores it in cache @@ -762,9 +763,9 @@ def test_expired_metadata(self, mock_time: Mock) -> None: # Mocking time so that local timestam has expired # but the new timestamp has not - mock_time.now.return_value = ( - datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=18) - ) + mock_time.now.return_value = datetime.datetime.now( + timezone.utc + ) + datetime.timedelta(days=18) with patch("datetime.datetime", mock_time): self._run_refresh() From c2edd30669e3c482bdeeb27a67c512e8ea2b7acd Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Thu, 29 Feb 2024 15:12:18 +0200 Subject: [PATCH 4/5] Metadata API: Make sure Signed.expires is UTC * Most importantly use strftime() to serialize the datetime * Force the timezone as UTC when deserializing Signed-off-by: Jussi Kukkonen --- tests/generated_data/generate_md.py | 5 ++--- tests/test_api.py | 3 ++- tests/test_trusted_metadata_set.py | 10 +++++----- tuf/api/_payload.py | 3 ++- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/generated_data/generate_md.py b/tests/generated_data/generate_md.py index df459c1d6d..884e97e207 100644 --- a/tests/generated_data/generate_md.py +++ b/tests/generated_data/generate_md.py @@ -5,7 +5,7 @@ import os import sys -from datetime import datetime +from datetime import UTC, datetime from typing import Dict, List, Optional from securesystemslib.signer import SSlibKey, SSlibSigner @@ -48,8 +48,7 @@ } ) -expires_str = "2050-01-01T00:00:00Z" -EXPIRY = datetime.strptime(expires_str, "%Y-%m-%dT%H:%M:%SZ") +EXPIRY = datetime(2050, 1, 1, tzinfo=UTC) OUT_DIR = "generated_data/ed25519_metadata" if not os.path.exists(OUT_DIR): os.mkdir(OUT_DIR) diff --git a/tests/test_api.py b/tests/test_api.py index c3b14fe59e..b0bd937f3a 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -313,7 +313,8 @@ def test_metadata_signed_is_expired(self) -> None: snapshot_path = os.path.join(self.repo_dir, "metadata", "snapshot.json") md = Metadata.from_file(snapshot_path) - self.assertEqual(md.signed.expires, datetime(2030, 1, 1, 0, 0)) + expected_expiry = datetime(2030, 1, 1, 0, 0, tzinfo=timezone.utc) + self.assertEqual(md.signed.expires, expected_expiry) # Test is_expired with reference_time provided is_expired = md.signed.is_expired(md.signed.expires) diff --git a/tests/test_trusted_metadata_set.py b/tests/test_trusted_metadata_set.py index b5ab042d7e..ac5b557cbc 100644 --- a/tests/test_trusted_metadata_set.py +++ b/tests/test_trusted_metadata_set.py @@ -4,7 +4,7 @@ import os import sys import unittest -from datetime import datetime +from datetime import UTC, datetime from typing import Callable, ClassVar, Dict, List, Optional, Tuple from securesystemslib.interface import ( @@ -279,7 +279,7 @@ def test_update_root_new_root_ver_same_as_trusted_root_ver(self) -> None: def test_root_expired_final_root(self) -> None: def root_expired_modifier(root: Root) -> None: - root.expires = datetime(1970, 1, 1) + root.expires = datetime(1970, 1, 1, tzinfo=UTC) # intermediate root can be expired root = self.modify_metadata(Root.type, root_expired_modifier) @@ -329,7 +329,7 @@ def bump_snapshot_version(timestamp: Timestamp) -> None: def test_update_timestamp_expired(self) -> None: # new_timestamp has expired def timestamp_expired_modifier(timestamp: Timestamp) -> None: - timestamp.expires = datetime(1970, 1, 1) + timestamp.expires = datetime(1970, 1, 1, tzinfo=UTC) # expired intermediate timestamp is loaded but raises timestamp = self.modify_metadata( @@ -406,7 +406,7 @@ def test_update_snapshot_expired_new_snapshot(self) -> None: self.trusted_set.update_timestamp(self.metadata[Timestamp.type]) def snapshot_expired_modifier(snapshot: Snapshot) -> None: - snapshot.expires = datetime(1970, 1, 1) + snapshot.expires = datetime(1970, 1, 1, tzinfo=UTC) # expired intermediate snapshot is loaded but will raise snapshot = self.modify_metadata( @@ -486,7 +486,7 @@ def test_update_targets_expired_new_target(self) -> None: # new_delegated_target has expired def target_expired_modifier(target: Targets) -> None: - target.expires = datetime(1970, 1, 1) + target.expires = datetime(1970, 1, 1, tzinfo=UTC) targets = self.modify_metadata(Targets.type, target_expired_modifier) with self.assertRaises(exceptions.ExpiredMetadataError): diff --git a/tuf/api/_payload.py b/tuf/api/_payload.py index 6e61ff9759..dc7c097239 100644 --- a/tuf/api/_payload.py +++ b/tuf/api/_payload.py @@ -176,6 +176,7 @@ def _common_fields_from_dict( # what the constructor expects and what we store. The inverse operation # is implemented in '_common_fields_to_dict'. expires = datetime.strptime(expires_str, "%Y-%m-%dT%H:%M:%SZ") + expires = expires.replace(tzinfo=timezone.utc) return version, spec_version, expires @@ -190,7 +191,7 @@ def _common_fields_to_dict(self) -> Dict[str, Any]: "_type": self._type, "version": self.version, "spec_version": self.spec_version, - "expires": self.expires.isoformat() + "Z", + "expires": self.expires.strftime("%Y-%m-%dT%H:%M:%SZ"), **self.unrecognized_fields, } From de9633dab75d88ad1d8e291139bd2a6a36745746 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Thu, 29 Feb 2024 15:27:06 +0200 Subject: [PATCH 5/5] Metadata API: convenience tweak to expires setter Practically were changing API if we start requiring that expires is non-naive because this no longer works: metadata.signed.expires = datetime(3000,1,1) We can make this work without API breaks though: * it the input is naive, just use UTC * if the input is not naive or UTC, raise Signed-off-by: Jussi Kukkonen --- tuf/api/_payload.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tuf/api/_payload.py b/tuf/api/_payload.py index dc7c097239..0bb361ee33 100644 --- a/tuf/api/_payload.py +++ b/tuf/api/_payload.py @@ -60,8 +60,8 @@ class Signed(metaclass=abc.ABCMeta): version: Metadata version number. If None, then 1 is assigned. spec_version: Supported TUF specification version. If None, then the version currently supported by the library is assigned. - expires: Metadata expiry date. If None, then current date and time is - assigned. + expires: Metadata expiry date in UTC timezone. If None, then current + date and time is assigned. unrecognized_fields: Dictionary of all attributes that are not managed by TUF Metadata API @@ -79,16 +79,22 @@ def _type(self) -> str: @property def expires(self) -> datetime: - """Get the metadata expiry date. - - # Use 'datetime' module to e.g. expire in seven days from now - obj.expires = now(timezone.utc) + timedelta(days=7) - """ + """Get the metadata expiry date.""" return self._expires @expires.setter def expires(self, value: datetime) -> None: + """Set the metadata expiry date. + + # Use 'datetime' module to e.g. expire in seven days from now + obj.expires = now(timezone.utc) + timedelta(days=7) + """ self._expires = value.replace(microsecond=0) + if self._expires.tzinfo is None: + # Naive datetime: just make it UTC + self._expires = self._expires.replace(tzinfo=timezone.utc) + elif self._expires.tzinfo != timezone.utc: + raise ValueError(f"Expected tz UTC, not {self._expires.tzinfo}") # NOTE: Signed is a stupid name, because this might not be signed yet, but # we keep it to match spec terminology (I often refer to this as "payload",