codeimprint 0.5.0a11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codeimprint-0.5.0a11.dist-info/METADATA +92 -0
- codeimprint-0.5.0a11.dist-info/RECORD +23 -0
- codeimprint-0.5.0a11.dist-info/WHEEL +4 -0
- codeimprint-0.5.0a11.dist-info/entry_points.txt +2 -0
- codeimprint-0.5.0a11.dist-info/licenses/LICENSE +177 -0
- codeimprint-0.5.0a11.dist-info/licenses/NOTICE +11 -0
- codeimprint_cli/__init__.py +7 -0
- codeimprint_cli/__main__.py +5 -0
- codeimprint_cli/api.py +315 -0
- codeimprint_cli/chainseal.py +175 -0
- codeimprint_cli/cli.py +2128 -0
- codeimprint_cli/config.py +181 -0
- codeimprint_cli/gitrepo.py +148 -0
- codeimprint_cli/imprint_file.py +128 -0
- codeimprint_cli/linecount.py +340 -0
- codeimprint_cli/ownership.py +341 -0
- codeimprint_cli/project.py +129 -0
- codeimprint_cli/receipt.py +679 -0
- codeimprint_cli/records.py +59 -0
- codeimprint_cli/timestamp.py +187 -0
- codeimprint_cli/transparency.py +71 -0
- codeimprint_cli/trust.py +190 -0
- codeimprint_cli/tsa_roots.py +137 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Ownership records (receipt format 4): the public byte format, for verification only.
|
|
2
|
+
|
|
3
|
+
A record's digest is SHA-256 of its canonical JSON document; its ID is a typed ID built from
|
|
4
|
+
that digest with its type's prefix. Each type fixes the attestation ``kind`` and the document
|
|
5
|
+
fields that name other records or seals (they become the attestation's ``parents``). Owner and
|
|
6
|
+
counter-party signatures are Ed25519 over the canonical
|
|
7
|
+
``{"type": "codeimprint.record-sign.v1", "record_type": <type>, "digest": <digest>}``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
RECORD_SIGN = "codeimprint.record-sign.v1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class RecordType:
|
|
19
|
+
prefix: str
|
|
20
|
+
kind: str
|
|
21
|
+
refs: tuple[str, ...] = ()
|
|
22
|
+
roles: tuple[str, ...] = ()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
TYPES: dict[str, RecordType] = {
|
|
26
|
+
"codeimprint.identity.v1": RecordType("IDT1", "identity", ("renews",)),
|
|
27
|
+
"codeimprint.revocation.v1": RecordType("REV1", "revocation", ("revokes",)),
|
|
28
|
+
"codeimprint.org-member.v1": RecordType("MBR1", "membership", (), ("member",)),
|
|
29
|
+
"codeimprint.org-member-end.v1": RecordType("MBR1", "membership-end", ("membership",)),
|
|
30
|
+
"codeimprint.org-endorsement.v1": RecordType("END1", "endorsement", ("head",), ("signer",)),
|
|
31
|
+
"codeimprint.claim.v1": RecordType("CLM1", "claim", ("counters", "supersedes"),
|
|
32
|
+
("claimant",)),
|
|
33
|
+
"codeimprint.claim-key.v1": RecordType("CLM1", "claim-key", ("claim",), ("authorised",)),
|
|
34
|
+
"codeimprint.transfer.v1": RecordType("XFR1", "transfer", ("from_claim",), ("from", "to")),
|
|
35
|
+
"codeimprint.org-transfer.v1": RecordType("XFR1", "org-transfer", (), ("from", "to")),
|
|
36
|
+
"codeimprint.assignment.v1": RecordType("ASG1", "assignment", ("to_claim",),
|
|
37
|
+
("assignor", "assignee")),
|
|
38
|
+
"codeimprint.declaration.v1": RecordType("DCL1", "declaration", ("head",), ("project",)),
|
|
39
|
+
"codeimprint.contributor.v1": RecordType("CTB1", "contributor", (),
|
|
40
|
+
("contributor", "project")),
|
|
41
|
+
"codeimprint.notice.v1": RecordType("NTC1", "notice", ("about",)),
|
|
42
|
+
"codeimprint.disclosure.v1": RecordType("DSC1", "disclosure", ("record",), ("admin",)),
|
|
43
|
+
"codeimprint.integration.v1": RecordType("INT1", "integration", (),
|
|
44
|
+
("integration", "owner")),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def parents(rtype: RecordType, document: dict) -> list[str]:
|
|
49
|
+
out: set[str] = set()
|
|
50
|
+
for field in rtype.refs:
|
|
51
|
+
value = document.get(field)
|
|
52
|
+
for ref in value if isinstance(value, list) else [value]:
|
|
53
|
+
if isinstance(ref, str):
|
|
54
|
+
out.add(ref)
|
|
55
|
+
return sorted(out)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def sign_statement(record_type: str, digest: str) -> dict:
|
|
59
|
+
return {"type": RECORD_SIGN, "record_type": record_type, "digest": digest}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Offline verification of RFC 3161 time-stamp tokens.
|
|
2
|
+
|
|
3
|
+
A token from an independent time-stamping authority (TSA) proves that a hash existed no later
|
|
4
|
+
than the token's ``genTime``. The client only checks tokens; it never requests them.
|
|
5
|
+
|
|
6
|
+
A token counts only if all of these hold:
|
|
7
|
+
|
|
8
|
+
- it is signed by the certificate it names, over a TSTInfo for exactly this hash;
|
|
9
|
+
- that signer certificate carries the timeStamping extended key usage, marked critical, and
|
|
10
|
+
was valid at ``genTime``;
|
|
11
|
+
- the signer certificate chains, through CA certificates that were valid at ``genTime``, to
|
|
12
|
+
a pinned root (``tsa_roots``) or to a certificate the user passed with ``--tsa-certs``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from .tsa_roots import PINNED_ROOTS
|
|
23
|
+
|
|
24
|
+
MAX_CHAIN = 8
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TimestampError(Exception):
|
|
28
|
+
"""The token is malformed, for other data, untrusted, or its signature does not verify."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class TokenInfo:
|
|
33
|
+
gen_time: str # YYYY-MM-DDTHH:MM:SSZ
|
|
34
|
+
tsa: str # the signer certificate's subject
|
|
35
|
+
tsa_cert_sha256: str
|
|
36
|
+
root: str # which trusted root the chain ends at
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Anchor:
|
|
41
|
+
name: str
|
|
42
|
+
cert: object # cryptography.x509.Certificate
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def pinned_anchors() -> list[Anchor]:
|
|
46
|
+
from cryptography import x509
|
|
47
|
+
|
|
48
|
+
return [Anchor(name, x509.load_pem_x509_certificate(pem.encode()))
|
|
49
|
+
for name, pem in PINNED_ROOTS.items()]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_anchors(extra: str | Path | None = None) -> list[Anchor]:
|
|
53
|
+
"""The pinned roots, plus every certificate in a PEM bundle the user chose to trust."""
|
|
54
|
+
from cryptography import x509
|
|
55
|
+
|
|
56
|
+
anchors = pinned_anchors()
|
|
57
|
+
if extra:
|
|
58
|
+
certs = x509.load_pem_x509_certificates(Path(extra).read_bytes())
|
|
59
|
+
anchors += [Anchor(f"--tsa-certs {c.subject.rfc4514_string()}", c) for c in certs]
|
|
60
|
+
return anchors
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _issued_by(cert, issuer) -> bool:
|
|
64
|
+
from cryptography.exceptions import InvalidSignature
|
|
65
|
+
|
|
66
|
+
if cert.issuer != issuer.subject:
|
|
67
|
+
return False
|
|
68
|
+
try:
|
|
69
|
+
cert.verify_directly_issued_by(issuer)
|
|
70
|
+
except (InvalidSignature, ValueError, TypeError):
|
|
71
|
+
return False
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _valid_at(cert, when: datetime) -> bool:
|
|
76
|
+
return cert.not_valid_before_utc <= when <= cert.not_valid_after_utc
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _may_issue(cert) -> bool:
|
|
80
|
+
"""A CA certificate whose usage (if restricted) still allows time-stamping below it."""
|
|
81
|
+
from cryptography import x509
|
|
82
|
+
from cryptography.x509.oid import ExtendedKeyUsageOID
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
if not cert.extensions.get_extension_for_class(x509.BasicConstraints).value.ca:
|
|
86
|
+
return False
|
|
87
|
+
except x509.ExtensionNotFound:
|
|
88
|
+
return False
|
|
89
|
+
try:
|
|
90
|
+
eku = cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage).value
|
|
91
|
+
except x509.ExtensionNotFound:
|
|
92
|
+
return True
|
|
93
|
+
return (ExtendedKeyUsageOID.TIME_STAMPING in eku
|
|
94
|
+
or ExtendedKeyUsageOID.ANY_EXTENDED_KEY_USAGE in eku)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _chain_to_anchor(signer, carried: list, anchors: list[Anchor], when: datetime) -> str:
|
|
98
|
+
current = signer
|
|
99
|
+
for _ in range(MAX_CHAIN):
|
|
100
|
+
for anchor in anchors:
|
|
101
|
+
if _issued_by(current, anchor.cert):
|
|
102
|
+
if not _valid_at(anchor.cert, when):
|
|
103
|
+
raise TimestampError(f"root {anchor.name} was not valid at genTime")
|
|
104
|
+
return anchor.name
|
|
105
|
+
if current.issuer == current.subject:
|
|
106
|
+
break # self-signed and not trusted
|
|
107
|
+
issuer = next((c for c in carried if c is not current and _issued_by(current, c)),
|
|
108
|
+
None)
|
|
109
|
+
if issuer is None or not _may_issue(issuer):
|
|
110
|
+
break
|
|
111
|
+
if not _valid_at(issuer, when):
|
|
112
|
+
raise TimestampError(f"CA certificate {issuer.subject.rfc4514_string()} was not "
|
|
113
|
+
"valid at genTime")
|
|
114
|
+
current = issuer
|
|
115
|
+
raise TimestampError("unknown TSA: its certificate does not chain to a pinned root "
|
|
116
|
+
"(pass --tsa-certs FILE to trust it)")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def verify_token(token_der: bytes, digest: bytes,
|
|
120
|
+
anchors: list[Anchor] | None = None) -> TokenInfo:
|
|
121
|
+
"""Check that ``token_der`` is a trusted RFC 3161 token over SHA-256 ``digest``."""
|
|
122
|
+
from asn1crypto import cms, tsp # noqa: F401 — importing tsp registers TSTInfo with cms
|
|
123
|
+
from cryptography import x509
|
|
124
|
+
from cryptography.exceptions import InvalidSignature
|
|
125
|
+
from cryptography.hazmat.primitives import hashes
|
|
126
|
+
from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa
|
|
127
|
+
from cryptography.hazmat.primitives.serialization import Encoding
|
|
128
|
+
from cryptography.x509.oid import ExtendedKeyUsageOID
|
|
129
|
+
|
|
130
|
+
anchors = pinned_anchors() if anchors is None else anchors
|
|
131
|
+
try:
|
|
132
|
+
signed = cms.ContentInfo.load(token_der)["content"]
|
|
133
|
+
encap = signed["encap_content_info"]
|
|
134
|
+
if encap["content_type"].native != "tst_info":
|
|
135
|
+
raise TimestampError("not a time-stamp token")
|
|
136
|
+
tst = encap["content"].parsed
|
|
137
|
+
imprint = tst["message_imprint"]
|
|
138
|
+
if imprint["hash_algorithm"]["algorithm"].native != "sha256":
|
|
139
|
+
raise TimestampError("token does not use SHA-256")
|
|
140
|
+
if imprint["hashed_message"].native != digest:
|
|
141
|
+
raise TimestampError("token is for different data")
|
|
142
|
+
signer = signed["signer_infos"][0]
|
|
143
|
+
algo = signer["digest_algorithm"]["algorithm"].native
|
|
144
|
+
hash_cls = {"sha256": hashes.SHA256, "sha384": hashes.SHA384,
|
|
145
|
+
"sha512": hashes.SHA512}.get(algo)
|
|
146
|
+
if hash_cls is None:
|
|
147
|
+
raise TimestampError(f"unsupported digest {algo}")
|
|
148
|
+
attrs = signer["signed_attrs"]
|
|
149
|
+
message_digest = next(a["values"][0].native for a in attrs
|
|
150
|
+
if a["type"].native == "message_digest")
|
|
151
|
+
if message_digest != hashlib.new(algo, encap["content"].contents).digest():
|
|
152
|
+
raise TimestampError("token content was changed")
|
|
153
|
+
carried = [x509.load_der_x509_certificate(c.chosen.dump())
|
|
154
|
+
for c in signed["certificates"] or []]
|
|
155
|
+
sid = signer["sid"].chosen
|
|
156
|
+
cert = next((c for c, raw in zip(carried, signed["certificates"] or [])
|
|
157
|
+
if raw.chosen.issuer == sid["issuer"]
|
|
158
|
+
and raw.chosen.serial_number == sid["serial_number"].native), None)
|
|
159
|
+
if cert is None:
|
|
160
|
+
raise TimestampError("token carries no TSA certificate")
|
|
161
|
+
eku = cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
|
|
162
|
+
if ExtendedKeyUsageOID.TIME_STAMPING not in eku.value:
|
|
163
|
+
raise TimestampError("certificate is not for time-stamping")
|
|
164
|
+
if not eku.critical:
|
|
165
|
+
raise TimestampError("the time-stamping key usage is not marked critical")
|
|
166
|
+
signed_bytes = b"\x31" + attrs.dump()[1:] # signed as SET OF, not [0] IMPLICIT
|
|
167
|
+
signature = signer["signature"].native
|
|
168
|
+
key = cert.public_key()
|
|
169
|
+
if isinstance(key, rsa.RSAPublicKey):
|
|
170
|
+
key.verify(signature, signed_bytes, padding.PKCS1v15(), hash_cls())
|
|
171
|
+
elif isinstance(key, ec.EllipticCurvePublicKey):
|
|
172
|
+
key.verify(signature, signed_bytes, ec.ECDSA(hash_cls()))
|
|
173
|
+
else:
|
|
174
|
+
raise TimestampError("unsupported TSA key type")
|
|
175
|
+
gen = tst["gen_time"].native
|
|
176
|
+
except InvalidSignature as exc:
|
|
177
|
+
raise TimestampError("token signature does not match") from exc
|
|
178
|
+
except (ValueError, KeyError, TypeError, IndexError, StopIteration,
|
|
179
|
+
x509.ExtensionNotFound) as exc:
|
|
180
|
+
raise TimestampError(f"malformed token: {exc}") from exc
|
|
181
|
+
if not _valid_at(cert, gen):
|
|
182
|
+
raise TimestampError("the TSA certificate was not valid at genTime")
|
|
183
|
+
root = _chain_to_anchor(cert, carried, anchors, gen)
|
|
184
|
+
return TokenInfo(gen_time=gen.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
185
|
+
tsa=cert.subject.rfc4514_string(),
|
|
186
|
+
tsa_cert_sha256=hashlib.sha256(cert.public_bytes(Encoding.DER)).hexdigest(),
|
|
187
|
+
root=root)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Merkle proofs for the CodeImprint transparency log (RFC 6962 hashing, RFC 9162 proofs).
|
|
2
|
+
|
|
3
|
+
Leaf hash = SHA-256(0x00 || leaf bytes); node hash = SHA-256(0x01 || left || right). These
|
|
4
|
+
are the public, standard checks that let anyone confirm a seal is in the log and that the
|
|
5
|
+
log only ever grew.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def leaf_hash(data: bytes) -> bytes:
|
|
14
|
+
return hashlib.sha256(b"\x00" + data).digest()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def node_hash(left: bytes, right: bytes) -> bytes:
|
|
18
|
+
return hashlib.sha256(b"\x01" + left + right).digest()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def verify_inclusion(index: int, size: int, leaf: bytes, proof: list[bytes],
|
|
22
|
+
root: bytes) -> bool:
|
|
23
|
+
"""RFC 9162 section 2.1.3.2: is ``leaf`` at ``index`` of the tree with this root?"""
|
|
24
|
+
if not 0 <= index < size:
|
|
25
|
+
return False
|
|
26
|
+
fn, sn, r = index, size - 1, leaf
|
|
27
|
+
for p in proof:
|
|
28
|
+
if sn == 0:
|
|
29
|
+
return False
|
|
30
|
+
if fn & 1 or fn == sn:
|
|
31
|
+
r = node_hash(p, r)
|
|
32
|
+
if not fn & 1:
|
|
33
|
+
while not fn & 1 and fn != 0:
|
|
34
|
+
fn >>= 1
|
|
35
|
+
sn >>= 1
|
|
36
|
+
else:
|
|
37
|
+
r = node_hash(r, p)
|
|
38
|
+
fn >>= 1
|
|
39
|
+
sn >>= 1
|
|
40
|
+
return sn == 0 and r == root
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def verify_consistency(first: int, second: int, first_root: bytes, second_root: bytes,
|
|
44
|
+
proof: list[bytes]) -> bool:
|
|
45
|
+
"""RFC 9162 section 2.1.4.2: does the tree of ``second`` leaves extend that of ``first``?"""
|
|
46
|
+
if first == second:
|
|
47
|
+
return not proof and first_root == second_root
|
|
48
|
+
if not 0 < first < second or not proof:
|
|
49
|
+
return False
|
|
50
|
+
if first & (first - 1) == 0: # first is an exact power of two
|
|
51
|
+
proof = [first_root, *proof]
|
|
52
|
+
fn, sn = first - 1, second - 1
|
|
53
|
+
while fn & 1:
|
|
54
|
+
fn >>= 1
|
|
55
|
+
sn >>= 1
|
|
56
|
+
fr = sr = proof[0]
|
|
57
|
+
for c in proof[1:]:
|
|
58
|
+
if sn == 0:
|
|
59
|
+
return False
|
|
60
|
+
if fn & 1 or fn == sn:
|
|
61
|
+
fr = node_hash(c, fr)
|
|
62
|
+
sr = node_hash(c, sr)
|
|
63
|
+
if not fn & 1:
|
|
64
|
+
while not fn & 1 and fn != 0:
|
|
65
|
+
fn >>= 1
|
|
66
|
+
sn >>= 1
|
|
67
|
+
else:
|
|
68
|
+
sr = node_hash(sr, c)
|
|
69
|
+
fn >>= 1
|
|
70
|
+
sn >>= 1
|
|
71
|
+
return fr == first_root and sr == second_root and sn == 0
|
codeimprint_cli/trust.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Keys: the server's published attestation keys, and the developer's own signing key.
|
|
2
|
+
|
|
3
|
+
Server keys are trusted on first use and then only ever extended: a later key set must keep
|
|
4
|
+
every key already seen, unchanged (rotation adds keys and closes old windows; it never removes
|
|
5
|
+
or replaces one). The developer key is an Ed25519 key that stays in the user config folder;
|
|
6
|
+
only its public half and signatures are sent.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import base64
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from . import config
|
|
18
|
+
from .imprint_file import b64d, canonical
|
|
19
|
+
|
|
20
|
+
TRUSTED_FILE = "trusted-keys.json" # in the user config folder: {server URL: keys}
|
|
21
|
+
PROJECT_KEYS = "server-keys.json" # in .imprint/: a copy for other verifiers
|
|
22
|
+
DEVELOPER_KEY = "developer-key.pem"
|
|
23
|
+
DEVELOPER_CONTEXT = b"codeimprint.developer-signature.v1\x00"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def b64e(data: bytes) -> str:
|
|
27
|
+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def fingerprint(public_raw: bytes) -> str:
|
|
31
|
+
return hashlib.sha256(public_raw).hexdigest()[:16]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _write_private(path: Path, data: bytes) -> None:
|
|
35
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
37
|
+
with os.fdopen(fd, "wb") as f:
|
|
38
|
+
f.write(data)
|
|
39
|
+
if os.name != "nt":
|
|
40
|
+
path.chmod(0o600)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# -- signed statements ---------------------------------------------------------------------
|
|
44
|
+
def verify_signed(block: dict, keys: list[dict]) -> tuple[bool, str]:
|
|
45
|
+
"""Check a server-signed block against trusted keys, including the key's validity window.
|
|
46
|
+
|
|
47
|
+
Returns (ok, key_id) or (False, reason).
|
|
48
|
+
"""
|
|
49
|
+
from cryptography.exceptions import InvalidSignature
|
|
50
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
51
|
+
|
|
52
|
+
if not isinstance(block, dict):
|
|
53
|
+
return False, "missing"
|
|
54
|
+
key = next((k for k in keys if k.get("key_id") == block.get("key_id")), None)
|
|
55
|
+
if key is None:
|
|
56
|
+
return False, f"signed by unknown key {block.get('key_id')}"
|
|
57
|
+
statement = block.get("statement") or {}
|
|
58
|
+
at = statement.get("attested_at") or statement.get("issued_at") or ""
|
|
59
|
+
if at < key["valid_from"] or (key.get("valid_until") and at > key["valid_until"]):
|
|
60
|
+
return False, f"key {key['key_id']} was not valid at {at}"
|
|
61
|
+
try:
|
|
62
|
+
Ed25519PublicKey.from_public_bytes(b64d(key["public_key"])).verify(
|
|
63
|
+
b64d(block["signature"]), canonical(statement))
|
|
64
|
+
except (InvalidSignature, ValueError, KeyError, TypeError):
|
|
65
|
+
return False, "signature does not match"
|
|
66
|
+
return True, key["key_id"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# -- server keys ---------------------------------------------------------------------------
|
|
70
|
+
def merge_keys(known: list[dict], published: list[dict]) -> tuple[list[dict], list[str]]:
|
|
71
|
+
"""Accept a newly published key set only if it extends the known one.
|
|
72
|
+
|
|
73
|
+
Returns the merged keys and a list of problems (empty when the new set is acceptable).
|
|
74
|
+
"""
|
|
75
|
+
by_id = {k["key_id"]: k for k in published}
|
|
76
|
+
problems = []
|
|
77
|
+
for old in known:
|
|
78
|
+
new = by_id.get(old["key_id"])
|
|
79
|
+
if new is None:
|
|
80
|
+
problems.append(f"key {old['key_id']} is no longer published")
|
|
81
|
+
elif new["public_key"] != old["public_key"] or new["valid_from"] != old["valid_from"]:
|
|
82
|
+
problems.append(f"key {old['key_id']} was replaced")
|
|
83
|
+
elif old.get("valid_until") and new.get("valid_until") != old["valid_until"]:
|
|
84
|
+
problems.append(f"key {old['key_id']} had its validity window changed")
|
|
85
|
+
for k in published:
|
|
86
|
+
if k.get("alg") != "Ed25519":
|
|
87
|
+
problems.append(f"key {k.get('key_id')} uses unsupported algorithm {k.get('alg')}")
|
|
88
|
+
elif fingerprint(b64d(k["public_key"])) != k["key_id"][3:]:
|
|
89
|
+
problems.append(f"key {k['key_id']} does not match its ID")
|
|
90
|
+
merged = sorted(by_id.values(), key=lambda k: (k["valid_from"], k["key_id"]))
|
|
91
|
+
return merged, problems
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _trusted_path() -> Path:
|
|
95
|
+
return config.config_dir() / TRUSTED_FILE
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def load_trusted() -> dict[str, list[dict]]:
|
|
99
|
+
path = _trusted_path()
|
|
100
|
+
try:
|
|
101
|
+
return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else {}
|
|
102
|
+
except (OSError, ValueError):
|
|
103
|
+
return {}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def trust_server_keys(url: str, published: dict) -> tuple[list[dict], list[str]]:
|
|
107
|
+
"""Merge the server's published keys into the trusted set for ``url``."""
|
|
108
|
+
trusted = load_trusted()
|
|
109
|
+
keys, problems = merge_keys(trusted.get(url, []), published.get("keys", []))
|
|
110
|
+
if not problems:
|
|
111
|
+
trusted[url] = keys
|
|
112
|
+
path = _trusted_path()
|
|
113
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
path.write_text(json.dumps(trusted, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
115
|
+
return keys, problems
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def all_trusted_keys() -> list[dict]:
|
|
119
|
+
"""Every key trusted for any server (key IDs are hashes of the keys, so they cannot clash)."""
|
|
120
|
+
seen: dict[str, dict] = {}
|
|
121
|
+
for keys in load_trusted().values():
|
|
122
|
+
for k in keys:
|
|
123
|
+
seen.setdefault(k["key_id"], k)
|
|
124
|
+
return list(seen.values())
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def load_keys_file(path: str | Path) -> list[dict]:
|
|
128
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
129
|
+
keys = data["keys"] if isinstance(data, dict) else data
|
|
130
|
+
merged, problems = merge_keys([], keys)
|
|
131
|
+
if problems:
|
|
132
|
+
raise ValueError(f"{path}: " + "; ".join(problems))
|
|
133
|
+
return merged
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def save_project_keys(folder: Path, url: str, keys: list[dict]) -> Path:
|
|
137
|
+
path = folder / PROJECT_KEYS
|
|
138
|
+
path.write_text(json.dumps({"server": url, "keys": keys}, indent=2, sort_keys=True) + "\n",
|
|
139
|
+
encoding="utf-8")
|
|
140
|
+
return path
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# -- developer key -------------------------------------------------------------------------
|
|
144
|
+
def developer_key_path() -> Path:
|
|
145
|
+
return config.config_dir() / DEVELOPER_KEY
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def load_developer_key():
|
|
149
|
+
"""The developer's Ed25519 private key: CODEIMPRINT_DEVELOPER_KEY (PEM) or the config
|
|
150
|
+
folder. Returns None when there is none."""
|
|
151
|
+
from cryptography.hazmat.primitives import serialization
|
|
152
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
153
|
+
|
|
154
|
+
pem = os.environ.get("CODEIMPRINT_DEVELOPER_KEY", "").strip().encode()
|
|
155
|
+
if not pem and developer_key_path().is_file():
|
|
156
|
+
pem = developer_key_path().read_bytes()
|
|
157
|
+
if not pem:
|
|
158
|
+
return None
|
|
159
|
+
key = serialization.load_pem_private_key(pem, password=None)
|
|
160
|
+
if not isinstance(key, Ed25519PrivateKey):
|
|
161
|
+
raise ValueError("the developer key is not an Ed25519 key") # noqa: TRY004 (bad input)
|
|
162
|
+
return key
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def create_developer_key():
|
|
166
|
+
from cryptography.hazmat.primitives import serialization
|
|
167
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
168
|
+
|
|
169
|
+
key = Ed25519PrivateKey.generate()
|
|
170
|
+
_write_private(developer_key_path(), key.private_bytes(
|
|
171
|
+
serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8,
|
|
172
|
+
serialization.NoEncryption()))
|
|
173
|
+
return key
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def public_raw(private_key) -> bytes:
|
|
177
|
+
from cryptography.hazmat.primitives import serialization
|
|
178
|
+
|
|
179
|
+
return private_key.public_key().public_bytes(serialization.Encoding.Raw,
|
|
180
|
+
serialization.PublicFormat.Raw)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def developer_message(digest_hex: str) -> bytes:
|
|
184
|
+
return DEVELOPER_CONTEXT + bytes.fromhex(digest_hex)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def sign_digest(private_key, digest_hex: str) -> dict:
|
|
188
|
+
raw = public_raw(private_key)
|
|
189
|
+
return {"public_key": b64e(raw), "signature": b64e(private_key.sign(
|
|
190
|
+
developer_message(digest_hex)))}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Pinned root certificates of the time-stamping authorities the client trusts.
|
|
2
|
+
|
|
3
|
+
A timestamp token counts only if its signer certificate chains to one of these roots
|
|
4
|
+
(or to a certificate passed with `imprint verify --tsa-certs FILE`).
|
|
5
|
+
|
|
6
|
+
Source: the Mozilla root store, as shipped by Debian `ca-certificates` in the
|
|
7
|
+
python:3.12-slim image and by certifi; both copies are byte-identical. Each root is
|
|
8
|
+
selected by its SHA-256 fingerprint, which matches the value its owner publishes.
|
|
9
|
+
Listed in docs/how-sealing-works.md.
|
|
10
|
+
|
|
11
|
+
| TSA | Root | SHA-256 | Valid until |
|
|
12
|
+
| --- | --- | --- | --- |
|
|
13
|
+
| DigiCert | DigiCert Trusted Root G4 | 552f7bdcf1a7af9e6ce672017f4f12abf77240c78e761ac203d1d9d20ac89988 | 2038-01-15 |
|
|
14
|
+
| Sectigo | USERTrust RSA Certification Authority | e793c9b02fd8aa13e21c31228accb08119643b749c898964b1746d46c3d4cbd2 | 2038-01-18 |
|
|
15
|
+
| GlobalSign | GlobalSign Root CA - R6 | 2cabeafe37d06ca22aba7391c0033d25982952c453647349763a3ab5ad6ccf69 | 2034-12-10 |
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# DigiCert: DigiCert Trusted Root G4
|
|
19
|
+
# SHA-256 552f7bdcf1a7af9e6ce672017f4f12abf77240c78e761ac203d1d9d20ac89988
|
|
20
|
+
DIGICERT = """\
|
|
21
|
+
-----BEGIN CERTIFICATE-----
|
|
22
|
+
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
|
|
52
|
+
-----END CERTIFICATE-----
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Sectigo: USERTrust RSA Certification Authority
|
|
57
|
+
# SHA-256 e793c9b02fd8aa13e21c31228accb08119643b749c898964b1746d46c3d4cbd2
|
|
58
|
+
SECTIGO = """\
|
|
59
|
+
-----BEGIN CERTIFICATE-----
|
|
60
|
+
MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB
|
|
61
|
+
iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
|
|
62
|
+
cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
|
|
63
|
+
BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw
|
|
64
|
+
MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV
|
|
65
|
+
BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU
|
|
66
|
+
aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy
|
|
67
|
+
dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
|
|
68
|
+
AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B
|
|
69
|
+
3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY
|
|
70
|
+
tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/
|
|
71
|
+
Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2
|
|
72
|
+
VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT
|
|
73
|
+
79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6
|
|
74
|
+
c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT
|
|
75
|
+
Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l
|
|
76
|
+
c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee
|
|
77
|
+
UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE
|
|
78
|
+
Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd
|
|
79
|
+
BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G
|
|
80
|
+
A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF
|
|
81
|
+
Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO
|
|
82
|
+
VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3
|
|
83
|
+
ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs
|
|
84
|
+
8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR
|
|
85
|
+
iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze
|
|
86
|
+
Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ
|
|
87
|
+
XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/
|
|
88
|
+
qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB
|
|
89
|
+
VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB
|
|
90
|
+
L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG
|
|
91
|
+
jjxDah2nGN59PRbxYvnKkKj9
|
|
92
|
+
-----END CERTIFICATE-----
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# GlobalSign: GlobalSign Root CA - R6
|
|
97
|
+
# SHA-256 2cabeafe37d06ca22aba7391c0033d25982952c453647349763a3ab5ad6ccf69
|
|
98
|
+
GLOBALSIGN = """\
|
|
99
|
+
-----BEGIN CERTIFICATE-----
|
|
100
|
+
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
|
|
129
|
+
5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA=
|
|
130
|
+
-----END CERTIFICATE-----
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
PINNED_ROOTS = {
|
|
134
|
+
"DigiCert": DIGICERT,
|
|
135
|
+
"Sectigo": SECTIGO,
|
|
136
|
+
"GlobalSign": GLOBALSIGN,
|
|
137
|
+
}
|