coderifts-verifier 0.1.0__tar.gz
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.
- coderifts_verifier-0.1.0/LICENSE +21 -0
- coderifts_verifier-0.1.0/PKG-INFO +124 -0
- coderifts_verifier-0.1.0/README.md +112 -0
- coderifts_verifier-0.1.0/coderifts_verifier/__init__.py +62 -0
- coderifts_verifier-0.1.0/coderifts_verifier/_from_commit.py +96 -0
- coderifts_verifier-0.1.0/coderifts_verifier/_verify.py +604 -0
- coderifts_verifier-0.1.0/coderifts_verifier/arity.py +29 -0
- coderifts_verifier-0.1.0/coderifts_verifier/dsse.py +181 -0
- coderifts_verifier-0.1.0/coderifts_verifier/keys/coderifts-keys.json +1 -0
- coderifts_verifier-0.1.0/coderifts_verifier/keys/coderifts-keys.json.sha256 +1 -0
- coderifts_verifier-0.1.0/coderifts_verifier.egg-info/PKG-INFO +124 -0
- coderifts_verifier-0.1.0/coderifts_verifier.egg-info/SOURCES.txt +20 -0
- coderifts_verifier-0.1.0/coderifts_verifier.egg-info/dependency_links.txt +1 -0
- coderifts_verifier-0.1.0/coderifts_verifier.egg-info/requires.txt +1 -0
- coderifts_verifier-0.1.0/coderifts_verifier.egg-info/top_level.txt +1 -0
- coderifts_verifier-0.1.0/pyproject.toml +36 -0
- coderifts_verifier-0.1.0/setup.cfg +4 -0
- coderifts_verifier-0.1.0/tests/test_envelope_step.py +160 -0
- coderifts_verifier-0.1.0/tests/test_from_commit.py +159 -0
- coderifts_verifier-0.1.0/tests/test_registry_unreachable.py +155 -0
- coderifts_verifier-0.1.0/tests/test_vendored_keys.py +111 -0
- coderifts_verifier-0.1.0/tests/test_verifier.py +256 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CodeRifts
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: coderifts-verifier
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Offline verifier for CodeRifts chain receipts and DSSE/in-toto envelopes. Ed25519, no network.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/coderifts/receipt-verifier
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: cryptography>=41
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# coderifts-verifier (Python)
|
|
14
|
+
|
|
15
|
+
Verify a CodeRifts chain receipt — or a DSSE / in-toto envelope carrying one —
|
|
16
|
+
offline, in Python, without calling CodeRifts.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from coderifts_verifier import unwrap, verify_receipt, keyring_from_document
|
|
20
|
+
|
|
21
|
+
keyring = keyring_from_document(registry_json, "my-registry.json")
|
|
22
|
+
token = unwrap(whatever_arrived) # compact token, or DSSE -> compact
|
|
23
|
+
result = verify_receipt(token, {"keyring": keyring})
|
|
24
|
+
|
|
25
|
+
if result["valid"]:
|
|
26
|
+
...
|
|
27
|
+
else:
|
|
28
|
+
handle(result["status"]) # INVALID_SIGNATURE, UNKNOWN_KEY, ...
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Ed25519 via `cryptography`. No network on the verify path.
|
|
32
|
+
|
|
33
|
+
## This is not the CodeRifts SDK
|
|
34
|
+
|
|
35
|
+
Two different packages, on purpose:
|
|
36
|
+
|
|
37
|
+
| | `coderifts` (SDK) | `coderifts-verifier` (this) |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| does | calls the API to get a decision | verifies evidence you already hold |
|
|
40
|
+
| crypto | none — deliberately | Ed25519 |
|
|
41
|
+
| network | yes, that is the point | none on the verify path |
|
|
42
|
+
|
|
43
|
+
A dependency on `cryptography` is correct here: this package checks signatures.
|
|
44
|
+
The SDK stays crypto-free.
|
|
45
|
+
|
|
46
|
+
## Unwrapping is not verification
|
|
47
|
+
|
|
48
|
+
`unwrap` and `from_dsse` check **no signature**. An envelope wrapping a receipt
|
|
49
|
+
with a bad signature unwraps cleanly and then fails `verify_receipt`, exactly as
|
|
50
|
+
that compact token would have — the signature is over the compact bytes.
|
|
51
|
+
|
|
52
|
+
The DSSE envelope proves **exactly what the compact receipt proves**. A standard
|
|
53
|
+
container does not strengthen a claim.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
token = from_dsse(envelope) # succeeds — nothing checked yet
|
|
57
|
+
verify_receipt(token, {"keyring": keyring}) # this is the verdict
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Cross-language agreement
|
|
61
|
+
|
|
62
|
+
The signed-bytes reconstruction is byte-identical to the JavaScript verifier —
|
|
63
|
+
`verify.js` says so in its own source, and `tests/xlang-vectors.json` holds
|
|
64
|
+
receipts produced and verified by that implementation. The test suite asserts
|
|
65
|
+
Python reaches the **same verdict on the same bytes**, for a passing receipt and
|
|
66
|
+
for two distinct failure modes.
|
|
67
|
+
|
|
68
|
+
Two implementations disagreeing about whether a receipt is valid is the failure
|
|
69
|
+
a multi-language verifier exists to avoid, so the agreement is pinned rather than
|
|
70
|
+
assumed.
|
|
71
|
+
|
|
72
|
+
The verification core (`coderifts_verifier/_verify.py`) is **vendored** from the
|
|
73
|
+
reference verifier, not reimplemented. The only edit is the import path for the
|
|
74
|
+
vendored `arity` helper; the file is otherwise byte-identical, so there is one
|
|
75
|
+
definition of what a valid receipt is rather than two that can drift.
|
|
76
|
+
|
|
77
|
+
## Refusals are named
|
|
78
|
+
|
|
79
|
+
`from_dsse` raises `DsseError` with a `code`:
|
|
80
|
+
|
|
81
|
+
| code | when |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `MALFORMED` | not an envelope; unreadable payload; zero, two, or signature-less entries; missing `encoded_payload` |
|
|
84
|
+
| `UNSUPPORTED` | a `payloadType`, `predicateType`, or compact `form` this version does not implement |
|
|
85
|
+
| `PREDICATE_MISMATCH` | the decoded `predicate.fields` disagree with the preserved payload segment |
|
|
86
|
+
|
|
87
|
+
That last one matters: the predicate carries the payload twice, once as bytes and
|
|
88
|
+
once decoded for a reader. If they disagree, the readable half describes
|
|
89
|
+
something the signed half does not contain — refuse rather than hand back bytes
|
|
90
|
+
that do not match the envelope someone read.
|
|
91
|
+
|
|
92
|
+
## What a valid verdict proves
|
|
93
|
+
|
|
94
|
+
* a holder of a key in **the keyring you supplied** signed this receipt;
|
|
95
|
+
* the receipt's signed fields are unmodified;
|
|
96
|
+
* per `status`: whether it is current, or was issued while a now-retired key was
|
|
97
|
+
still valid.
|
|
98
|
+
|
|
99
|
+
## What it does NOT prove
|
|
100
|
+
|
|
101
|
+
* **That anything happened.** A receipt is a signed statement about a decision,
|
|
102
|
+
not an observation of an effect.
|
|
103
|
+
* **That the key is uncompromised.** Everything reduces to "a holder of this key
|
|
104
|
+
signed this". Custody is yours.
|
|
105
|
+
* **That the receipt is the right one for what you are about to do.** Scope
|
|
106
|
+
matching — does this receipt cover THIS operation on THIS target — is the
|
|
107
|
+
caller's check, against the decision envelope. This verifies the receipt.
|
|
108
|
+
* **That a human saw anything.**
|
|
109
|
+
|
|
110
|
+
## Format
|
|
111
|
+
|
|
112
|
+
`RECEIPT_FORMAT.md` in the reference verifier is the specification; section 9
|
|
113
|
+
covers the DSSE / in-toto export. The constants here are byte-exact to it and
|
|
114
|
+
pinned by a test.
|
|
115
|
+
|
|
116
|
+
## Tests
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
python3 -m pytest tests/ -q
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# coderifts-verifier (Python)
|
|
2
|
+
|
|
3
|
+
Verify a CodeRifts chain receipt — or a DSSE / in-toto envelope carrying one —
|
|
4
|
+
offline, in Python, without calling CodeRifts.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from coderifts_verifier import unwrap, verify_receipt, keyring_from_document
|
|
8
|
+
|
|
9
|
+
keyring = keyring_from_document(registry_json, "my-registry.json")
|
|
10
|
+
token = unwrap(whatever_arrived) # compact token, or DSSE -> compact
|
|
11
|
+
result = verify_receipt(token, {"keyring": keyring})
|
|
12
|
+
|
|
13
|
+
if result["valid"]:
|
|
14
|
+
...
|
|
15
|
+
else:
|
|
16
|
+
handle(result["status"]) # INVALID_SIGNATURE, UNKNOWN_KEY, ...
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Ed25519 via `cryptography`. No network on the verify path.
|
|
20
|
+
|
|
21
|
+
## This is not the CodeRifts SDK
|
|
22
|
+
|
|
23
|
+
Two different packages, on purpose:
|
|
24
|
+
|
|
25
|
+
| | `coderifts` (SDK) | `coderifts-verifier` (this) |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| does | calls the API to get a decision | verifies evidence you already hold |
|
|
28
|
+
| crypto | none — deliberately | Ed25519 |
|
|
29
|
+
| network | yes, that is the point | none on the verify path |
|
|
30
|
+
|
|
31
|
+
A dependency on `cryptography` is correct here: this package checks signatures.
|
|
32
|
+
The SDK stays crypto-free.
|
|
33
|
+
|
|
34
|
+
## Unwrapping is not verification
|
|
35
|
+
|
|
36
|
+
`unwrap` and `from_dsse` check **no signature**. An envelope wrapping a receipt
|
|
37
|
+
with a bad signature unwraps cleanly and then fails `verify_receipt`, exactly as
|
|
38
|
+
that compact token would have — the signature is over the compact bytes.
|
|
39
|
+
|
|
40
|
+
The DSSE envelope proves **exactly what the compact receipt proves**. A standard
|
|
41
|
+
container does not strengthen a claim.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
token = from_dsse(envelope) # succeeds — nothing checked yet
|
|
45
|
+
verify_receipt(token, {"keyring": keyring}) # this is the verdict
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Cross-language agreement
|
|
49
|
+
|
|
50
|
+
The signed-bytes reconstruction is byte-identical to the JavaScript verifier —
|
|
51
|
+
`verify.js` says so in its own source, and `tests/xlang-vectors.json` holds
|
|
52
|
+
receipts produced and verified by that implementation. The test suite asserts
|
|
53
|
+
Python reaches the **same verdict on the same bytes**, for a passing receipt and
|
|
54
|
+
for two distinct failure modes.
|
|
55
|
+
|
|
56
|
+
Two implementations disagreeing about whether a receipt is valid is the failure
|
|
57
|
+
a multi-language verifier exists to avoid, so the agreement is pinned rather than
|
|
58
|
+
assumed.
|
|
59
|
+
|
|
60
|
+
The verification core (`coderifts_verifier/_verify.py`) is **vendored** from the
|
|
61
|
+
reference verifier, not reimplemented. The only edit is the import path for the
|
|
62
|
+
vendored `arity` helper; the file is otherwise byte-identical, so there is one
|
|
63
|
+
definition of what a valid receipt is rather than two that can drift.
|
|
64
|
+
|
|
65
|
+
## Refusals are named
|
|
66
|
+
|
|
67
|
+
`from_dsse` raises `DsseError` with a `code`:
|
|
68
|
+
|
|
69
|
+
| code | when |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `MALFORMED` | not an envelope; unreadable payload; zero, two, or signature-less entries; missing `encoded_payload` |
|
|
72
|
+
| `UNSUPPORTED` | a `payloadType`, `predicateType`, or compact `form` this version does not implement |
|
|
73
|
+
| `PREDICATE_MISMATCH` | the decoded `predicate.fields` disagree with the preserved payload segment |
|
|
74
|
+
|
|
75
|
+
That last one matters: the predicate carries the payload twice, once as bytes and
|
|
76
|
+
once decoded for a reader. If they disagree, the readable half describes
|
|
77
|
+
something the signed half does not contain — refuse rather than hand back bytes
|
|
78
|
+
that do not match the envelope someone read.
|
|
79
|
+
|
|
80
|
+
## What a valid verdict proves
|
|
81
|
+
|
|
82
|
+
* a holder of a key in **the keyring you supplied** signed this receipt;
|
|
83
|
+
* the receipt's signed fields are unmodified;
|
|
84
|
+
* per `status`: whether it is current, or was issued while a now-retired key was
|
|
85
|
+
still valid.
|
|
86
|
+
|
|
87
|
+
## What it does NOT prove
|
|
88
|
+
|
|
89
|
+
* **That anything happened.** A receipt is a signed statement about a decision,
|
|
90
|
+
not an observation of an effect.
|
|
91
|
+
* **That the key is uncompromised.** Everything reduces to "a holder of this key
|
|
92
|
+
signed this". Custody is yours.
|
|
93
|
+
* **That the receipt is the right one for what you are about to do.** Scope
|
|
94
|
+
matching — does this receipt cover THIS operation on THIS target — is the
|
|
95
|
+
caller's check, against the decision envelope. This verifies the receipt.
|
|
96
|
+
* **That a human saw anything.**
|
|
97
|
+
|
|
98
|
+
## Format
|
|
99
|
+
|
|
100
|
+
`RECEIPT_FORMAT.md` in the reference verifier is the specification; section 9
|
|
101
|
+
covers the DSSE / in-toto export. The constants here are byte-exact to it and
|
|
102
|
+
pinned by a test.
|
|
103
|
+
|
|
104
|
+
## Tests
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
python3 -m pytest tests/ -q
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""CodeRifts offline verifier for Python.
|
|
2
|
+
|
|
3
|
+
Verify a CodeRifts chain receipt, or a DSSE / in-toto envelope carrying one,
|
|
4
|
+
without calling CodeRifts. Offline Ed25519 against a keyring you pin.
|
|
5
|
+
|
|
6
|
+
from coderifts_verifier import unwrap, verify_receipt
|
|
7
|
+
|
|
8
|
+
token = unwrap(whatever_arrived) # compact token, or DSSE -> compact
|
|
9
|
+
result = verify_receipt(token, {"keyring": my_keyring})
|
|
10
|
+
if result["valid"]:
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
UNWRAPPING IS NOT VERIFICATION. ``unwrap`` / ``from_dsse`` check no signature: a
|
|
14
|
+
DSSE envelope wrapping a bad receipt unwraps cleanly and then fails
|
|
15
|
+
``verify_receipt``, exactly as that compact token would have. The DSSE proves
|
|
16
|
+
exactly what the compact receipt proves -- a standard container does not
|
|
17
|
+
strengthen a claim.
|
|
18
|
+
|
|
19
|
+
The verification core (``_verify.py``) is VENDORED from the reference JS/Python
|
|
20
|
+
verifier rather than reimplemented, so there is one definition of what a valid
|
|
21
|
+
receipt is. Its signed-bytes reconstruction is byte-identical to ``verify.js``;
|
|
22
|
+
the cross-language tests pin that against shared vectors.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from ._verify import ( # noqa: F401
|
|
26
|
+
canonical_json,
|
|
27
|
+
derive_status,
|
|
28
|
+
keyring_from_document,
|
|
29
|
+
reconstruct_signed_input,
|
|
30
|
+
verify_chain,
|
|
31
|
+
verify_receipt,
|
|
32
|
+
)
|
|
33
|
+
from .dsse import ( # noqa: F401
|
|
34
|
+
FORM_ATTESTATION,
|
|
35
|
+
FORM_RECEIPT,
|
|
36
|
+
PAYLOAD_TYPE,
|
|
37
|
+
PREDICATE_TYPE,
|
|
38
|
+
STATEMENT_TYPE,
|
|
39
|
+
DsseError,
|
|
40
|
+
from_dsse,
|
|
41
|
+
looks_like_dsse,
|
|
42
|
+
unwrap,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"verify_receipt",
|
|
47
|
+
"verify_chain",
|
|
48
|
+
"from_dsse",
|
|
49
|
+
"unwrap",
|
|
50
|
+
"looks_like_dsse",
|
|
51
|
+
"DsseError",
|
|
52
|
+
"keyring_from_document",
|
|
53
|
+
"canonical_json",
|
|
54
|
+
"derive_status",
|
|
55
|
+
"reconstruct_signed_input",
|
|
56
|
+
"PAYLOAD_TYPE",
|
|
57
|
+
"STATEMENT_TYPE",
|
|
58
|
+
"PREDICATE_TYPE",
|
|
59
|
+
"FORM_RECEIPT",
|
|
60
|
+
"FORM_ATTESTATION",
|
|
61
|
+
]
|
|
62
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""1961 TAG 1 -- finding the receipt that belongs to a commit (Python side).
|
|
2
|
+
|
|
3
|
+
Byte-for-byte the same convention as ``receipt-verifier/receipt-from-commit.js``: the
|
|
4
|
+
``CodeRifts-Receipt`` git trailer, or ``.coderifts/receipts/<full-sha>.json``. See
|
|
5
|
+
``receipt-verifier/docs/receipt-commit-binding.md`` for why both exist and why the trailer wins.
|
|
6
|
+
|
|
7
|
+
WARNING -- THE CARRIER PROVES NOTHING. Whichever one delivers the token, the token is then
|
|
8
|
+
verified exactly as if it had been pasted on the command line. A forged sidecar yields
|
|
9
|
+
INVALID_SIGNATURE, not a false pass. The sidecar is a POINTER; the signature is the authority.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import pathlib
|
|
15
|
+
import subprocess
|
|
16
|
+
|
|
17
|
+
TRAILER_KEY = "CodeRifts-Receipt"
|
|
18
|
+
SIDECAR_DIR = os.path.join(".coderifts", "receipts")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _git(args, cwd):
|
|
22
|
+
return subprocess.run(
|
|
23
|
+
["git", *args], cwd=cwd, capture_output=True, text=True, check=True,
|
|
24
|
+
).stdout.strip()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def resolve_sha(ref, cwd):
|
|
28
|
+
"""Abbreviations collide, and a verifier resolving one itself would be guessing which commit
|
|
29
|
+
the operator meant. Git knows; ask it."""
|
|
30
|
+
return _git(["rev-parse", ref + "^{commit}"], cwd)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def receipt_from_trailer(sha, cwd):
|
|
34
|
+
raw = _git(["log", "-1", "--format=%%(trailers:key=%s,valueonly)" % TRAILER_KEY, sha], cwd)
|
|
35
|
+
if not raw:
|
|
36
|
+
return None
|
|
37
|
+
# Unfold: git wraps long trailer values, and a receipt token is long. A token has no
|
|
38
|
+
# whitespace in it, so joining the trimmed lines is lossless.
|
|
39
|
+
token = "".join(line.strip() for line in raw.splitlines())
|
|
40
|
+
return token or None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def receipt_from_sidecar(sha, cwd):
|
|
44
|
+
"""A MALFORMED SIDECAR IS NOT "NO SIDECAR".
|
|
45
|
+
|
|
46
|
+
Treating it as absent would let a corrupted pointer fall through to the trailer, or to
|
|
47
|
+
"nothing found" -- both of which read as an innocent absence rather than the broken file it is.
|
|
48
|
+
"""
|
|
49
|
+
path = pathlib.Path(cwd) / SIDECAR_DIR / (sha + ".json")
|
|
50
|
+
if not path.exists():
|
|
51
|
+
return None
|
|
52
|
+
try:
|
|
53
|
+
doc = json.loads(path.read_text())
|
|
54
|
+
except Exception as e:
|
|
55
|
+
raise ValueError("sidecar %s is not valid JSON: %s" % (path, e))
|
|
56
|
+
token = (doc or {}).get("receipt") or (doc or {}).get("token")
|
|
57
|
+
if not token:
|
|
58
|
+
raise ValueError('sidecar %s has no "receipt" field' % path)
|
|
59
|
+
return {"token": str(token), "envelope": (doc or {}).get("envelope"), "file": str(path)}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def receipt_for_commit(ref, cwd=None):
|
|
63
|
+
"""Returns {sha, token, envelope, carrier}. Raises on anything that is not a clean find.
|
|
64
|
+
|
|
65
|
+
WARNING -- TRAILER WINS, BUT A DISAGREEMENT IS REFUSED RATHER THAN RESOLVED. The trailer is
|
|
66
|
+
inside the commit object and the sidecar is a file anyone can edit, so where both exist the
|
|
67
|
+
immutable one is authoritative. When they DISAGREE, silently preferring the trailer would hide
|
|
68
|
+
precisely the tampering the precedence rule exists to make visible.
|
|
69
|
+
"""
|
|
70
|
+
cwd = cwd or os.getcwd()
|
|
71
|
+
try:
|
|
72
|
+
sha = resolve_sha(ref, cwd)
|
|
73
|
+
except subprocess.CalledProcessError as e:
|
|
74
|
+
raise ValueError("cannot resolve %s in %s: %s" % (ref, cwd, (e.stderr or "").splitlines()[:1]))
|
|
75
|
+
|
|
76
|
+
trailer = receipt_from_trailer(sha, cwd)
|
|
77
|
+
sidecar = receipt_from_sidecar(sha, cwd)
|
|
78
|
+
|
|
79
|
+
if trailer and sidecar:
|
|
80
|
+
if trailer != sidecar["token"]:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
"receipt conflict for %s: the commit trailer and %s carry DIFFERENT receipts. "
|
|
83
|
+
"The trailer is covered by the commit SHA and the sidecar is not, so they should "
|
|
84
|
+
"never disagree -- resolve it deliberately rather than letting one win silently."
|
|
85
|
+
% (sha, sidecar["file"])
|
|
86
|
+
)
|
|
87
|
+
return {"sha": sha, "token": trailer, "envelope": sidecar["envelope"], "carrier": "both"}
|
|
88
|
+
if trailer:
|
|
89
|
+
return {"sha": sha, "token": trailer, "envelope": None, "carrier": "trailer"}
|
|
90
|
+
if sidecar:
|
|
91
|
+
return {"sha": sha, "token": sidecar["token"], "envelope": sidecar["envelope"], "carrier": "sidecar"}
|
|
92
|
+
|
|
93
|
+
raise ValueError(
|
|
94
|
+
"no receipt attached to %s: no %s trailer and no %s/%s.json. That means no receipt was "
|
|
95
|
+
"ATTACHED -- it does not mean none exists." % (sha, TRAILER_KEY, SIDECAR_DIR, sha)
|
|
96
|
+
)
|