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.
@@ -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
@@ -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
+ }