Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
# Copyright 2020, New York University and the TUF contributors
|
|
|
|
|
# SPDX-License-Identifier: MIT OR Apache-2.0
|
2024-03-07 08:05:36 +00:00
|
|
|
"""Unit tests for api/metadata.py"""
|
2020-08-18 13:55:43 +00:00
|
|
|
|
2024-11-29 10:29:32 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2020-08-18 15:04:55 +00:00
|
|
|
import json
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
import shutil
|
2021-10-12 13:54:10 +00:00
|
|
|
import sys
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
import tempfile
|
|
|
|
|
import unittest
|
2024-02-01 18:32:07 +00:00
|
|
|
from copy import copy, deepcopy
|
2024-02-26 20:27:38 +00:00
|
|
|
from datetime import datetime, timedelta, timezone
|
2023-10-12 09:55:45 +00:00
|
|
|
from pathlib import Path
|
2024-11-29 10:29:32 +00:00
|
|
|
from typing import ClassVar
|
2021-10-12 13:54:10 +00:00
|
|
|
|
2022-12-01 09:22:54 +00:00
|
|
|
from securesystemslib import exceptions as sslib_exceptions
|
2023-08-16 14:36:27 +00:00
|
|
|
from securesystemslib.signer import (
|
2024-04-24 08:36:57 +00:00
|
|
|
CryptoSigner,
|
|
|
|
|
Key,
|
2023-08-16 14:36:27 +00:00
|
|
|
SecretsHandler,
|
|
|
|
|
Signer,
|
2025-03-15 15:08:45 +00:00
|
|
|
SSlibKey,
|
2023-08-16 14:36:27 +00:00
|
|
|
)
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
|
2020-11-19 12:45:09 +00:00
|
|
|
from tests import utils
|
2021-12-13 16:20:26 +00:00
|
|
|
from tuf.api import exceptions
|
2023-10-12 09:55:45 +00:00
|
|
|
from tuf.api.dsse import SimpleEnvelope
|
2021-03-02 21:13:12 +00:00
|
|
|
from tuf.api.metadata import (
|
2021-12-03 16:11:40 +00:00
|
|
|
TOP_LEVEL_ROLE_NAMES,
|
2021-10-12 13:54:10 +00:00
|
|
|
DelegatedRole,
|
2021-11-18 16:58:16 +00:00
|
|
|
Delegations,
|
2021-03-02 21:13:12 +00:00
|
|
|
Metadata,
|
2023-08-14 12:22:29 +00:00
|
|
|
MetaFile,
|
2020-11-24 10:49:58 +00:00
|
|
|
Root,
|
2024-02-05 13:26:31 +00:00
|
|
|
RootVerificationResult,
|
2022-12-01 09:08:05 +00:00
|
|
|
Signature,
|
2021-03-02 21:13:12 +00:00
|
|
|
Snapshot,
|
2022-05-18 14:47:24 +00:00
|
|
|
SuccinctRoles,
|
2021-03-30 15:13:16 +00:00
|
|
|
TargetFile,
|
2021-10-12 13:54:10 +00:00
|
|
|
Targets,
|
|
|
|
|
Timestamp,
|
2024-02-05 13:26:31 +00:00
|
|
|
VerificationResult,
|
2021-03-02 21:13:12 +00:00
|
|
|
)
|
2021-12-21 16:12:30 +00:00
|
|
|
from tuf.api.serialization import DeserializationError, SerializationError
|
2023-08-01 12:29:17 +00:00
|
|
|
from tuf.api.serialization.json import JSONSerializer
|
2021-08-09 15:56:02 +00:00
|
|
|
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestMetadata(unittest.TestCase):
|
2021-10-13 16:56:53 +00:00
|
|
|
"""Tests for public API of all classes in 'tuf/api/metadata.py'."""
|
|
|
|
|
|
2021-11-18 16:58:16 +00:00
|
|
|
temporary_directory: ClassVar[str]
|
|
|
|
|
repo_dir: ClassVar[str]
|
|
|
|
|
keystore_dir: ClassVar[str]
|
2024-11-04 04:21:23 +00:00
|
|
|
signers: ClassVar[dict[str, Signer]]
|
2021-11-18 16:58:16 +00:00
|
|
|
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
@classmethod
|
2021-11-18 16:58:16 +00:00
|
|
|
def setUpClass(cls) -> None:
|
2020-08-31 14:13:59 +00:00
|
|
|
# Create a temporary directory to store the repository, metadata, and
|
|
|
|
|
# target files. 'temporary_directory' must be deleted in
|
|
|
|
|
# TearDownClass() so that temporary files are always removed, even when
|
|
|
|
|
# exceptions occur.
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
cls.temporary_directory = tempfile.mkdtemp(dir=os.getcwd())
|
|
|
|
|
|
|
|
|
|
test_repo_data = os.path.join(
|
2021-10-12 13:14:24 +00:00
|
|
|
os.path.dirname(os.path.realpath(__file__)), "repository_data"
|
|
|
|
|
)
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
|
2021-10-12 13:14:24 +00:00
|
|
|
cls.repo_dir = os.path.join(cls.temporary_directory, "repository")
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
shutil.copytree(
|
2021-10-12 13:14:24 +00:00
|
|
|
os.path.join(test_repo_data, "repository"), cls.repo_dir
|
|
|
|
|
)
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
|
2021-10-12 13:14:24 +00:00
|
|
|
cls.keystore_dir = os.path.join(cls.temporary_directory, "keystore")
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
shutil.copytree(
|
2021-10-12 13:14:24 +00:00
|
|
|
os.path.join(test_repo_data, "keystore"), cls.keystore_dir
|
|
|
|
|
)
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
|
2024-04-24 08:36:57 +00:00
|
|
|
path = os.path.join(cls.repo_dir, "metadata", "root.json")
|
|
|
|
|
root = Metadata[Root].from_file(path).signed
|
|
|
|
|
|
|
|
|
|
# Load signers
|
|
|
|
|
|
|
|
|
|
cls.signers = {}
|
|
|
|
|
for role in [Snapshot.type, Targets.type, Timestamp.type]:
|
|
|
|
|
uri = f"file2:{os.path.join(cls.keystore_dir, role + '_key')}"
|
|
|
|
|
role_obj = root.get_delegated_role(role)
|
|
|
|
|
key = root.get_key(role_obj.keyids[0])
|
|
|
|
|
cls.signers[role] = CryptoSigner.from_priv_key_uri(uri, key)
|
2020-08-31 14:10:19 +00:00
|
|
|
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
@classmethod
|
2021-11-18 16:58:16 +00:00
|
|
|
def tearDownClass(cls) -> None:
|
2020-08-31 14:13:59 +00:00
|
|
|
# Remove the temporary repository directory, which should contain all
|
|
|
|
|
# the metadata, targets, and key files generated for the test cases.
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
shutil.rmtree(cls.temporary_directory)
|
|
|
|
|
|
2021-11-18 16:58:16 +00:00
|
|
|
def test_generic_read(self) -> None:
|
2020-08-18 15:04:55 +00:00
|
|
|
for metadata, inner_metadata_cls in [
|
2021-11-17 12:24:03 +00:00
|
|
|
(Root.type, Root),
|
|
|
|
|
(Snapshot.type, Snapshot),
|
|
|
|
|
(Timestamp.type, Timestamp),
|
|
|
|
|
(Targets.type, Targets),
|
2021-10-12 13:14:24 +00:00
|
|
|
]:
|
2025-03-10 20:48:43 +00:00
|
|
|
# Load JSON-formatted metadata of each supported type from file
|
2020-09-03 13:35:05 +00:00
|
|
|
# and from out-of-band read JSON string
|
2021-10-12 13:14:24 +00:00
|
|
|
path = os.path.join(self.repo_dir, "metadata", metadata + ".json")
|
2021-10-12 13:46:52 +00:00
|
|
|
md_obj = Metadata.from_file(path)
|
2021-10-12 13:14:24 +00:00
|
|
|
with open(path, "rb") as f:
|
2021-10-12 13:46:52 +00:00
|
|
|
md_obj2 = Metadata.from_bytes(f.read())
|
2020-08-18 15:04:55 +00:00
|
|
|
|
2020-09-03 13:35:05 +00:00
|
|
|
# Assert that both methods instantiate the right inner class for
|
|
|
|
|
# each metadata type and ...
|
2021-10-12 13:46:52 +00:00
|
|
|
self.assertTrue(isinstance(md_obj.signed, inner_metadata_cls))
|
|
|
|
|
self.assertTrue(isinstance(md_obj2.signed, inner_metadata_cls))
|
2020-09-03 13:35:05 +00:00
|
|
|
|
2021-03-04 11:46:16 +00:00
|
|
|
# ... and return the same object (compared by dict representation)
|
2021-10-12 13:46:52 +00:00
|
|
|
self.assertDictEqual(md_obj.to_dict(), md_obj2.to_dict())
|
2020-09-03 13:35:05 +00:00
|
|
|
|
2020-08-18 15:04:55 +00:00
|
|
|
# Assert that it chokes correctly on an unknown metadata type
|
2021-10-12 13:14:24 +00:00
|
|
|
bad_metadata_path = "bad-metadata.json"
|
|
|
|
|
bad_metadata = {"signed": {"_type": "bad-metadata"}}
|
|
|
|
|
bad_string = json.dumps(bad_metadata).encode("utf-8")
|
|
|
|
|
with open(bad_metadata_path, "wb") as f:
|
2021-04-16 13:12:15 +00:00
|
|
|
f.write(bad_string)
|
2020-08-18 15:04:55 +00:00
|
|
|
|
2021-02-09 14:36:49 +00:00
|
|
|
with self.assertRaises(DeserializationError):
|
2021-03-04 09:51:45 +00:00
|
|
|
Metadata.from_file(bad_metadata_path)
|
2021-04-16 13:12:15 +00:00
|
|
|
with self.assertRaises(DeserializationError):
|
|
|
|
|
Metadata.from_bytes(bad_string)
|
2020-08-18 15:04:55 +00:00
|
|
|
|
|
|
|
|
os.remove(bad_metadata_path)
|
|
|
|
|
|
2021-12-15 17:42:13 +00:00
|
|
|
def test_md_read_write_file_exceptions(self) -> None:
|
|
|
|
|
# Test writing to a file with bad filename
|
|
|
|
|
with self.assertRaises(exceptions.StorageError):
|
|
|
|
|
Metadata.from_file("bad-metadata.json")
|
|
|
|
|
|
|
|
|
|
# Test serializing to a file with bad filename
|
|
|
|
|
with self.assertRaises(exceptions.StorageError):
|
2022-04-14 06:30:55 +00:00
|
|
|
md = Metadata.from_file(
|
|
|
|
|
os.path.join(self.repo_dir, "metadata", "root.json")
|
|
|
|
|
)
|
2021-12-15 17:42:13 +00:00
|
|
|
md.to_file("")
|
|
|
|
|
|
2021-11-18 16:58:16 +00:00
|
|
|
def test_compact_json(self) -> None:
|
2021-10-12 13:14:24 +00:00
|
|
|
path = os.path.join(self.repo_dir, "metadata", "targets.json")
|
2021-10-12 13:46:52 +00:00
|
|
|
md_obj = Metadata.from_file(path)
|
2020-08-19 08:40:58 +00:00
|
|
|
self.assertTrue(
|
2021-10-12 13:46:52 +00:00
|
|
|
len(JSONSerializer(compact=True).serialize(md_obj))
|
|
|
|
|
< len(JSONSerializer().serialize(md_obj))
|
2021-10-12 13:14:24 +00:00
|
|
|
)
|
2020-08-19 08:40:58 +00:00
|
|
|
|
2021-11-18 16:58:16 +00:00
|
|
|
def test_read_write_read_compare(self) -> None:
|
2021-12-03 16:11:40 +00:00
|
|
|
for metadata in TOP_LEVEL_ROLE_NAMES:
|
2021-10-12 13:14:24 +00:00
|
|
|
path = os.path.join(self.repo_dir, "metadata", metadata + ".json")
|
2021-10-12 13:46:52 +00:00
|
|
|
md_obj = Metadata.from_file(path)
|
2020-08-19 08:40:58 +00:00
|
|
|
|
2021-10-12 13:14:24 +00:00
|
|
|
path_2 = path + ".tmp"
|
2021-10-12 13:46:52 +00:00
|
|
|
md_obj.to_file(path_2)
|
|
|
|
|
md_obj_2 = Metadata.from_file(path_2)
|
|
|
|
|
self.assertDictEqual(md_obj.to_dict(), md_obj_2.to_dict())
|
2020-08-19 08:40:58 +00:00
|
|
|
|
|
|
|
|
os.remove(path_2)
|
|
|
|
|
|
2021-12-21 16:12:30 +00:00
|
|
|
def test_serialize_with_validate(self) -> None:
|
|
|
|
|
# Assert that by changing one required attribute validation will fail.
|
2022-04-14 06:30:55 +00:00
|
|
|
root = Metadata.from_file(
|
|
|
|
|
os.path.join(self.repo_dir, "metadata", "root.json")
|
|
|
|
|
)
|
2021-12-21 16:12:30 +00:00
|
|
|
root.signed.version = 0
|
|
|
|
|
with self.assertRaises(SerializationError):
|
|
|
|
|
root.to_bytes(JSONSerializer(validate=True))
|
|
|
|
|
|
2021-11-18 16:58:16 +00:00
|
|
|
def test_to_from_bytes(self) -> None:
|
2021-12-03 16:11:40 +00:00
|
|
|
for metadata in TOP_LEVEL_ROLE_NAMES:
|
2021-10-12 13:14:24 +00:00
|
|
|
path = os.path.join(self.repo_dir, "metadata", metadata + ".json")
|
|
|
|
|
with open(path, "rb") as f:
|
2021-07-09 12:53:48 +00:00
|
|
|
metadata_bytes = f.read()
|
2021-10-12 13:46:52 +00:00
|
|
|
md_obj = Metadata.from_bytes(metadata_bytes)
|
2025-03-10 20:48:43 +00:00
|
|
|
# Compare that from_bytes/to_bytes doesn't change the content
|
2021-07-09 12:53:48 +00:00
|
|
|
# for two cases for the serializer: noncompact and compact.
|
|
|
|
|
|
|
|
|
|
# Case 1: test noncompact by overriding the default serializer.
|
2021-10-12 13:46:52 +00:00
|
|
|
self.assertEqual(md_obj.to_bytes(JSONSerializer()), metadata_bytes)
|
2021-07-09 12:53:48 +00:00
|
|
|
|
|
|
|
|
# Case 2: test compact by using the default serializer.
|
2021-10-12 13:46:52 +00:00
|
|
|
obj_bytes = md_obj.to_bytes()
|
2021-07-09 12:53:48 +00:00
|
|
|
metadata_obj_2 = Metadata.from_bytes(obj_bytes)
|
2021-10-12 13:14:24 +00:00
|
|
|
self.assertEqual(metadata_obj_2.to_bytes(), obj_bytes)
|
2021-07-09 12:53:48 +00:00
|
|
|
|
2021-11-18 16:58:16 +00:00
|
|
|
def test_sign_verify(self) -> None:
|
2022-12-01 09:22:54 +00:00
|
|
|
path = os.path.join(self.repo_dir, "metadata")
|
|
|
|
|
root = Metadata[Root].from_file(os.path.join(path, "root.json")).signed
|
2021-05-22 09:57:07 +00:00
|
|
|
|
|
|
|
|
# Locate the public keys we need from root
|
2021-11-17 12:24:03 +00:00
|
|
|
targets_keyid = next(iter(root.roles[Targets.type].keyids))
|
2021-05-22 09:57:07 +00:00
|
|
|
targets_key = root.keys[targets_keyid]
|
2021-11-17 12:24:03 +00:00
|
|
|
snapshot_keyid = next(iter(root.roles[Snapshot.type].keyids))
|
2021-05-22 09:57:07 +00:00
|
|
|
snapshot_key = root.keys[snapshot_keyid]
|
2021-11-17 12:24:03 +00:00
|
|
|
timestamp_keyid = next(iter(root.roles[Timestamp.type].keyids))
|
2021-05-22 09:57:07 +00:00
|
|
|
timestamp_key = root.keys[timestamp_keyid]
|
|
|
|
|
|
2020-08-31 14:10:19 +00:00
|
|
|
# Load sample metadata (targets) and assert ...
|
2022-12-01 09:22:54 +00:00
|
|
|
md_obj = Metadata.from_file(os.path.join(path, "targets.json"))
|
|
|
|
|
sig = md_obj.signatures[targets_keyid]
|
2023-08-01 12:29:17 +00:00
|
|
|
data = md_obj.signed_bytes
|
2020-08-31 14:10:19 +00:00
|
|
|
|
|
|
|
|
# ... it has a single existing signature,
|
2021-10-12 13:46:52 +00:00
|
|
|
self.assertEqual(len(md_obj.signatures), 1)
|
2020-09-08 15:28:50 +00:00
|
|
|
# ... which is valid for the correct key.
|
2022-12-01 09:22:54 +00:00
|
|
|
targets_key.verify_signature(sig, data)
|
|
|
|
|
with self.assertRaises(sslib_exceptions.VerificationError):
|
|
|
|
|
snapshot_key.verify_signature(sig, data)
|
2021-05-30 09:43:09 +00:00
|
|
|
|
2020-08-31 14:10:19 +00:00
|
|
|
# Append a new signature with the unrelated key and assert that ...
|
2024-04-24 08:36:57 +00:00
|
|
|
snapshot_sig = md_obj.sign(self.signers[Snapshot.type], append=True)
|
2020-08-31 14:10:19 +00:00
|
|
|
# ... there are now two signatures, and
|
2021-10-12 13:46:52 +00:00
|
|
|
self.assertEqual(len(md_obj.signatures), 2)
|
2020-08-31 14:10:19 +00:00
|
|
|
# ... both are valid for the corresponding keys.
|
2022-12-01 09:22:54 +00:00
|
|
|
targets_key.verify_signature(sig, data)
|
|
|
|
|
snapshot_key.verify_signature(snapshot_sig, data)
|
2021-06-11 06:57:08 +00:00
|
|
|
# ... the returned (appended) signature is for snapshot key
|
2022-12-01 09:22:54 +00:00
|
|
|
self.assertEqual(snapshot_sig.keyid, snapshot_keyid)
|
2020-08-31 14:10:19 +00:00
|
|
|
|
|
|
|
|
# Create and assign (don't append) a new signature and assert that ...
|
2024-04-24 08:36:57 +00:00
|
|
|
ts_sig = md_obj.sign(self.signers[Timestamp.type], append=False)
|
2020-08-31 14:10:19 +00:00
|
|
|
# ... there now is only one signature,
|
2021-10-12 13:46:52 +00:00
|
|
|
self.assertEqual(len(md_obj.signatures), 1)
|
2020-08-31 14:10:19 +00:00
|
|
|
# ... valid for that key.
|
2022-12-01 09:22:54 +00:00
|
|
|
timestamp_key.verify_signature(ts_sig, data)
|
|
|
|
|
with self.assertRaises(sslib_exceptions.VerificationError):
|
|
|
|
|
targets_key.verify_signature(ts_sig, data)
|
2020-08-31 14:10:19 +00:00
|
|
|
|
2021-12-15 17:42:13 +00:00
|
|
|
def test_sign_failures(self) -> None:
|
|
|
|
|
# Test throwing UnsignedMetadataError because of signing problems
|
2022-04-14 06:30:55 +00:00
|
|
|
md = Metadata.from_file(
|
|
|
|
|
os.path.join(self.repo_dir, "metadata", "snapshot.json")
|
|
|
|
|
)
|
2023-08-16 14:36:27 +00:00
|
|
|
|
2024-02-21 03:57:53 +00:00
|
|
|
class FailingSigner(Signer):
|
2023-08-16 14:36:27 +00:00
|
|
|
@classmethod
|
|
|
|
|
def from_priv_key_uri(
|
|
|
|
|
cls,
|
2025-03-15 15:08:45 +00:00
|
|
|
_priv_key_uri: str,
|
|
|
|
|
_public_key: Key,
|
|
|
|
|
_secrets_handler: SecretsHandler | None = None,
|
2024-11-29 10:29:32 +00:00
|
|
|
) -> Signer:
|
2025-03-15 15:08:45 +00:00
|
|
|
raise RuntimeError("Not a real signer")
|
2023-08-16 14:36:27 +00:00
|
|
|
|
2024-03-27 11:49:56 +00:00
|
|
|
@property
|
|
|
|
|
def public_key(self) -> Key:
|
|
|
|
|
raise RuntimeError("Not a real signer")
|
|
|
|
|
|
2024-04-03 11:47:14 +00:00
|
|
|
def sign(self, _payload: bytes) -> Signature:
|
2023-08-16 14:36:27 +00:00
|
|
|
raise RuntimeError("signing failed")
|
|
|
|
|
|
|
|
|
|
failing_signer = FailingSigner()
|
|
|
|
|
|
2021-12-15 17:42:13 +00:00
|
|
|
with self.assertRaises(exceptions.UnsignedMetadataError):
|
2023-08-16 14:36:27 +00:00
|
|
|
md.sign(failing_signer)
|
2021-12-15 17:42:13 +00:00
|
|
|
|
2022-12-01 09:22:54 +00:00
|
|
|
def test_key_verify_failures(self) -> None:
|
2021-11-10 13:51:16 +00:00
|
|
|
root_path = os.path.join(self.repo_dir, "metadata", "root.json")
|
|
|
|
|
root = Metadata[Root].from_file(root_path).signed
|
|
|
|
|
|
|
|
|
|
# Locate the timestamp public key we need from root
|
2021-11-17 12:24:03 +00:00
|
|
|
timestamp_keyid = next(iter(root.roles[Timestamp.type].keyids))
|
2021-11-10 13:51:16 +00:00
|
|
|
timestamp_key = root.keys[timestamp_keyid]
|
|
|
|
|
|
|
|
|
|
# Load sample metadata (timestamp)
|
|
|
|
|
path = os.path.join(self.repo_dir, "metadata", "timestamp.json")
|
|
|
|
|
md_obj = Metadata.from_file(path)
|
2022-12-01 09:22:54 +00:00
|
|
|
sig = md_obj.signatures[timestamp_keyid]
|
2023-08-01 12:29:17 +00:00
|
|
|
data = md_obj.signed_bytes
|
2021-11-10 13:51:16 +00:00
|
|
|
|
|
|
|
|
# Test failure on unknown scheme (securesystemslib
|
|
|
|
|
# UnsupportedAlgorithmError)
|
2021-06-16 12:12:10 +00:00
|
|
|
scheme = timestamp_key.scheme
|
|
|
|
|
timestamp_key.scheme = "foo"
|
2022-12-01 09:22:54 +00:00
|
|
|
with self.assertRaises(sslib_exceptions.VerificationError):
|
|
|
|
|
timestamp_key.verify_signature(sig, data)
|
2021-06-16 12:12:10 +00:00
|
|
|
timestamp_key.scheme = scheme
|
|
|
|
|
|
2021-11-10 13:51:16 +00:00
|
|
|
# Test failure on broken public key data (securesystemslib
|
|
|
|
|
# CryptoError)
|
2021-05-30 09:59:32 +00:00
|
|
|
public = timestamp_key.keyval["public"]
|
|
|
|
|
timestamp_key.keyval["public"] = "ffff"
|
2022-12-01 09:22:54 +00:00
|
|
|
with self.assertRaises(sslib_exceptions.VerificationError):
|
|
|
|
|
timestamp_key.verify_signature(sig, data)
|
2021-05-30 09:59:32 +00:00
|
|
|
timestamp_key.keyval["public"] = public
|
|
|
|
|
|
2021-11-10 13:51:16 +00:00
|
|
|
# Test failure with invalid signature (securesystemslib
|
|
|
|
|
# FormatError)
|
2022-12-01 09:22:54 +00:00
|
|
|
incorrect_sig = copy(sig)
|
|
|
|
|
incorrect_sig.signature = "foo"
|
|
|
|
|
with self.assertRaises(sslib_exceptions.VerificationError):
|
|
|
|
|
timestamp_key.verify_signature(incorrect_sig, data)
|
2021-05-30 09:59:32 +00:00
|
|
|
|
|
|
|
|
# Test failure with valid but incorrect signature
|
2022-12-01 09:22:54 +00:00
|
|
|
incorrect_sig.signature = "ff" * 64
|
|
|
|
|
with self.assertRaises(sslib_exceptions.UnverifiedSignatureError):
|
|
|
|
|
timestamp_key.verify_signature(incorrect_sig, data)
|
2020-08-31 14:10:19 +00:00
|
|
|
|
2022-01-28 18:03:20 +00:00
|
|
|
def test_metadata_signed_is_expired(self) -> None:
|
2021-10-13 16:56:53 +00:00
|
|
|
# Use of Snapshot is arbitrary, we're just testing the base class
|
|
|
|
|
# features with real data
|
2021-10-12 13:14:24 +00:00
|
|
|
snapshot_path = os.path.join(self.repo_dir, "metadata", "snapshot.json")
|
2021-03-04 09:51:45 +00:00
|
|
|
md = Metadata.from_file(snapshot_path)
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
|
2024-02-29 13:12:18 +00:00
|
|
|
expected_expiry = datetime(2030, 1, 1, 0, 0, tzinfo=timezone.utc)
|
|
|
|
|
self.assertEqual(md.signed.expires, expected_expiry)
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
|
2021-04-12 10:57:01 +00:00
|
|
|
# Test is_expired with reference_time provided
|
|
|
|
|
is_expired = md.signed.is_expired(md.signed.expires)
|
|
|
|
|
self.assertTrue(is_expired)
|
|
|
|
|
is_expired = md.signed.is_expired(md.signed.expires + timedelta(days=1))
|
|
|
|
|
self.assertTrue(is_expired)
|
|
|
|
|
is_expired = md.signed.is_expired(md.signed.expires - timedelta(days=1))
|
|
|
|
|
self.assertFalse(is_expired)
|
|
|
|
|
|
2021-05-12 11:35:32 +00:00
|
|
|
# Test is_expired without reference_time,
|
2021-04-12 10:57:01 +00:00
|
|
|
# manipulating md.signed.expires
|
|
|
|
|
expires = md.signed.expires
|
2024-02-26 20:27:38 +00:00
|
|
|
md.signed.expires = datetime.now(timezone.utc)
|
2021-04-12 10:57:01 +00:00
|
|
|
is_expired = md.signed.is_expired()
|
|
|
|
|
self.assertTrue(is_expired)
|
2024-02-26 20:27:38 +00:00
|
|
|
md.signed.expires = datetime.now(timezone.utc) + timedelta(days=1)
|
2021-04-12 10:57:01 +00:00
|
|
|
is_expired = md.signed.is_expired()
|
|
|
|
|
self.assertFalse(is_expired)
|
|
|
|
|
md.signed.expires = expires
|
2021-05-12 11:35:32 +00:00
|
|
|
|
2021-11-18 16:58:16 +00:00
|
|
|
def test_metadata_verify_delegate(self) -> None:
|
2021-10-12 13:14:24 +00:00
|
|
|
root_path = os.path.join(self.repo_dir, "metadata", "root.json")
|
2021-06-15 09:55:39 +00:00
|
|
|
root = Metadata[Root].from_file(root_path)
|
2021-10-12 13:14:24 +00:00
|
|
|
snapshot_path = os.path.join(self.repo_dir, "metadata", "snapshot.json")
|
2021-06-15 09:55:39 +00:00
|
|
|
snapshot = Metadata[Snapshot].from_file(snapshot_path)
|
2021-10-12 13:14:24 +00:00
|
|
|
targets_path = os.path.join(self.repo_dir, "metadata", "targets.json")
|
2021-06-15 09:55:39 +00:00
|
|
|
targets = Metadata[Targets].from_file(targets_path)
|
2021-10-12 13:14:24 +00:00
|
|
|
role1_path = os.path.join(self.repo_dir, "metadata", "role1.json")
|
2021-06-15 09:55:39 +00:00
|
|
|
role1 = Metadata[Targets].from_file(role1_path)
|
2021-10-12 13:14:24 +00:00
|
|
|
role2_path = os.path.join(self.repo_dir, "metadata", "role2.json")
|
2021-06-15 09:55:39 +00:00
|
|
|
role2 = Metadata[Targets].from_file(role2_path)
|
2021-04-22 16:37:38 +00:00
|
|
|
|
|
|
|
|
# test the expected delegation tree
|
2021-11-17 12:24:03 +00:00
|
|
|
root.verify_delegate(Root.type, root)
|
|
|
|
|
root.verify_delegate(Snapshot.type, snapshot)
|
|
|
|
|
root.verify_delegate(Targets.type, targets)
|
2021-10-12 13:14:24 +00:00
|
|
|
targets.verify_delegate("role1", role1)
|
|
|
|
|
role1.verify_delegate("role2", role2)
|
2021-04-22 16:37:38 +00:00
|
|
|
|
|
|
|
|
# only root and targets can verify delegates
|
2021-06-16 11:54:17 +00:00
|
|
|
with self.assertRaises(TypeError):
|
2021-11-17 12:24:03 +00:00
|
|
|
snapshot.verify_delegate(Snapshot.type, snapshot)
|
2021-04-22 16:37:38 +00:00
|
|
|
# verify fails for roles that are not delegated by delegator
|
|
|
|
|
with self.assertRaises(ValueError):
|
2021-10-12 13:14:24 +00:00
|
|
|
root.verify_delegate("role1", role1)
|
2021-04-22 16:37:38 +00:00
|
|
|
with self.assertRaises(ValueError):
|
2021-11-17 12:24:03 +00:00
|
|
|
targets.verify_delegate(Targets.type, targets)
|
2021-07-05 12:01:31 +00:00
|
|
|
# verify fails when delegator has no delegations
|
|
|
|
|
with self.assertRaises(ValueError):
|
2021-10-12 13:14:24 +00:00
|
|
|
role2.verify_delegate("role1", role1)
|
2021-04-22 16:37:38 +00:00
|
|
|
|
2023-05-05 07:22:16 +00:00
|
|
|
def test_signed_verify_delegate(self) -> None:
|
|
|
|
|
root_path = os.path.join(self.repo_dir, "metadata", "root.json")
|
|
|
|
|
root_md = Metadata[Root].from_file(root_path)
|
|
|
|
|
root = root_md.signed
|
|
|
|
|
snapshot_path = os.path.join(self.repo_dir, "metadata", "snapshot.json")
|
|
|
|
|
snapshot_md = Metadata[Snapshot].from_file(snapshot_path)
|
|
|
|
|
snapshot = snapshot_md.signed
|
|
|
|
|
targets_path = os.path.join(self.repo_dir, "metadata", "targets.json")
|
|
|
|
|
targets_md = Metadata[Targets].from_file(targets_path)
|
|
|
|
|
targets = targets_md.signed
|
|
|
|
|
role1_path = os.path.join(self.repo_dir, "metadata", "role1.json")
|
|
|
|
|
role1_md = Metadata[Targets].from_file(role1_path)
|
|
|
|
|
role1 = role1_md.signed
|
|
|
|
|
role2_path = os.path.join(self.repo_dir, "metadata", "role2.json")
|
|
|
|
|
role2_md = Metadata[Targets].from_file(role2_path)
|
|
|
|
|
role2 = role2_md.signed
|
|
|
|
|
|
|
|
|
|
# test the expected delegation tree
|
2023-08-01 13:21:07 +00:00
|
|
|
root.verify_delegate(
|
|
|
|
|
Root.type, root_md.signed_bytes, root_md.signatures
|
|
|
|
|
)
|
|
|
|
|
root.verify_delegate(
|
|
|
|
|
Snapshot.type, snapshot_md.signed_bytes, snapshot_md.signatures
|
|
|
|
|
)
|
|
|
|
|
root.verify_delegate(
|
|
|
|
|
Targets.type, targets_md.signed_bytes, targets_md.signatures
|
|
|
|
|
)
|
|
|
|
|
targets.verify_delegate(
|
|
|
|
|
"role1", role1_md.signed_bytes, role1_md.signatures
|
|
|
|
|
)
|
|
|
|
|
role1.verify_delegate(
|
|
|
|
|
"role2", role2_md.signed_bytes, role2_md.signatures
|
|
|
|
|
)
|
2023-05-05 07:22:16 +00:00
|
|
|
|
|
|
|
|
# only root and targets can verify delegates
|
|
|
|
|
with self.assertRaises(AttributeError):
|
2023-08-01 13:21:07 +00:00
|
|
|
snapshot.verify_delegate(
|
|
|
|
|
Snapshot.type, snapshot_md.signed_bytes, snapshot_md.signatures
|
|
|
|
|
)
|
2023-05-05 07:22:16 +00:00
|
|
|
# verify fails for roles that are not delegated by delegator
|
|
|
|
|
with self.assertRaises(ValueError):
|
2023-08-01 13:21:07 +00:00
|
|
|
root.verify_delegate(
|
|
|
|
|
"role1", role1_md.signed_bytes, role1_md.signatures
|
|
|
|
|
)
|
2023-05-05 07:22:16 +00:00
|
|
|
with self.assertRaises(ValueError):
|
2023-08-01 13:21:07 +00:00
|
|
|
targets.verify_delegate(
|
|
|
|
|
Targets.type, targets_md.signed_bytes, targets_md.signatures
|
|
|
|
|
)
|
2023-05-05 07:22:16 +00:00
|
|
|
# verify fails when delegator has no delegations
|
|
|
|
|
with self.assertRaises(ValueError):
|
2023-08-01 13:21:07 +00:00
|
|
|
role2.verify_delegate(
|
|
|
|
|
"role1", role1_md.signed_bytes, role1_md.signatures
|
|
|
|
|
)
|
2023-05-05 07:22:16 +00:00
|
|
|
|
2021-04-22 16:37:38 +00:00
|
|
|
# verify fails when delegate content is modified
|
2023-05-05 07:22:16 +00:00
|
|
|
expires = snapshot.expires
|
|
|
|
|
snapshot.expires = expires + timedelta(days=1)
|
2021-04-22 16:37:38 +00:00
|
|
|
with self.assertRaises(exceptions.UnsignedMetadataError):
|
2023-08-01 13:21:07 +00:00
|
|
|
root.verify_delegate(
|
|
|
|
|
Snapshot.type, snapshot_md.signed_bytes, snapshot_md.signatures
|
|
|
|
|
)
|
2023-05-05 07:22:16 +00:00
|
|
|
snapshot.expires = expires
|
2021-04-22 16:37:38 +00:00
|
|
|
|
2022-12-01 09:38:19 +00:00
|
|
|
# verify fails if sslib verify fails with VerificationError
|
|
|
|
|
# (in this case signature is malformed)
|
2023-05-05 07:22:16 +00:00
|
|
|
keyid = next(iter(root.roles[Snapshot.type].keyids))
|
|
|
|
|
good_sig = snapshot_md.signatures[keyid].signature
|
|
|
|
|
snapshot_md.signatures[keyid].signature = "foo"
|
2022-12-01 09:38:19 +00:00
|
|
|
with self.assertRaises(exceptions.UnsignedMetadataError):
|
2023-08-01 13:21:07 +00:00
|
|
|
root.verify_delegate(
|
|
|
|
|
Snapshot.type, snapshot_md.signed_bytes, snapshot_md.signatures
|
|
|
|
|
)
|
2023-05-05 07:22:16 +00:00
|
|
|
snapshot_md.signatures[keyid].signature = good_sig
|
2022-12-01 09:38:19 +00:00
|
|
|
|
2021-04-22 16:37:38 +00:00
|
|
|
# verify fails if roles keys do not sign the metadata
|
|
|
|
|
with self.assertRaises(exceptions.UnsignedMetadataError):
|
2023-08-01 13:21:07 +00:00
|
|
|
root.verify_delegate(
|
|
|
|
|
Timestamp.type, snapshot_md.signed_bytes, snapshot_md.signatures
|
|
|
|
|
)
|
2021-04-22 16:37:38 +00:00
|
|
|
|
2021-06-16 11:54:17 +00:00
|
|
|
# Add a key to snapshot role, make sure the new sig fails to verify
|
2023-05-05 07:22:16 +00:00
|
|
|
ts_keyid = next(iter(root.roles[Timestamp.type].keyids))
|
|
|
|
|
root.add_key(root.keys[ts_keyid], Snapshot.type)
|
|
|
|
|
snapshot_md.signatures[ts_keyid] = Signature(ts_keyid, "ff" * 64)
|
2021-06-16 11:54:17 +00:00
|
|
|
|
|
|
|
|
# verify succeeds if threshold is reached even if some signatures
|
|
|
|
|
# fail to verify
|
2023-08-01 13:21:07 +00:00
|
|
|
root.verify_delegate(
|
|
|
|
|
Snapshot.type, snapshot_md.signed_bytes, snapshot_md.signatures
|
|
|
|
|
)
|
2021-06-16 11:54:17 +00:00
|
|
|
|
2021-04-22 16:37:38 +00:00
|
|
|
# verify fails if threshold of signatures is not reached
|
2023-05-05 07:22:16 +00:00
|
|
|
root.roles[Snapshot.type].threshold = 2
|
2021-04-22 16:37:38 +00:00
|
|
|
with self.assertRaises(exceptions.UnsignedMetadataError):
|
2023-08-01 13:21:07 +00:00
|
|
|
root.verify_delegate(
|
|
|
|
|
Snapshot.type, snapshot_md.signed_bytes, snapshot_md.signatures
|
|
|
|
|
)
|
2021-04-22 16:37:38 +00:00
|
|
|
|
2021-06-16 11:54:17 +00:00
|
|
|
# verify succeeds when we correct the new signature and reach the
|
|
|
|
|
# threshold of 2 keys
|
2024-04-24 08:36:57 +00:00
|
|
|
snapshot_md.sign(self.signers[Timestamp.type], append=True)
|
2023-08-01 13:21:07 +00:00
|
|
|
root.verify_delegate(
|
|
|
|
|
Snapshot.type, snapshot_md.signed_bytes, snapshot_md.signatures
|
|
|
|
|
)
|
2021-06-04 15:32:10 +00:00
|
|
|
|
2024-02-05 13:26:31 +00:00
|
|
|
def test_verification_result(self) -> None:
|
2025-03-15 15:08:45 +00:00
|
|
|
key = SSlibKey("", "", "", {"public": ""})
|
|
|
|
|
vr = VerificationResult(3, {"a": key}, {"b": key})
|
2024-02-05 13:26:31 +00:00
|
|
|
self.assertEqual(vr.missing, 2)
|
|
|
|
|
self.assertFalse(vr.verified)
|
|
|
|
|
self.assertFalse(vr)
|
|
|
|
|
|
|
|
|
|
# Add a signature
|
2025-03-15 15:08:45 +00:00
|
|
|
vr.signed["c"] = key
|
2024-02-05 13:26:31 +00:00
|
|
|
self.assertEqual(vr.missing, 1)
|
|
|
|
|
self.assertFalse(vr.verified)
|
|
|
|
|
self.assertFalse(vr)
|
|
|
|
|
|
|
|
|
|
# Add last missing signature
|
2025-03-15 15:08:45 +00:00
|
|
|
vr.signed["d"] = key
|
2024-02-05 13:26:31 +00:00
|
|
|
self.assertEqual(vr.missing, 0)
|
|
|
|
|
self.assertTrue(vr.verified)
|
|
|
|
|
self.assertTrue(vr)
|
|
|
|
|
|
|
|
|
|
# Add one more signature
|
2025-03-15 15:08:45 +00:00
|
|
|
vr.signed["e"] = key
|
2024-02-05 13:26:31 +00:00
|
|
|
self.assertEqual(vr.missing, 0)
|
|
|
|
|
self.assertTrue(vr.verified)
|
|
|
|
|
self.assertTrue(vr)
|
|
|
|
|
|
|
|
|
|
def test_root_verification_result(self) -> None:
|
2025-03-15 15:08:45 +00:00
|
|
|
key = SSlibKey("", "", "", {"public": ""})
|
|
|
|
|
vr1 = VerificationResult(3, {"a": key}, {"b": key})
|
|
|
|
|
vr2 = VerificationResult(1, {"c": key}, {"b": key})
|
2024-02-05 13:26:31 +00:00
|
|
|
|
|
|
|
|
vr = RootVerificationResult(vr1, vr2)
|
2025-03-15 15:08:45 +00:00
|
|
|
self.assertEqual(vr.signed, {"a": key, "c": key})
|
|
|
|
|
self.assertEqual(vr.unsigned, {"b": key})
|
2024-02-05 13:26:31 +00:00
|
|
|
self.assertFalse(vr.verified)
|
|
|
|
|
self.assertFalse(vr)
|
|
|
|
|
|
2025-03-15 15:08:45 +00:00
|
|
|
vr1.signed["c"] = key
|
|
|
|
|
vr1.signed["f"] = key
|
|
|
|
|
self.assertEqual(vr.signed, {"a": key, "c": key, "f": key})
|
|
|
|
|
self.assertEqual(vr.unsigned, {"b": key})
|
2024-02-05 13:26:31 +00:00
|
|
|
self.assertTrue(vr.verified)
|
|
|
|
|
self.assertTrue(vr)
|
|
|
|
|
|
2023-10-03 09:54:04 +00:00
|
|
|
def test_signed_get_verification_result(self) -> None:
|
|
|
|
|
# Setup: Load test metadata and keys
|
|
|
|
|
root_path = os.path.join(self.repo_dir, "metadata", "root.json")
|
|
|
|
|
root = Metadata[Root].from_file(root_path)
|
2024-02-01 17:54:39 +00:00
|
|
|
|
|
|
|
|
key1_id = root.signed.roles[Root.type].keyids[0]
|
|
|
|
|
key1 = root.signed.get_key(key1_id)
|
|
|
|
|
|
|
|
|
|
key2_id = root.signed.roles[Timestamp.type].keyids[0]
|
|
|
|
|
key2 = root.signed.get_key(key2_id)
|
|
|
|
|
|
2023-10-03 09:54:04 +00:00
|
|
|
key3_id = "123456789abcdefg"
|
2024-04-24 08:36:57 +00:00
|
|
|
|
|
|
|
|
key4_id = self.signers[Snapshot.type].public_key.keyid
|
2023-10-03 09:54:04 +00:00
|
|
|
|
|
|
|
|
# Test: 1 authorized key, 1 valid signature
|
|
|
|
|
result = root.signed.get_verification_result(
|
|
|
|
|
Root.type, root.signed_bytes, root.signatures
|
|
|
|
|
)
|
2024-02-01 17:54:39 +00:00
|
|
|
self.assertTrue(result)
|
|
|
|
|
self.assertEqual(result.signed, {key1_id: key1})
|
|
|
|
|
self.assertEqual(result.unsigned, {})
|
2023-10-03 09:54:04 +00:00
|
|
|
|
|
|
|
|
# Test: 2 authorized keys, 1 invalid signature
|
|
|
|
|
# Adding a key, i.e. metadata change, invalidates existing signature
|
2024-02-01 17:54:39 +00:00
|
|
|
root.signed.add_key(key2, Root.type)
|
2023-10-03 09:54:04 +00:00
|
|
|
result = root.signed.get_verification_result(
|
|
|
|
|
Root.type, root.signed_bytes, root.signatures
|
|
|
|
|
)
|
2024-02-01 17:54:39 +00:00
|
|
|
self.assertFalse(result)
|
|
|
|
|
self.assertEqual(result.signed, {})
|
|
|
|
|
self.assertEqual(result.unsigned, {key1_id: key1, key2_id: key2})
|
2023-10-03 09:54:04 +00:00
|
|
|
|
|
|
|
|
# Test: 3 authorized keys, 1 invalid signature, 1 key missing key data
|
2024-02-01 17:54:39 +00:00
|
|
|
# Adding a keyid w/o key, fails verification but this key is not listed
|
|
|
|
|
# in unsigned
|
2023-10-03 09:54:04 +00:00
|
|
|
root.signed.roles[Root.type].keyids.append(key3_id)
|
|
|
|
|
result = root.signed.get_verification_result(
|
|
|
|
|
Root.type, root.signed_bytes, root.signatures
|
|
|
|
|
)
|
2024-02-01 17:54:39 +00:00
|
|
|
self.assertFalse(result)
|
|
|
|
|
self.assertEqual(result.signed, {})
|
|
|
|
|
self.assertEqual(result.unsigned, {key1_id: key1, key2_id: key2})
|
2023-10-03 09:54:04 +00:00
|
|
|
|
|
|
|
|
# Test: 3 authorized keys, 1 valid signature, 1 invalid signature, 1
|
|
|
|
|
# key missing key data
|
2024-04-24 08:36:57 +00:00
|
|
|
root.sign(self.signers[Timestamp.type], append=True)
|
2023-10-03 09:54:04 +00:00
|
|
|
result = root.signed.get_verification_result(
|
|
|
|
|
Root.type, root.signed_bytes, root.signatures
|
|
|
|
|
)
|
2024-02-01 17:54:39 +00:00
|
|
|
self.assertTrue(result)
|
|
|
|
|
self.assertEqual(result.signed, {key2_id: key2})
|
|
|
|
|
self.assertEqual(result.unsigned, {key1_id: key1})
|
2023-10-03 09:54:04 +00:00
|
|
|
|
|
|
|
|
# Test: 3 authorized keys, 1 valid signature, 1 invalid signature, 1
|
|
|
|
|
# key missing key data, 1 ignored unrelated signature
|
2024-04-24 08:36:57 +00:00
|
|
|
root.sign(self.signers[Snapshot.type], append=True)
|
2023-10-03 09:54:04 +00:00
|
|
|
self.assertEqual(
|
|
|
|
|
set(root.signatures.keys()), {key1_id, key2_id, key4_id}
|
|
|
|
|
)
|
2024-02-01 17:54:39 +00:00
|
|
|
self.assertTrue(result)
|
|
|
|
|
self.assertEqual(result.signed, {key2_id: key2})
|
|
|
|
|
self.assertEqual(result.unsigned, {key1_id: key1})
|
2023-10-03 09:54:04 +00:00
|
|
|
|
|
|
|
|
# See test_signed_verify_delegate for more related tests ...
|
|
|
|
|
|
2024-02-01 18:32:07 +00:00
|
|
|
def test_root_get_root_verification_result(self) -> None:
|
|
|
|
|
# Setup: Load test metadata and keys
|
|
|
|
|
root_path = os.path.join(self.repo_dir, "metadata", "root.json")
|
|
|
|
|
root = Metadata[Root].from_file(root_path)
|
|
|
|
|
|
|
|
|
|
key1_id = root.signed.roles[Root.type].keyids[0]
|
|
|
|
|
key1 = root.signed.get_key(key1_id)
|
|
|
|
|
|
|
|
|
|
key2_id = root.signed.roles[Timestamp.type].keyids[0]
|
|
|
|
|
key2 = root.signed.get_key(key2_id)
|
|
|
|
|
|
2024-02-05 13:10:48 +00:00
|
|
|
# Test: Verify with no previous root version
|
|
|
|
|
result = root.signed.get_root_verification_result(
|
|
|
|
|
None, root.signed_bytes, root.signatures
|
|
|
|
|
)
|
|
|
|
|
self.assertTrue(result)
|
|
|
|
|
self.assertEqual(result.signed, {key1_id: key1})
|
|
|
|
|
self.assertEqual(result.unsigned, {})
|
|
|
|
|
|
|
|
|
|
# Test: Verify with other root that is not version N-1
|
|
|
|
|
prev_root: Metadata[Root] = deepcopy(root)
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
result = root.signed.get_root_verification_result(
|
|
|
|
|
prev_root.signed, root.signed_bytes, root.signatures
|
|
|
|
|
)
|
2024-02-01 18:32:07 +00:00
|
|
|
|
2024-02-05 13:10:48 +00:00
|
|
|
# Test: Verify with previous root
|
|
|
|
|
prev_root.signed.version -= 1
|
2024-02-01 18:32:07 +00:00
|
|
|
result = root.signed.get_root_verification_result(
|
2024-02-05 13:10:48 +00:00
|
|
|
prev_root.signed, root.signed_bytes, root.signatures
|
2024-02-01 18:32:07 +00:00
|
|
|
)
|
|
|
|
|
self.assertTrue(result)
|
|
|
|
|
self.assertEqual(result.signed, {key1_id: key1})
|
|
|
|
|
self.assertEqual(result.unsigned, {})
|
|
|
|
|
|
2024-02-05 13:10:48 +00:00
|
|
|
# Test: Add a signer to previous root (threshold still 1)
|
|
|
|
|
prev_root.signed.add_key(key2, Root.type)
|
2024-02-01 18:32:07 +00:00
|
|
|
result = root.signed.get_root_verification_result(
|
2024-02-05 13:10:48 +00:00
|
|
|
prev_root.signed, root.signed_bytes, root.signatures
|
2024-02-01 18:32:07 +00:00
|
|
|
)
|
|
|
|
|
self.assertTrue(result)
|
|
|
|
|
self.assertEqual(result.signed, {key1_id: key1})
|
|
|
|
|
self.assertEqual(result.unsigned, {key2_id: key2})
|
|
|
|
|
|
2024-02-05 13:10:48 +00:00
|
|
|
# Test: Increase threshold in previous root
|
|
|
|
|
prev_root.signed.roles[Root.type].threshold += 1
|
2024-02-01 18:32:07 +00:00
|
|
|
result = root.signed.get_root_verification_result(
|
2024-02-05 13:10:48 +00:00
|
|
|
prev_root.signed, root.signed_bytes, root.signatures
|
2024-02-01 18:32:07 +00:00
|
|
|
)
|
|
|
|
|
self.assertFalse(result)
|
|
|
|
|
self.assertEqual(result.signed, {key1_id: key1})
|
|
|
|
|
self.assertEqual(result.unsigned, {key2_id: key2})
|
|
|
|
|
|
|
|
|
|
# Test: Sign root with both keys
|
2024-04-24 08:36:57 +00:00
|
|
|
root.sign(self.signers[Timestamp.type], append=True)
|
2024-02-01 18:32:07 +00:00
|
|
|
result = root.signed.get_root_verification_result(
|
2024-02-05 13:10:48 +00:00
|
|
|
prev_root.signed, root.signed_bytes, root.signatures
|
2024-02-01 18:32:07 +00:00
|
|
|
)
|
|
|
|
|
self.assertTrue(result)
|
|
|
|
|
self.assertEqual(result.signed, {key1_id: key1, key2_id: key2})
|
|
|
|
|
self.assertEqual(result.unsigned, {})
|
|
|
|
|
|
|
|
|
|
# Test: Sign root with an unrelated key
|
2024-04-24 08:36:57 +00:00
|
|
|
root.sign(self.signers[Snapshot.type], append=True)
|
2024-02-01 18:32:07 +00:00
|
|
|
result = root.signed.get_root_verification_result(
|
2024-02-05 13:10:48 +00:00
|
|
|
prev_root.signed, root.signed_bytes, root.signatures
|
2024-02-01 18:32:07 +00:00
|
|
|
)
|
|
|
|
|
self.assertTrue(result)
|
|
|
|
|
self.assertEqual(result.signed, {key1_id: key1, key2_id: key2})
|
|
|
|
|
self.assertEqual(result.unsigned, {})
|
|
|
|
|
|
2024-02-05 13:10:48 +00:00
|
|
|
# Test: Remove key1 from previous root
|
|
|
|
|
prev_root.signed.revoke_key(key1_id, Root.type)
|
2024-02-01 18:32:07 +00:00
|
|
|
result = root.signed.get_root_verification_result(
|
2024-02-05 13:10:48 +00:00
|
|
|
prev_root.signed, root.signed_bytes, root.signatures
|
2024-02-01 18:32:07 +00:00
|
|
|
)
|
|
|
|
|
self.assertFalse(result)
|
|
|
|
|
self.assertEqual(result.signed, {key1_id: key1, key2_id: key2})
|
|
|
|
|
self.assertEqual(result.unsigned, {})
|
|
|
|
|
|
2024-02-05 13:10:48 +00:00
|
|
|
# Test: Lower threshold in previous root
|
|
|
|
|
prev_root.signed.roles[Root.type].threshold -= 1
|
2024-02-01 18:32:07 +00:00
|
|
|
result = root.signed.get_root_verification_result(
|
2024-02-05 13:10:48 +00:00
|
|
|
prev_root.signed, root.signed_bytes, root.signatures
|
2024-02-01 18:32:07 +00:00
|
|
|
)
|
|
|
|
|
self.assertTrue(result)
|
|
|
|
|
self.assertEqual(result.signed, {key1_id: key1, key2_id: key2})
|
|
|
|
|
self.assertEqual(result.unsigned, {})
|
|
|
|
|
|
2022-06-02 17:33:20 +00:00
|
|
|
def test_root_add_key_and_revoke_key(self) -> None:
|
2021-10-12 13:14:24 +00:00
|
|
|
root_path = os.path.join(self.repo_dir, "metadata", "root.json")
|
2021-06-15 09:55:39 +00:00
|
|
|
root = Metadata[Root].from_file(root_path)
|
2020-10-28 14:37:01 +00:00
|
|
|
|
2021-08-30 11:28:34 +00:00
|
|
|
# Create a new key
|
2024-04-24 08:36:57 +00:00
|
|
|
signer = CryptoSigner.generate_ecdsa()
|
|
|
|
|
key = signer.public_key
|
2020-10-28 14:37:01 +00:00
|
|
|
|
|
|
|
|
# Assert that root does not contain the new key
|
2024-04-24 08:36:57 +00:00
|
|
|
self.assertNotIn(key.keyid, root.signed.roles[Root.type].keyids)
|
|
|
|
|
self.assertNotIn(key.keyid, root.signed.keys)
|
2020-10-28 14:37:01 +00:00
|
|
|
|
2022-06-02 17:33:20 +00:00
|
|
|
# Assert that add_key with old argument order will raise an error
|
|
|
|
|
with self.assertRaises(ValueError):
|
2025-03-15 15:08:45 +00:00
|
|
|
root.signed.add_key(Root.type, key) # type: ignore [arg-type]
|
2022-06-02 17:33:20 +00:00
|
|
|
|
2020-10-28 14:37:01 +00:00
|
|
|
# Add new root key
|
2024-04-24 08:36:57 +00:00
|
|
|
root.signed.add_key(key, Root.type)
|
2020-10-28 14:37:01 +00:00
|
|
|
|
|
|
|
|
# Assert that key is added
|
2024-04-24 08:36:57 +00:00
|
|
|
self.assertIn(key.keyid, root.signed.roles[Root.type].keyids)
|
|
|
|
|
self.assertIn(key.keyid, root.signed.keys)
|
2020-10-28 14:37:01 +00:00
|
|
|
|
2021-05-12 11:35:32 +00:00
|
|
|
# Confirm that the newly added key does not break
|
|
|
|
|
# the object serialization
|
|
|
|
|
root.to_dict()
|
|
|
|
|
|
2021-04-28 10:01:41 +00:00
|
|
|
# Try adding the same key again and assert its ignored.
|
2021-11-17 12:24:03 +00:00
|
|
|
pre_add_keyid = root.signed.roles[Root.type].keyids.copy()
|
2024-04-24 08:36:57 +00:00
|
|
|
root.signed.add_key(key, Root.type)
|
2021-11-17 12:24:03 +00:00
|
|
|
self.assertEqual(pre_add_keyid, root.signed.roles[Root.type].keyids)
|
2021-04-28 10:01:41 +00:00
|
|
|
|
2021-08-30 11:28:34 +00:00
|
|
|
# Add the same key to targets role as well
|
2024-04-24 08:36:57 +00:00
|
|
|
root.signed.add_key(key, Targets.type)
|
2020-10-28 14:37:01 +00:00
|
|
|
|
2021-09-16 12:17:31 +00:00
|
|
|
# Add the same key to a nonexistent role.
|
|
|
|
|
with self.assertRaises(ValueError):
|
2024-04-24 08:36:57 +00:00
|
|
|
root.signed.add_key(key, "nosuchrole")
|
2021-09-16 12:17:31 +00:00
|
|
|
|
2021-08-30 11:28:34 +00:00
|
|
|
# Remove the key from root role (targets role still uses it)
|
2024-04-24 08:36:57 +00:00
|
|
|
root.signed.revoke_key(key.keyid, Root.type)
|
|
|
|
|
self.assertNotIn(key.keyid, root.signed.roles[Root.type].keyids)
|
|
|
|
|
self.assertIn(key.keyid, root.signed.keys)
|
2021-08-30 11:28:34 +00:00
|
|
|
|
|
|
|
|
# Remove the key from targets as well
|
2024-04-24 08:36:57 +00:00
|
|
|
root.signed.revoke_key(key.keyid, Targets.type)
|
|
|
|
|
self.assertNotIn(key.keyid, root.signed.roles[Targets.type].keyids)
|
|
|
|
|
self.assertNotIn(key.keyid, root.signed.keys)
|
2020-10-28 14:37:01 +00:00
|
|
|
|
2021-09-16 12:17:31 +00:00
|
|
|
with self.assertRaises(ValueError):
|
2022-06-02 17:33:20 +00:00
|
|
|
root.signed.revoke_key("nosuchkey", Root.type)
|
2021-09-16 12:17:31 +00:00
|
|
|
with self.assertRaises(ValueError):
|
2024-04-24 08:36:57 +00:00
|
|
|
root.signed.revoke_key(key.keyid, "nosuchrole")
|
2020-10-28 14:37:01 +00:00
|
|
|
|
2021-11-18 16:58:16 +00:00
|
|
|
def test_is_target_in_pathpattern(self) -> None:
|
2021-07-29 14:08:17 +00:00
|
|
|
supported_use_cases = [
|
|
|
|
|
("foo.tgz", "foo.tgz"),
|
|
|
|
|
("foo.tgz", "*"),
|
|
|
|
|
("foo.tgz", "*.tgz"),
|
|
|
|
|
("foo-version-a.tgz", "foo-version-?.tgz"),
|
|
|
|
|
("targets/foo.tgz", "targets/*.tgz"),
|
|
|
|
|
("foo/bar/zoo/k.tgz", "foo/bar/zoo/*"),
|
|
|
|
|
("foo/bar/zoo/k.tgz", "foo/*/zoo/*"),
|
|
|
|
|
("foo/bar/zoo/k.tgz", "*/*/*/*"),
|
|
|
|
|
("foo/bar", "f?o/bar"),
|
|
|
|
|
("foo/bar", "*o/bar"),
|
|
|
|
|
]
|
|
|
|
|
for targetpath, pathpattern in supported_use_cases:
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
DelegatedRole._is_target_in_pathpattern(targetpath, pathpattern)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
invalid_use_cases = [
|
|
|
|
|
("targets/foo.tgz", "*.tgz"),
|
2021-10-12 13:14:24 +00:00
|
|
|
("/foo.tgz", "*.tgz"),
|
2021-07-29 14:08:17 +00:00
|
|
|
("targets/foo.tgz", "*"),
|
|
|
|
|
("foo-version-alpha.tgz", "foo-version-?.tgz"),
|
|
|
|
|
("foo//bar", "*/bar"),
|
2021-10-12 13:14:24 +00:00
|
|
|
("foo/bar", "f?/bar"),
|
2026-04-21 09:27:16 +00:00
|
|
|
("FOO.tgz", "foo.tgz"),
|
|
|
|
|
("foo/bar", "Foo/*"),
|
2021-07-29 14:08:17 +00:00
|
|
|
]
|
|
|
|
|
for targetpath, pathpattern in invalid_use_cases:
|
|
|
|
|
self.assertFalse(
|
|
|
|
|
DelegatedRole._is_target_in_pathpattern(targetpath, pathpattern)
|
|
|
|
|
)
|
|
|
|
|
|
2021-11-18 16:58:16 +00:00
|
|
|
def test_targets_key_api(self) -> None:
|
2021-10-12 13:14:24 +00:00
|
|
|
targets_path = os.path.join(self.repo_dir, "metadata", "targets.json")
|
2021-08-24 14:39:34 +00:00
|
|
|
targets: Targets = Metadata[Targets].from_file(targets_path).signed
|
|
|
|
|
|
|
|
|
|
# Add a new delegated role "role2" in targets
|
2021-10-12 13:14:24 +00:00
|
|
|
delegated_role = DelegatedRole.from_dict(
|
|
|
|
|
{
|
2021-08-24 14:39:34 +00:00
|
|
|
"keyids": [],
|
|
|
|
|
"name": "role2",
|
|
|
|
|
"paths": ["fn3", "fn4"],
|
|
|
|
|
"terminating": False,
|
2021-10-12 13:14:24 +00:00
|
|
|
"threshold": 1,
|
|
|
|
|
}
|
|
|
|
|
)
|
2021-11-18 16:58:16 +00:00
|
|
|
assert isinstance(targets.delegations, Delegations)
|
2024-11-04 04:21:23 +00:00
|
|
|
assert isinstance(targets.delegations.roles, dict)
|
2021-08-24 14:39:34 +00:00
|
|
|
targets.delegations.roles["role2"] = delegated_role
|
|
|
|
|
|
|
|
|
|
key_dict = {
|
|
|
|
|
"keytype": "ed25519",
|
|
|
|
|
"keyval": {
|
|
|
|
|
"public": "edcd0a32a07dce33f7c7873aaffbff36d20ea30787574ead335eefd337e4dacd"
|
|
|
|
|
},
|
2021-10-12 13:14:24 +00:00
|
|
|
"scheme": "ed25519",
|
2021-08-24 14:39:34 +00:00
|
|
|
}
|
|
|
|
|
key = Key.from_dict("id2", key_dict)
|
|
|
|
|
|
2022-06-02 17:33:20 +00:00
|
|
|
# Assert that add_key with old argument order will raise an error
|
|
|
|
|
with self.assertRaises(ValueError):
|
2025-03-15 15:08:45 +00:00
|
|
|
targets.add_key(Root.type, key) # type: ignore [arg-type]
|
2022-06-02 17:33:20 +00:00
|
|
|
|
2021-08-24 14:39:34 +00:00
|
|
|
# Assert that delegated role "role1" does not contain the new key
|
|
|
|
|
self.assertNotIn(key.keyid, targets.delegations.roles["role1"].keyids)
|
2022-06-02 17:33:20 +00:00
|
|
|
targets.add_key(key, "role1")
|
2021-08-24 14:39:34 +00:00
|
|
|
|
|
|
|
|
# Assert that the new key is added to the delegated role "role1"
|
|
|
|
|
self.assertIn(key.keyid, targets.delegations.roles["role1"].keyids)
|
|
|
|
|
|
|
|
|
|
# Confirm that the newly added key does not break the obj serialization
|
|
|
|
|
targets.to_dict()
|
|
|
|
|
|
|
|
|
|
# Try adding the same key again and assert its ignored.
|
|
|
|
|
past_keyid = targets.delegations.roles["role1"].keyids.copy()
|
2022-06-02 17:33:20 +00:00
|
|
|
targets.add_key(key, "role1")
|
2021-08-24 14:39:34 +00:00
|
|
|
self.assertEqual(past_keyid, targets.delegations.roles["role1"].keyids)
|
|
|
|
|
|
|
|
|
|
# Try adding a key to a delegated role that doesn't exists
|
|
|
|
|
with self.assertRaises(ValueError):
|
2022-06-02 17:33:20 +00:00
|
|
|
targets.add_key(key, "nosuchrole")
|
2021-08-24 14:39:34 +00:00
|
|
|
|
|
|
|
|
# Add the same key to "role2" as well
|
2022-06-02 17:33:20 +00:00
|
|
|
targets.add_key(key, "role2")
|
2021-08-24 14:39:34 +00:00
|
|
|
|
|
|
|
|
# Remove the key from "role1" role ("role2" still uses it)
|
2022-06-02 17:33:20 +00:00
|
|
|
targets.revoke_key(key.keyid, "role1")
|
2021-08-24 14:39:34 +00:00
|
|
|
|
|
|
|
|
# Assert that delegated role "role1" doesn't contain the key.
|
|
|
|
|
self.assertNotIn(key.keyid, targets.delegations.roles["role1"].keyids)
|
|
|
|
|
self.assertIn(key.keyid, targets.delegations.roles["role2"].keyids)
|
|
|
|
|
|
|
|
|
|
# Remove the key from "role2" as well
|
2022-06-02 17:33:20 +00:00
|
|
|
targets.revoke_key(key.keyid, "role2")
|
2021-08-24 14:39:34 +00:00
|
|
|
self.assertNotIn(key.keyid, targets.delegations.roles["role2"].keyids)
|
|
|
|
|
|
|
|
|
|
# Try remove key not used by "role1"
|
|
|
|
|
with self.assertRaises(ValueError):
|
2022-06-02 17:33:20 +00:00
|
|
|
targets.revoke_key(key.keyid, "role1")
|
2021-08-24 14:39:34 +00:00
|
|
|
|
|
|
|
|
# Try removing a key from delegated role that doesn't exists
|
|
|
|
|
with self.assertRaises(ValueError):
|
2022-06-02 17:33:20 +00:00
|
|
|
targets.revoke_key(key.keyid, "nosuchrole")
|
2021-08-24 14:39:34 +00:00
|
|
|
|
|
|
|
|
# Remove delegations as a whole
|
|
|
|
|
targets.delegations = None
|
2022-06-02 17:33:20 +00:00
|
|
|
# Test that calling add_key and revoke_key throws an error
|
2021-08-24 14:39:34 +00:00
|
|
|
# and that delegations is still None after each of the api calls
|
|
|
|
|
with self.assertRaises(ValueError):
|
2022-06-02 17:33:20 +00:00
|
|
|
targets.add_key(key, "role1")
|
2021-08-24 14:39:34 +00:00
|
|
|
self.assertTrue(targets.delegations is None)
|
|
|
|
|
with self.assertRaises(ValueError):
|
2022-06-02 17:33:20 +00:00
|
|
|
targets.revoke_key(key.keyid, "role1")
|
2021-08-24 14:39:34 +00:00
|
|
|
self.assertTrue(targets.delegations is None)
|
|
|
|
|
|
2022-06-02 17:33:20 +00:00
|
|
|
def test_targets_key_api_with_succinct_roles(self) -> None:
|
|
|
|
|
targets_path = os.path.join(self.repo_dir, "metadata", "targets.json")
|
|
|
|
|
targets: Targets = Metadata[Targets].from_file(targets_path).signed
|
|
|
|
|
key_dict = {
|
|
|
|
|
"keytype": "ed25519",
|
|
|
|
|
"keyval": {
|
|
|
|
|
"public": "edcd0a32a07dce33f7c7873aaffbff36d20ea30787574ead335eefd337e4dacd"
|
|
|
|
|
},
|
|
|
|
|
"scheme": "ed25519",
|
|
|
|
|
}
|
|
|
|
|
key = Key.from_dict("id2", key_dict)
|
|
|
|
|
|
|
|
|
|
# Remove delegated roles.
|
|
|
|
|
assert targets.delegations is not None
|
|
|
|
|
assert targets.delegations.roles is not None
|
|
|
|
|
targets.delegations.roles = None
|
|
|
|
|
targets.delegations.keys = {}
|
|
|
|
|
|
|
|
|
|
# Add succinct_roles information.
|
|
|
|
|
targets.delegations.succinct_roles = SuccinctRoles([], 1, 8, "foo")
|
|
|
|
|
self.assertEqual(len(targets.delegations.keys), 0)
|
|
|
|
|
self.assertEqual(len(targets.delegations.succinct_roles.keyids), 0)
|
|
|
|
|
|
|
|
|
|
# Add a key to succinct_roles and verify it's saved.
|
|
|
|
|
targets.add_key(key)
|
|
|
|
|
self.assertIn(key.keyid, targets.delegations.keys)
|
|
|
|
|
self.assertIn(key.keyid, targets.delegations.succinct_roles.keyids)
|
|
|
|
|
self.assertEqual(len(targets.delegations.keys), 1)
|
|
|
|
|
|
|
|
|
|
# Try adding the same key again and verify that noting is added.
|
|
|
|
|
targets.add_key(key)
|
|
|
|
|
self.assertEqual(len(targets.delegations.keys), 1)
|
|
|
|
|
|
|
|
|
|
# Remove the key and verify it's not stored anymore.
|
|
|
|
|
targets.revoke_key(key.keyid)
|
|
|
|
|
self.assertNotIn(key.keyid, targets.delegations.keys)
|
|
|
|
|
self.assertNotIn(key.keyid, targets.delegations.succinct_roles.keyids)
|
|
|
|
|
self.assertEqual(len(targets.delegations.keys), 0)
|
|
|
|
|
|
|
|
|
|
# Try removing it again.
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
targets.revoke_key(key.keyid)
|
|
|
|
|
|
2021-11-18 16:58:16 +00:00
|
|
|
def test_length_and_hash_validation(self) -> None:
|
2021-06-04 15:32:10 +00:00
|
|
|
# Test metadata files' hash and length verification.
|
|
|
|
|
# Use timestamp to get a MetaFile object and snapshot
|
|
|
|
|
# for untrusted metadata file to verify.
|
|
|
|
|
timestamp_path = os.path.join(
|
2021-10-12 13:14:24 +00:00
|
|
|
self.repo_dir, "metadata", "timestamp.json"
|
|
|
|
|
)
|
2021-06-15 09:55:39 +00:00
|
|
|
timestamp = Metadata[Timestamp].from_file(timestamp_path)
|
2021-06-14 11:13:15 +00:00
|
|
|
snapshot_metafile = timestamp.signed.snapshot_meta
|
2021-06-04 15:32:10 +00:00
|
|
|
|
2021-10-12 13:14:24 +00:00
|
|
|
snapshot_path = os.path.join(self.repo_dir, "metadata", "snapshot.json")
|
2021-06-04 15:32:10 +00:00
|
|
|
|
|
|
|
|
with open(snapshot_path, "rb") as file:
|
|
|
|
|
# test with data as a file object
|
|
|
|
|
snapshot_metafile.verify_length_and_hashes(file)
|
|
|
|
|
file.seek(0)
|
|
|
|
|
data = file.read()
|
|
|
|
|
# test with data as bytes
|
|
|
|
|
snapshot_metafile.verify_length_and_hashes(data)
|
|
|
|
|
|
2025-03-19 09:34:35 +00:00
|
|
|
# test with custom blake algorithm
|
|
|
|
|
snapshot_metafile.hashes = {
|
|
|
|
|
"blake2b-256": "963a3c31aad8e2a91cfc603fdba12555e48dd0312674ac48cce2c19c243236a1"
|
|
|
|
|
}
|
|
|
|
|
snapshot_metafile.verify_length_and_hashes(data)
|
|
|
|
|
|
2021-06-04 15:32:10 +00:00
|
|
|
# test exceptions
|
|
|
|
|
expected_length = snapshot_metafile.length
|
|
|
|
|
snapshot_metafile.length = 2345
|
2021-11-10 13:27:03 +00:00
|
|
|
with self.assertRaises(exceptions.LengthOrHashMismatchError):
|
|
|
|
|
snapshot_metafile.verify_length_and_hashes(data)
|
2021-06-04 15:32:10 +00:00
|
|
|
|
|
|
|
|
snapshot_metafile.length = expected_length
|
2021-10-12 13:14:24 +00:00
|
|
|
snapshot_metafile.hashes = {"sha256": "incorrecthash"}
|
2021-11-10 13:27:03 +00:00
|
|
|
with self.assertRaises(exceptions.LengthOrHashMismatchError):
|
|
|
|
|
snapshot_metafile.verify_length_and_hashes(data)
|
2021-06-04 15:32:10 +00:00
|
|
|
|
2021-10-12 13:14:24 +00:00
|
|
|
snapshot_metafile.hashes = {
|
|
|
|
|
"unsupported-alg": "8f88e2ba48b412c3843e9bb26e1b6f8fc9e98aceb0fbaa97ba37b4c98717d7ab"
|
|
|
|
|
}
|
2021-11-10 13:27:03 +00:00
|
|
|
with self.assertRaises(exceptions.LengthOrHashMismatchError):
|
|
|
|
|
snapshot_metafile.verify_length_and_hashes(data)
|
2021-06-17 11:11:22 +00:00
|
|
|
|
|
|
|
|
# Test wrong algorithm format (sslib.FormatError)
|
2021-10-12 13:14:24 +00:00
|
|
|
snapshot_metafile.hashes = {
|
2021-11-18 16:58:16 +00:00
|
|
|
256: "8f88e2ba48b412c3843e9bb26e1b6f8fc9e98aceb0fbaa97ba37b4c98717d7ab" # type: ignore[dict-item]
|
2021-10-12 13:14:24 +00:00
|
|
|
}
|
2021-11-10 13:27:03 +00:00
|
|
|
with self.assertRaises(exceptions.LengthOrHashMismatchError):
|
|
|
|
|
snapshot_metafile.verify_length_and_hashes(data)
|
2021-06-17 11:11:22 +00:00
|
|
|
|
2021-06-04 15:32:10 +00:00
|
|
|
# test optional length and hashes
|
|
|
|
|
snapshot_metafile.length = None
|
|
|
|
|
snapshot_metafile.hashes = None
|
|
|
|
|
snapshot_metafile.verify_length_and_hashes(data)
|
|
|
|
|
|
|
|
|
|
# Test target files' hash and length verification
|
2021-10-12 13:14:24 +00:00
|
|
|
targets_path = os.path.join(self.repo_dir, "metadata", "targets.json")
|
2021-06-15 09:55:39 +00:00
|
|
|
targets = Metadata[Targets].from_file(targets_path)
|
2021-10-12 13:14:24 +00:00
|
|
|
file1_targetfile = targets.signed.targets["file1.txt"]
|
2021-11-17 12:24:03 +00:00
|
|
|
filepath = os.path.join(self.repo_dir, Targets.type, "file1.txt")
|
2021-06-04 15:32:10 +00:00
|
|
|
|
|
|
|
|
with open(filepath, "rb") as file1:
|
|
|
|
|
file1_targetfile.verify_length_and_hashes(file1)
|
|
|
|
|
|
|
|
|
|
# test exceptions
|
|
|
|
|
expected_length = file1_targetfile.length
|
|
|
|
|
file1_targetfile.length = 2345
|
2021-11-10 13:27:03 +00:00
|
|
|
with self.assertRaises(exceptions.LengthOrHashMismatchError):
|
|
|
|
|
file1_targetfile.verify_length_and_hashes(file1)
|
2021-06-04 15:32:10 +00:00
|
|
|
|
|
|
|
|
file1_targetfile.length = expected_length
|
2021-10-12 13:14:24 +00:00
|
|
|
file1_targetfile.hashes = {"sha256": "incorrecthash"}
|
2021-11-10 13:27:03 +00:00
|
|
|
with self.assertRaises(exceptions.LengthOrHashMismatchError):
|
|
|
|
|
file1_targetfile.verify_length_and_hashes(file1)
|
2021-08-10 06:13:33 +00:00
|
|
|
|
2021-11-18 16:58:16 +00:00
|
|
|
def test_targetfile_from_file(self) -> None:
|
2021-08-10 06:13:33 +00:00
|
|
|
# Test with an existing file and valid hash algorithm
|
2021-11-17 12:24:03 +00:00
|
|
|
file_path = os.path.join(self.repo_dir, Targets.type, "file1.txt")
|
2021-08-10 06:13:33 +00:00
|
|
|
targetfile_from_file = TargetFile.from_file(
|
2021-10-12 13:14:24 +00:00
|
|
|
file_path, file_path, ["sha256"]
|
2021-08-10 06:13:33 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with open(file_path, "rb") as file:
|
|
|
|
|
targetfile_from_file.verify_length_and_hashes(file)
|
|
|
|
|
|
|
|
|
|
# Test with a non-existing file
|
2021-11-17 12:24:03 +00:00
|
|
|
file_path = os.path.join(self.repo_dir, Targets.type, "file123.txt")
|
2021-11-10 13:27:03 +00:00
|
|
|
with self.assertRaises(FileNotFoundError):
|
2025-03-18 13:49:24 +00:00
|
|
|
TargetFile.from_file(file_path, file_path, ["sha256"])
|
2021-08-10 06:13:33 +00:00
|
|
|
|
|
|
|
|
# Test with an unsupported algorithm
|
2021-11-17 12:24:03 +00:00
|
|
|
file_path = os.path.join(self.repo_dir, Targets.type, "file1.txt")
|
2021-12-17 16:17:33 +00:00
|
|
|
with self.assertRaises(ValueError):
|
2021-11-10 13:27:03 +00:00
|
|
|
TargetFile.from_file(file_path, file_path, ["123"])
|
2021-08-10 06:13:33 +00:00
|
|
|
|
2022-02-07 15:00:26 +00:00
|
|
|
def test_targetfile_custom(self) -> None:
|
|
|
|
|
# Test creating TargetFile and accessing custom.
|
|
|
|
|
targetfile = TargetFile(
|
|
|
|
|
100, {"sha256": "abc"}, "file.txt", {"custom": "foo"}
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(targetfile.custom, "foo")
|
|
|
|
|
|
2021-11-18 16:58:16 +00:00
|
|
|
def test_targetfile_from_data(self) -> None:
|
2021-08-10 06:13:33 +00:00
|
|
|
data = b"Inline test content"
|
2021-12-03 16:11:40 +00:00
|
|
|
target_file_path = os.path.join(
|
|
|
|
|
self.repo_dir, Targets.type, "file1.txt"
|
|
|
|
|
)
|
2021-10-12 13:14:24 +00:00
|
|
|
|
2021-08-10 06:13:33 +00:00
|
|
|
# Test with a valid hash algorithm
|
2021-10-12 13:14:24 +00:00
|
|
|
targetfile_from_data = TargetFile.from_data(
|
|
|
|
|
target_file_path, data, ["sha256"]
|
|
|
|
|
)
|
2021-08-10 06:13:33 +00:00
|
|
|
targetfile_from_data.verify_length_and_hashes(data)
|
|
|
|
|
|
|
|
|
|
# Test with no algorithms specified
|
|
|
|
|
targetfile_from_data = TargetFile.from_data(target_file_path, data)
|
|
|
|
|
targetfile_from_data.verify_length_and_hashes(data)
|
2021-06-04 15:32:10 +00:00
|
|
|
|
2025-03-19 09:34:35 +00:00
|
|
|
# Test with custom blake hash algorithm
|
|
|
|
|
targetfile_from_data = TargetFile.from_data(
|
|
|
|
|
target_file_path, data, ["blake2b-256"]
|
|
|
|
|
)
|
|
|
|
|
targetfile_from_data.verify_length_and_hashes(data)
|
|
|
|
|
|
2023-08-14 12:22:29 +00:00
|
|
|
def test_metafile_from_data(self) -> None:
|
|
|
|
|
data = b"Inline test content"
|
|
|
|
|
|
|
|
|
|
# Test with a valid hash algorithm
|
|
|
|
|
metafile = MetaFile.from_data(1, data, ["sha256"])
|
|
|
|
|
metafile.verify_length_and_hashes(data)
|
|
|
|
|
|
|
|
|
|
# Test with an invalid hash algorithm
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
metafile = MetaFile.from_data(1, data, ["invalid_algorithm"])
|
|
|
|
|
metafile.verify_length_and_hashes(data)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
metafile,
|
|
|
|
|
MetaFile(
|
|
|
|
|
1,
|
|
|
|
|
19,
|
|
|
|
|
{
|
|
|
|
|
"sha256": "fcee2e6d56ab08eab279016f7db7e4e1d172ccea78e15f4cf8bd939991a418fa"
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
2025-03-19 09:34:35 +00:00
|
|
|
# Test with custom blake hash algorithm
|
|
|
|
|
metafile = MetaFile.from_data(1, data, ["blake2b-256"])
|
|
|
|
|
metafile.verify_length_and_hashes(data)
|
|
|
|
|
|
2023-03-20 14:12:00 +00:00
|
|
|
def test_targetfile_get_prefixed_paths(self) -> None:
|
|
|
|
|
target = TargetFile(100, {"sha256": "abc", "md5": "def"}, "a/b/f.ext")
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
target.get_prefixed_paths(), ["a/b/abc.f.ext", "a/b/def.f.ext"]
|
2022-11-17 00:22:21 +00:00
|
|
|
)
|
|
|
|
|
|
2023-03-20 14:12:00 +00:00
|
|
|
target = TargetFile(100, {"sha256": "abc", "md5": "def"}, "")
|
|
|
|
|
self.assertEqual(target.get_prefixed_paths(), ["abc.", "def."])
|
2022-11-18 22:36:08 +00:00
|
|
|
|
2023-03-20 14:12:00 +00:00
|
|
|
target = TargetFile(100, {"sha256": "abc", "md5": "def"}, "a/b/")
|
|
|
|
|
self.assertEqual(target.get_prefixed_paths(), ["a/b/abc.", "a/b/def."])
|
2022-11-18 22:36:08 +00:00
|
|
|
|
2023-03-20 14:12:00 +00:00
|
|
|
target = TargetFile(100, {"sha256": "abc", "md5": "def"}, "f.ext")
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
target.get_prefixed_paths(), ["abc.f.ext", "def.f.ext"]
|
2022-11-18 22:36:08 +00:00
|
|
|
)
|
|
|
|
|
|
2023-03-20 14:12:00 +00:00
|
|
|
target = TargetFile(100, {"sha256": "abc", "md5": "def"}, "a/b/.ext")
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
target.get_prefixed_paths(), ["a/b/abc..ext", "a/b/def..ext"]
|
2022-11-18 22:36:08 +00:00
|
|
|
)
|
|
|
|
|
|
2023-03-20 14:12:00 +00:00
|
|
|
target = TargetFile(100, {"sha256": "abc"}, "/root/file.ext")
|
|
|
|
|
self.assertEqual(target.get_prefixed_paths(), ["/root/abc.file.ext"])
|
2022-11-18 22:36:08 +00:00
|
|
|
|
2023-03-20 14:12:00 +00:00
|
|
|
target = TargetFile(100, {"sha256": "abc"}, "/")
|
|
|
|
|
self.assertEqual(target.get_prefixed_paths(), ["/abc."])
|
2022-11-18 22:36:08 +00:00
|
|
|
|
2021-11-18 16:58:16 +00:00
|
|
|
def test_is_delegated_role(self) -> None:
|
2021-08-30 12:03:37 +00:00
|
|
|
# test path matches
|
|
|
|
|
# see more extensive tests in test_is_target_in_pathpattern()
|
|
|
|
|
for paths in [
|
|
|
|
|
["a/path"],
|
|
|
|
|
["otherpath", "a/path"],
|
|
|
|
|
["*/?ath"],
|
|
|
|
|
]:
|
|
|
|
|
role = DelegatedRole("", [], 1, False, paths, None)
|
|
|
|
|
self.assertFalse(role.is_delegated_path("a/non-matching path"))
|
|
|
|
|
self.assertTrue(role.is_delegated_path("a/path"))
|
|
|
|
|
|
|
|
|
|
# test path hash prefix matches: sha256 sum of "a/path" is 927b0ecf9...
|
|
|
|
|
for hash_prefixes in [
|
|
|
|
|
["927b0ecf9"],
|
|
|
|
|
["other prefix", "927b0ecf9"],
|
|
|
|
|
["927b0"],
|
|
|
|
|
["92"],
|
|
|
|
|
]:
|
|
|
|
|
role = DelegatedRole("", [], 1, False, None, hash_prefixes)
|
|
|
|
|
self.assertFalse(role.is_delegated_path("a/non-matching path"))
|
|
|
|
|
self.assertTrue(role.is_delegated_path("a/path"))
|
2021-04-09 13:20:24 +00:00
|
|
|
|
2022-05-18 14:47:24 +00:00
|
|
|
def test_is_delegated_role_in_succinct_roles(self) -> None:
|
|
|
|
|
succinct_roles = SuccinctRoles([], 1, 5, "bin")
|
2022-06-24 13:48:56 +00:00
|
|
|
false_role_name_examples = [
|
|
|
|
|
"foo",
|
|
|
|
|
"bin-",
|
|
|
|
|
"bin-s",
|
|
|
|
|
"bin-0t",
|
|
|
|
|
"bin-20",
|
|
|
|
|
"bin-100",
|
|
|
|
|
]
|
2022-05-18 14:47:24 +00:00
|
|
|
for role_name in false_role_name_examples:
|
|
|
|
|
msg = f"Error for {role_name}"
|
|
|
|
|
self.assertFalse(succinct_roles.is_delegated_role(role_name), msg)
|
|
|
|
|
|
|
|
|
|
# delegated role name suffixes are in hex format.
|
|
|
|
|
true_name_examples = ["bin-00", "bin-0f", "bin-1f"]
|
|
|
|
|
for role_name in true_name_examples:
|
|
|
|
|
msg = f"Error for {role_name}"
|
|
|
|
|
self.assertTrue(succinct_roles.is_delegated_role(role_name), msg)
|
|
|
|
|
|
|
|
|
|
def test_get_roles_in_succinct_roles(self) -> None:
|
|
|
|
|
succinct_roles = SuccinctRoles([], 1, 16, "bin")
|
|
|
|
|
# bin names are in hex format and 4 hex digits are enough to represent
|
|
|
|
|
# all bins between 0 and 2^16 - 1 meaning suffix_len must be 4
|
|
|
|
|
expected_suffix_length = 4
|
|
|
|
|
self.assertEqual(succinct_roles.suffix_len, expected_suffix_length)
|
|
|
|
|
for bin_numer, role_name in enumerate(succinct_roles.get_roles()):
|
|
|
|
|
# This adds zero-padding if the bin_numer is represented by a hex
|
|
|
|
|
# number with a length less than expected_suffix_length.
|
|
|
|
|
expected_bin_suffix = f"{bin_numer:0{expected_suffix_length}x}"
|
|
|
|
|
self.assertEqual(role_name, f"bin-{expected_bin_suffix}")
|
|
|
|
|
|
2024-02-11 11:03:11 +00:00
|
|
|
def test_delegations_get_delegated_role(self) -> None:
|
|
|
|
|
delegations = Delegations({}, {})
|
|
|
|
|
targets = Targets(delegations=delegations)
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
targets.get_delegated_role("abc")
|
|
|
|
|
|
|
|
|
|
# test "normal" delegated role (path or path_hash_prefix)
|
|
|
|
|
role = DelegatedRole("delegated", [], 1, False, [])
|
|
|
|
|
delegations.roles = {"delegated": role}
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
targets.get_delegated_role("not-delegated")
|
|
|
|
|
self.assertEqual(targets.get_delegated_role("delegated"), role)
|
|
|
|
|
delegations.roles = None
|
|
|
|
|
|
|
|
|
|
# test succinct delegation
|
|
|
|
|
bit_len = 3
|
|
|
|
|
role2 = SuccinctRoles([], 1, bit_len, "prefix")
|
|
|
|
|
delegations.succinct_roles = role2
|
|
|
|
|
for name in ["prefix-", "prefix--1", f"prefix-{2**bit_len:0x}"]:
|
|
|
|
|
with self.assertRaises(ValueError, msg=f"role name '{name}'"):
|
|
|
|
|
targets.get_delegated_role(name)
|
2024-04-03 10:44:36 +00:00
|
|
|
for i in range(2**bit_len):
|
2024-02-11 11:03:11 +00:00
|
|
|
self.assertEqual(
|
|
|
|
|
targets.get_delegated_role(f"prefix-{i:0x}"), role2
|
|
|
|
|
)
|
|
|
|
|
|
2021-10-12 13:14:24 +00:00
|
|
|
|
2023-10-12 09:55:45 +00:00
|
|
|
class TestSimpleEnvelope(unittest.TestCase):
|
|
|
|
|
"""Tests for public API in 'tuf/api/dsse.py'."""
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def setUpClass(cls) -> None:
|
|
|
|
|
repo_data_dir = Path(utils.TESTS_DIR) / "repository_data"
|
|
|
|
|
cls.metadata_dir = repo_data_dir / "repository" / "metadata"
|
2024-04-24 08:36:57 +00:00
|
|
|
cls.keystore_dir = repo_data_dir / "keystore"
|
|
|
|
|
cls.signers = {}
|
|
|
|
|
root_path = os.path.join(cls.metadata_dir, "root.json")
|
|
|
|
|
root: Root = Metadata.from_file(root_path).signed
|
|
|
|
|
|
2023-10-12 09:55:45 +00:00
|
|
|
for role in [Snapshot, Targets, Timestamp]:
|
2024-04-24 08:36:57 +00:00
|
|
|
uri = f"file2:{os.path.join(cls.keystore_dir, role.type + '_key')}"
|
|
|
|
|
role_obj = root.get_delegated_role(role.type)
|
|
|
|
|
key = root.get_key(role_obj.keyids[0])
|
|
|
|
|
cls.signers[role.type] = CryptoSigner.from_priv_key_uri(uri, key)
|
2023-10-12 09:55:45 +00:00
|
|
|
|
|
|
|
|
def test_serialization(self) -> None:
|
|
|
|
|
"""Basic de/serialization test.
|
|
|
|
|
|
|
|
|
|
1. Load test metadata for each role
|
|
|
|
|
2. Wrap metadata payloads in envelope serializing the payload
|
|
|
|
|
3. Serialize envelope
|
|
|
|
|
4. De-serialize envelope
|
|
|
|
|
5. De-serialize payload
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
for role in [Root, Timestamp, Snapshot, Targets]:
|
|
|
|
|
metadata_path = self.metadata_dir / f"{role.type}.json"
|
|
|
|
|
metadata = Metadata.from_file(str(metadata_path))
|
|
|
|
|
self.assertIsInstance(metadata.signed, role)
|
|
|
|
|
|
|
|
|
|
envelope = SimpleEnvelope.from_signed(metadata.signed)
|
|
|
|
|
envelope_bytes = envelope.to_bytes()
|
|
|
|
|
|
|
|
|
|
envelope2 = SimpleEnvelope.from_bytes(envelope_bytes)
|
|
|
|
|
payload = envelope2.get_signed()
|
|
|
|
|
self.assertEqual(metadata.signed, payload)
|
|
|
|
|
|
|
|
|
|
def test_fail_envelope_serialization(self) -> None:
|
2025-03-15 15:08:45 +00:00
|
|
|
envelope = SimpleEnvelope(b"foo", "bar", []) # type: ignore[arg-type]
|
2023-10-12 09:55:45 +00:00
|
|
|
with self.assertRaises(SerializationError):
|
|
|
|
|
envelope.to_bytes()
|
|
|
|
|
|
|
|
|
|
def test_fail_envelope_deserialization(self) -> None:
|
|
|
|
|
with self.assertRaises(DeserializationError):
|
|
|
|
|
SimpleEnvelope.from_bytes(b"[")
|
|
|
|
|
|
|
|
|
|
def test_fail_payload_serialization(self) -> None:
|
|
|
|
|
with self.assertRaises(SerializationError):
|
2024-04-03 11:43:10 +00:00
|
|
|
SimpleEnvelope.from_signed("foo") # type: ignore[type-var]
|
2023-10-12 09:55:45 +00:00
|
|
|
|
|
|
|
|
def test_fail_payload_deserialization(self) -> None:
|
|
|
|
|
payloads = [b"[", b'{"_type": "foo"}']
|
|
|
|
|
for payload in payloads:
|
2025-03-15 15:08:45 +00:00
|
|
|
envelope = SimpleEnvelope(payload, "bar", {})
|
2023-10-12 09:55:45 +00:00
|
|
|
with self.assertRaises(DeserializationError):
|
|
|
|
|
envelope.get_signed()
|
|
|
|
|
|
|
|
|
|
def test_verify_delegate(self) -> None:
|
|
|
|
|
"""Basic verification test.
|
|
|
|
|
|
|
|
|
|
1. Load test metadata for each role
|
|
|
|
|
2. Wrap non-root payloads in envelope serializing the payload
|
|
|
|
|
3. Sign with correct delegated key
|
|
|
|
|
4. Verify delegate with root
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
root_path = self.metadata_dir / "root.json"
|
|
|
|
|
root = Metadata[Root].from_file(str(root_path)).signed
|
|
|
|
|
|
|
|
|
|
for role in [Timestamp, Snapshot, Targets]:
|
|
|
|
|
metadata_path = self.metadata_dir / f"{role.type}.json"
|
|
|
|
|
metadata = Metadata.from_file(str(metadata_path))
|
|
|
|
|
self.assertIsInstance(metadata.signed, role)
|
|
|
|
|
|
2024-04-24 08:36:57 +00:00
|
|
|
signer = self.signers[role.type]
|
|
|
|
|
self.assertIn(signer.public_key.keyid, root.roles[role.type].keyids)
|
2023-10-12 09:55:45 +00:00
|
|
|
|
|
|
|
|
envelope = SimpleEnvelope.from_signed(metadata.signed)
|
|
|
|
|
envelope.sign(signer)
|
|
|
|
|
self.assertTrue(len(envelope.signatures) == 1)
|
|
|
|
|
|
2024-04-24 08:36:57 +00:00
|
|
|
root.verify_delegate(role.type, envelope.pae(), envelope.signatures)
|
2023-10-12 09:55:45 +00:00
|
|
|
|
|
|
|
|
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
# Run unit test.
|
2021-10-12 13:14:24 +00:00
|
|
|
if __name__ == "__main__":
|
2020-09-15 15:05:51 +00:00
|
|
|
utils.configure_test_logging(sys.argv)
|
Add simple TUF role metadata model (WIP)
Add metadata module with container classes for TUF role metadata, including
methods to read/serialize/write from and to JSON, perform TUF-compliant
metadata updates, and create and verify signatures.
The 'Metadata' class provides a container for inner TUF metadata objects (Root,
Timestamp, Snapshot, Targets) (i.e. OOP composition)
The 'Signed' class provides a base class to aggregate common attributes (i.e.
version, expires, spec_version) of the inner metadata classes. (i.e. OOP
inheritance). The name of the class also aligns with the 'signed' field of
the outer metadata container.
Based on prior observations in TUF's sister project in-toto, this architecture
seems to well represent the metadata model as it is defined in the
specification (see in-toto/in-toto#98 and in-toto/in-toto#142 for related
discussions).
This commits also adds tests.
**TODO: See doc header TODO list**
**Additional design considerations**
(also in regards to prior sketches of this module)
- Aims at simplicity, brevity and recognizability of the wireline metadata
format.
- All attributes that correspond to fields in TUF JSON metadata are public.
There doesn't seem to be a good reason to protect them with leading
underscores and use setters/getters instead, it just adds more code, and
impedes recognizability of the wireline metadata format.
- Although, it might be convenient to have short-cuts on the Metadata class
that point to methods and attributes that are common to all subclasses of
the contained Signed class (e.g. Metadata.version instead of
Metadata.signed.version, etc.), this also conflicts with goal of
recognizability of the wireline metadata. Thus we won't add such short-cuts
for now. See:
https://github.com/theupdateframework/tuf/pull/1060#discussion_r452906629
- Signing keys and a 'consistent_snapshot' boolean are not on the targets
metadata class. They are a better fit for management code. See:
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376,
and #660.
- Does not use sslib schema checks (see TODO notes about validation in
doc header)
- Does not use existing tuf utils, such as make_metadata_fileinfo,
build_dict_conforming_to_schema, if it is easy and more explicit to
just re-implement the desired behavior on the metadata classes.
- All datetime's are treated as UTC. Since timezone info is not captured in
the wireline metadata format it should not be captured in the internal
representation either.
- Does not use 3rd-party dateutil package, in order to minimize dependency
footprint, which is especially important for update clients which often have
to vendor their dependencies.
However, compatibility between the more advanced dateutil.relativedelta (e.g
handles leap years automatically) and timedelta is tested.
- Uses PEP8 indentation (4 space) and Google-style doc string instead of
sslab-style. See
https://github.com/secure-systems-lab/code-style-guidelines/issues/20
- Does not support Python =< 3.5
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
Co-authored-by: Joshua Lock <jlock@vmware.com>
Co-authored-by: Teodora Sechkova <tsechkova@vmware.com>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2020-08-18 09:15:49 +00:00
|
|
|
unittest.main()
|