|
| 1 | +# Copyright 2021-2022 python-tuf contributors |
| 2 | +# SPDX-License-Identifier: MIT OR Apache-2.0 |
| 3 | + |
| 4 | +"""Simple example of using the repository library to build a repository""" |
| 5 | + |
| 6 | +import copy |
| 7 | +import logging |
| 8 | +from collections import defaultdict |
| 9 | +from datetime import datetime, timedelta |
| 10 | +from typing import Dict, List |
| 11 | + |
| 12 | +from securesystemslib import keys |
| 13 | +from securesystemslib.signer import Signer, SSlibSigner |
| 14 | + |
| 15 | +from tuf.api.metadata import ( |
| 16 | + Key, |
| 17 | + Metadata, |
| 18 | + MetaFile, |
| 19 | + Root, |
| 20 | + Snapshot, |
| 21 | + TargetFile, |
| 22 | + Targets, |
| 23 | + Timestamp, |
| 24 | +) |
| 25 | +from tuf.repository import Repository |
| 26 | + |
| 27 | +logger = logging.getLogger(__name__) |
| 28 | + |
| 29 | +_signed_init = { |
| 30 | + Root.type: Root, |
| 31 | + Snapshot.type: Snapshot, |
| 32 | + Targets.type: Targets, |
| 33 | + Timestamp.type: Timestamp, |
| 34 | +} |
| 35 | + |
| 36 | + |
| 37 | +class SimpleRepository(Repository): |
| 38 | + """Very simple in-memory repository implementation |
| 39 | +
|
| 40 | + This repository keeps the metadata for all versions of all roles in memory. |
| 41 | + It also keeps all target content in memory. |
| 42 | +
|
| 43 | +
|
| 44 | + Attributes: |
| 45 | + role_cache: Contains every historical metadata version of every role in |
| 46 | + this repositorys. Keys are rolenames and values are lists of |
| 47 | + Metadata |
| 48 | + signer_cache: Contains all signers available to the repository. Keys |
| 49 | + are rolenames, values are lists of signers |
| 50 | + target_cache: |
| 51 | + """ |
| 52 | + |
| 53 | + expiry_period = timedelta(days=1) |
| 54 | + |
| 55 | + def __init__(self) -> None: |
| 56 | + # all versions of all metadata |
| 57 | + self.role_cache: Dict[str, List[Metadata]] = defaultdict(list) |
| 58 | + # all current keys |
| 59 | + self.signer_cache: Dict[str, List[Signer]] = defaultdict(list) |
| 60 | + # all target content |
| 61 | + self.target_cache: Dict[str, bytes] = {} |
| 62 | + |
| 63 | + # setup a basic repository, generate signing key per top-level role |
| 64 | + with self.edit("root", init=True) as root: |
| 65 | + for role in ["root", "timestamp", "snapshot", "targets"]: |
| 66 | + key = keys.generate_ed25519_key() |
| 67 | + self.signer_cache[role].append(SSlibSigner(key)) |
| 68 | + root.add_key(Key.from_securesystemslib_key(key), role) |
| 69 | + |
| 70 | + for role in ["timestamp", "snapshot", "targets"]: |
| 71 | + with self.edit(role, init=True): |
| 72 | + pass |
| 73 | + |
| 74 | + def open(self, role: str, init: bool = False) -> Metadata: |
| 75 | + """Return current Metadata for role from 'storage' (or create a new one)""" |
| 76 | + |
| 77 | + if init: |
| 78 | + signed_init = _signed_init.get(role, Targets) |
| 79 | + md = Metadata(signed_init()) |
| 80 | + |
| 81 | + # this makes version bumping in close() simpler |
| 82 | + md.signed.version = 0 |
| 83 | + return md |
| 84 | + |
| 85 | + # return latest metadata from storage (but don't return a reference) |
| 86 | + return copy.deepcopy(self.role_cache[role][-1]) |
| 87 | + |
| 88 | + def close(self, role: str, md: Metadata, sign_only: bool = False) -> None: |
| 89 | + """Store a version of metadata. Handle version bumps, expiry, signing""" |
| 90 | + if sign_only: |
| 91 | + for signer in self.signer_cache[role]: |
| 92 | + md.sign(signer, append=True) |
| 93 | + self.role_cache[role][-1] = md |
| 94 | + else: |
| 95 | + md.signed.version += 1 |
| 96 | + md.signed.expires = datetime.utcnow() + self.expiry_period |
| 97 | + |
| 98 | + md.signatures.clear() |
| 99 | + for signer in self.signer_cache[role]: |
| 100 | + md.sign(signer, append=True) |
| 101 | + |
| 102 | + self.role_cache[role].append(md) |
| 103 | + |
| 104 | + def add_target(self, path: str, content: str) -> None: |
| 105 | + """Add a target to repository""" |
| 106 | + data = bytes(content, "utf-8") |
| 107 | + |
| 108 | + # add content to cache for serving to clients |
| 109 | + self.target_cache[path] = data |
| 110 | + |
| 111 | + # add a target in the targets metadata |
| 112 | + with self.edit("targets") as targets: |
| 113 | + targets.targets[path] = TargetFile.from_data(path, data) |
| 114 | + |
| 115 | + logger.debug("Targets v%d", targets.version) |
| 116 | + |
| 117 | + # update snapshot, timestamp |
| 118 | + meta = {"targets.json": MetaFile(targets.version)} |
| 119 | + new_version, _ = self.snapshot(meta) |
| 120 | + if new_version is not None: |
| 121 | + self.timestamp(MetaFile(new_version)) |
0 commit comments