sar-envelope 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.
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: sar-envelope
3
+ Version: 0.1.0
4
+ Summary: Portable SAR (Settlement Attestation Receipt) v0.1 envelope primitives: JCS canonicalization, receipt ID derivation, Ed25519 signature-input construction, and .well-known/sar-keys.json key discovery.
5
+ License: MIT
6
+ Project-URL: Homepage, https://defaultverifier.com
7
+ Project-URL: Fixtures, https://github.com/nutstrut/sar-envelope/tree/main/py/fixtures
8
+ Keywords: sar,jcs,rfc8785,canonicalization,ed25519,receipts
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Security :: Cryptography
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: jcs>=0.2.1
16
+ Requires-Dist: cryptography>=3.4
17
+
18
+ # sar-envelope (Python)
19
+
20
+ Envelope primitives for the **portable SAR v0.1 signed core** — the
21
+ six-field `task_id_hash, verdict, confidence, reason_code, ts,
22
+ verifier_kid` contract used across SAR (Settlement Attestation Receipt)
23
+ implementations, including independent implementations on non-EVM/non-
24
+ wallet rails.
25
+
26
+ This package implements exactly four things:
27
+
28
+ 1. **JCS (RFC 8785) canonicalization** — `canonicalize(value) -> bytes`.
29
+ 2. **Deterministic receipt/claim ID derivation** —
30
+ `receipt_id(core) = "sha256:" + hex(sha256(JCS(core)))`.
31
+ 3. **Ed25519 signature-input construction** — `signing_input(core)`
32
+ returns the exact 32 bytes an Ed25519 key signs (the sha256 digest of
33
+ the JCS-canonicalized core, not the raw canonical JSON bytes).
34
+ 4. **Key discovery** against a published `.well-known/sar-keys.json`
35
+ registry (JWK format) — `parse_key_registry`, `resolve_key`,
36
+ `fetch_key_registry`, including `kid` resolution and algorithm binding
37
+ (only `kty=OKP, crv=Ed25519` is recognized).
38
+
39
+ It does **not** verify signatures, run a trust registry, implement a CLI,
40
+ or encode any issuer-specific extension (e.g. a wallet-bound counterparty
41
+ binding). `counterparty` and `_ext` are never part of the six-field
42
+ signed core this package computes over.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install sar-envelope
48
+ ```
49
+
50
+ ## Example
51
+
52
+ This reproduces fixture `01_valid_portable` from the published conformance
53
+ corpus (see [Fixtures](#fixtures) below) byte-for-byte:
54
+
55
+ ```python
56
+ from sar_envelope import extract_core, receipt_id, signing_input
57
+
58
+ receipt = {
59
+ "receipt_version": "0.1",
60
+ "task_id_hash": "sha256:fixture-portable-task-0001",
61
+ "verdict": "PASS",
62
+ "confidence": 1,
63
+ "reason_code": "SPEC_MATCH",
64
+ "ts": "2026-07-23T00:00:00Z",
65
+ "verifier_kid": "fixture-portable-kid-01",
66
+ "sig_alg": "Ed25519",
67
+ }
68
+
69
+ core = extract_core(receipt)
70
+ rid = receipt_id(core)
71
+ assert rid == "sha256:be6c760480f4127a842d3321774680531caa4a9acf16684c067086f83a8d8b6f"
72
+
73
+ # The bytes an Ed25519 private key signs for this receipt:
74
+ digest = signing_input(core) # 32 bytes; receipt_id hex-encodes this same value
75
+ ```
76
+
77
+ Key discovery against the real published registry format:
78
+
79
+ ```python
80
+ from sar_envelope import fetch_key_registry, resolve_key
81
+
82
+ registry = fetch_key_registry("https://defaultverifier.com/.well-known/sar-keys.json")
83
+ entry = resolve_key(registry, "sar-prod-ed25519-01")
84
+ entry.pubkey # raw 32-byte Ed25519 public key
85
+ entry.alg # "Ed25519" — only OKP/Ed25519 registry entries are returned
86
+ ```
87
+
88
+ ## Fixtures
89
+
90
+ The test suite in this package runs against the canonical Portable SAR
91
+ v0.1 conformance corpus, bundled at `fixtures/portable-sar-fixtures.json`
92
+ and `fixtures/portable-sar-fixture-keys.json`:
93
+ https://github.com/nutstrut/sar-envelope/tree/main/py/fixtures
94
+
95
+ ## Scope and versioning
96
+
97
+ Implements **SAR v0.1**, portable six-field signed-core profile only.
98
+ Pre-1.0 (`0.1.0`): the API may change before a 1.0 release. This package
99
+ computes canonical bytes, receipt IDs, and signature input deterministically
100
+ from data you supply — it does not itself authenticate a producer's
101
+ identity, prove human authority over an action, or provide any
102
+ end-to-end verification guarantee. Signature verification and trust-registry
103
+ policy (pinning, revocation, refresh) are the caller's responsibility and
104
+ are out of scope for this package.
@@ -0,0 +1,87 @@
1
+ # sar-envelope (Python)
2
+
3
+ Envelope primitives for the **portable SAR v0.1 signed core** — the
4
+ six-field `task_id_hash, verdict, confidence, reason_code, ts,
5
+ verifier_kid` contract used across SAR (Settlement Attestation Receipt)
6
+ implementations, including independent implementations on non-EVM/non-
7
+ wallet rails.
8
+
9
+ This package implements exactly four things:
10
+
11
+ 1. **JCS (RFC 8785) canonicalization** — `canonicalize(value) -> bytes`.
12
+ 2. **Deterministic receipt/claim ID derivation** —
13
+ `receipt_id(core) = "sha256:" + hex(sha256(JCS(core)))`.
14
+ 3. **Ed25519 signature-input construction** — `signing_input(core)`
15
+ returns the exact 32 bytes an Ed25519 key signs (the sha256 digest of
16
+ the JCS-canonicalized core, not the raw canonical JSON bytes).
17
+ 4. **Key discovery** against a published `.well-known/sar-keys.json`
18
+ registry (JWK format) — `parse_key_registry`, `resolve_key`,
19
+ `fetch_key_registry`, including `kid` resolution and algorithm binding
20
+ (only `kty=OKP, crv=Ed25519` is recognized).
21
+
22
+ It does **not** verify signatures, run a trust registry, implement a CLI,
23
+ or encode any issuer-specific extension (e.g. a wallet-bound counterparty
24
+ binding). `counterparty` and `_ext` are never part of the six-field
25
+ signed core this package computes over.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install sar-envelope
31
+ ```
32
+
33
+ ## Example
34
+
35
+ This reproduces fixture `01_valid_portable` from the published conformance
36
+ corpus (see [Fixtures](#fixtures) below) byte-for-byte:
37
+
38
+ ```python
39
+ from sar_envelope import extract_core, receipt_id, signing_input
40
+
41
+ receipt = {
42
+ "receipt_version": "0.1",
43
+ "task_id_hash": "sha256:fixture-portable-task-0001",
44
+ "verdict": "PASS",
45
+ "confidence": 1,
46
+ "reason_code": "SPEC_MATCH",
47
+ "ts": "2026-07-23T00:00:00Z",
48
+ "verifier_kid": "fixture-portable-kid-01",
49
+ "sig_alg": "Ed25519",
50
+ }
51
+
52
+ core = extract_core(receipt)
53
+ rid = receipt_id(core)
54
+ assert rid == "sha256:be6c760480f4127a842d3321774680531caa4a9acf16684c067086f83a8d8b6f"
55
+
56
+ # The bytes an Ed25519 private key signs for this receipt:
57
+ digest = signing_input(core) # 32 bytes; receipt_id hex-encodes this same value
58
+ ```
59
+
60
+ Key discovery against the real published registry format:
61
+
62
+ ```python
63
+ from sar_envelope import fetch_key_registry, resolve_key
64
+
65
+ registry = fetch_key_registry("https://defaultverifier.com/.well-known/sar-keys.json")
66
+ entry = resolve_key(registry, "sar-prod-ed25519-01")
67
+ entry.pubkey # raw 32-byte Ed25519 public key
68
+ entry.alg # "Ed25519" — only OKP/Ed25519 registry entries are returned
69
+ ```
70
+
71
+ ## Fixtures
72
+
73
+ The test suite in this package runs against the canonical Portable SAR
74
+ v0.1 conformance corpus, bundled at `fixtures/portable-sar-fixtures.json`
75
+ and `fixtures/portable-sar-fixture-keys.json`:
76
+ https://github.com/nutstrut/sar-envelope/tree/main/py/fixtures
77
+
78
+ ## Scope and versioning
79
+
80
+ Implements **SAR v0.1**, portable six-field signed-core profile only.
81
+ Pre-1.0 (`0.1.0`): the API may change before a 1.0 release. This package
82
+ computes canonical bytes, receipt IDs, and signature input deterministically
83
+ from data you supply — it does not itself authenticate a producer's
84
+ identity, prove human authority over an action, or provide any
85
+ end-to-end verification guarantee. Signature verification and trust-registry
86
+ policy (pinning, revocation, refresh) are the caller's responsibility and
87
+ are out of scope for this package.
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sar-envelope"
7
+ version = "0.1.0"
8
+ description = "Portable SAR (Settlement Attestation Receipt) v0.1 envelope primitives: JCS canonicalization, receipt ID derivation, Ed25519 signature-input construction, and .well-known/sar-keys.json key discovery."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ keywords = [
13
+ "sar",
14
+ "jcs",
15
+ "rfc8785",
16
+ "canonicalization",
17
+ "ed25519",
18
+ "receipts",
19
+ ]
20
+ classifiers = [
21
+ "Development Status :: 3 - Alpha",
22
+ "Intended Audience :: Developers",
23
+ "Programming Language :: Python :: 3",
24
+ "Topic :: Security :: Cryptography",
25
+ ]
26
+ dependencies = [
27
+ "jcs>=0.2.1",
28
+ "cryptography>=3.4",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://defaultverifier.com"
33
+ Fixtures = "https://github.com/nutstrut/sar-envelope/tree/main/py/fixtures"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+
38
+ [tool.setuptools.package-data]
39
+ sar_envelope = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,52 @@
1
+ """SAR (Settlement Attestation Receipt) portable v0.1 envelope primitives.
2
+
3
+ Implements exactly four capabilities for the six-field portable signed core
4
+ (`task_id_hash, verdict, confidence, reason_code, ts, verifier_kid`):
5
+
6
+ 1. JCS (RFC 8785) canonicalization — `canonicalize`.
7
+ 2. Deterministic receipt/claim ID derivation — `receipt_id`.
8
+ 3. Ed25519 signature-input construction — `signing_input`.
9
+ 4. Key discovery against a published `.well-known/sar-keys.json` registry —
10
+ `parse_key_registry`, `resolve_key`, `fetch_key_registry`.
11
+
12
+ This package does not verify signatures, run a trust registry, or implement
13
+ any issuer-specific extension (e.g. wallet-bound counterparty binding). See
14
+ the README for exact scope.
15
+ """
16
+
17
+ from .canonicalize import canonicalize
18
+ from .core import (
19
+ CORE_FIELDS,
20
+ CoreFieldError,
21
+ core_digest,
22
+ extract_core,
23
+ receipt_id,
24
+ signing_input,
25
+ )
26
+ from .keys import (
27
+ KeyEntry,
28
+ UnknownKeyError,
29
+ UnsupportedAlgorithmError,
30
+ fetch_key_registry,
31
+ parse_key_registry,
32
+ resolve_key,
33
+ )
34
+
35
+ __version__ = "0.1.0"
36
+
37
+ __all__ = [
38
+ "canonicalize",
39
+ "CORE_FIELDS",
40
+ "CoreFieldError",
41
+ "core_digest",
42
+ "extract_core",
43
+ "receipt_id",
44
+ "signing_input",
45
+ "KeyEntry",
46
+ "UnknownKeyError",
47
+ "UnsupportedAlgorithmError",
48
+ "fetch_key_registry",
49
+ "parse_key_registry",
50
+ "resolve_key",
51
+ "__version__",
52
+ ]
@@ -0,0 +1,20 @@
1
+ """JCS (RFC 8785) canonicalization.
2
+
3
+ Thin wrapper around the `jcs` package so callers depend on one stable
4
+ entry point (`canonicalize`) regardless of which conformant JCS library
5
+ backs it.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ import jcs as _jcs
12
+
13
+
14
+ def canonicalize(value: Any) -> bytes:
15
+ """Return the RFC 8785 JSON Canonicalization Scheme encoding of `value`.
16
+
17
+ `value` must be a JSON-compatible structure (dict/list/str/int/float/
18
+ bool/None). Returns UTF-8 bytes.
19
+ """
20
+ return _jcs.canonicalize(value)
@@ -0,0 +1,81 @@
1
+ """Receipt ID derivation and Ed25519 signature-input construction.
2
+
3
+ The portable SAR v0.1 signed core is exactly six fields:
4
+
5
+ task_id_hash, verdict, confidence, reason_code, ts, verifier_kid
6
+
7
+ `receipt_id` and `signing_input` are two views of the same underlying
8
+ value: `sha256(JCS(core))`. `receipt_id` hex-encodes it with a `sha256:`
9
+ prefix; `signing_input` returns the raw 32 digest bytes that an Ed25519
10
+ private key signs directly (this profile signs the digest, not the raw
11
+ canonical bytes).
12
+
13
+ `counterparty` and any `_ext` metadata are never part of this core. That
14
+ is an issuer-specific extension of Default Settlement's wallet-bound
15
+ profile, not the portable v0.1 contract this package implements.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ from typing import Any, Mapping
21
+
22
+ from .canonicalize import canonicalize
23
+
24
+ CORE_FIELDS = (
25
+ "task_id_hash",
26
+ "verdict",
27
+ "confidence",
28
+ "reason_code",
29
+ "ts",
30
+ "verifier_kid",
31
+ )
32
+
33
+
34
+ class CoreFieldError(ValueError):
35
+ """Raised when a receipt/core mapping is missing a required field."""
36
+
37
+
38
+ def extract_core(receipt: Mapping[str, Any]) -> dict:
39
+ """Pull exactly the six signed-core fields out of a full receipt object.
40
+
41
+ Raises `CoreFieldError` if any of the six fields is missing. Ignores
42
+ any other field present on `receipt` (e.g. `receipt_id`, `sig`,
43
+ `counterparty`, `_ext`) — those are envelope or unsigned metadata,
44
+ not part of the signed core.
45
+ """
46
+ missing = [f for f in CORE_FIELDS if f not in receipt]
47
+ if missing:
48
+ raise CoreFieldError(f"receipt is missing required core field(s): {missing}")
49
+ return {k: receipt[k] for k in CORE_FIELDS}
50
+
51
+
52
+ def core_digest(core: Mapping[str, Any]) -> bytes:
53
+ """Return sha256(JCS(core)) as raw 32 bytes.
54
+
55
+ `core` must contain exactly the six `CORE_FIELDS` (extra keys are
56
+ rejected to avoid silently signing/deriving over the wrong field set —
57
+ use `extract_core` first if you have a full receipt envelope).
58
+ """
59
+ extra = set(core.keys()) - set(CORE_FIELDS)
60
+ if extra:
61
+ raise CoreFieldError(f"core contains non-core field(s): {sorted(extra)}")
62
+ missing = [f for f in CORE_FIELDS if f not in core]
63
+ if missing:
64
+ raise CoreFieldError(f"core is missing required field(s): {missing}")
65
+ canonical = canonicalize(core)
66
+ return hashlib.sha256(canonical).digest()
67
+
68
+
69
+ def receipt_id(core: Mapping[str, Any]) -> str:
70
+ """Return the deterministic receipt ID: 'sha256:' + hex(core_digest(core))."""
71
+ return "sha256:" + core_digest(core).hex()
72
+
73
+
74
+ def signing_input(core: Mapping[str, Any]) -> bytes:
75
+ """Return the exact bytes an Ed25519 key signs for this core.
76
+
77
+ Under the portable SAR v0.1 profile this is the 32-byte sha256 digest
78
+ of the JCS-canonicalized core (the same value `receipt_id` hex-encodes),
79
+ not the raw canonical JSON bytes.
80
+ """
81
+ return core_digest(core)
@@ -0,0 +1,99 @@
1
+ """Key discovery against a published `.well-known/sar-keys.json` registry.
2
+
3
+ The registry format is a JWK set: `{"keys": [{"kid", "kty", "crv", "x", ...}]}`.
4
+ This module resolves a `kid` to a usable Ed25519 public key and binds the
5
+ algorithm: only `kty == "OKP"` and `crv == "Ed25519"` are recognized. Any
6
+ other `kty`/`crv` combination is treated as an unsupported algorithm, not
7
+ silently accepted.
8
+
9
+ This module does not implement trust-registry logic (pinning, snapshotting,
10
+ revocation, or refresh policy) — it only parses and resolves. Callers that
11
+ need those properties should build them on top of `parse_key_registry`.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import base64
16
+ import json
17
+ import urllib.request
18
+ from dataclasses import dataclass
19
+ from typing import Any, Dict, Mapping
20
+
21
+
22
+ SUPPORTED_KTY = "OKP"
23
+ SUPPORTED_CRV = "Ed25519"
24
+
25
+
26
+ class UnknownKeyError(KeyError):
27
+ """Raised when a `kid` is not present in the registry."""
28
+
29
+
30
+ class UnsupportedAlgorithmError(ValueError):
31
+ """Raised when a registry entry's kty/crv is not a recognized Ed25519 binding."""
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class KeyEntry:
36
+ kid: str
37
+ pubkey: bytes # raw 32-byte Ed25519 public key
38
+ alg: str # always "Ed25519" for entries this module accepts
39
+ raw: Mapping[str, Any] # the original JWK entry, for inspection
40
+
41
+
42
+ def _b64url_decode(value: str) -> bytes:
43
+ pad = (4 - (len(value) % 4)) % 4
44
+ return base64.urlsafe_b64decode(value + ("=" * pad))
45
+
46
+
47
+ def parse_key_registry(document: Mapping[str, Any]) -> Dict[str, KeyEntry]:
48
+ """Parse a `.well-known/sar-keys.json` document into `{kid: KeyEntry}`.
49
+
50
+ Only entries with `kty == "OKP"` and `crv == "Ed25519"` are included.
51
+ Raises `UnsupportedAlgorithmError` if an entry declares a `kid` but an
52
+ unrecognized kty/crv, since silently skipping it would let a caller
53
+ believe a key doesn't exist rather than that it exists under an
54
+ unsupported algorithm binding.
55
+ """
56
+ keys = document.get("keys")
57
+ if not isinstance(keys, list):
58
+ raise ValueError("registry document must contain a 'keys' list")
59
+
60
+ result: Dict[str, KeyEntry] = {}
61
+ for entry in keys:
62
+ kid = entry.get("kid")
63
+ if not kid:
64
+ continue
65
+ kty = entry.get("kty")
66
+ crv = entry.get("crv")
67
+ if kty != SUPPORTED_KTY or crv != SUPPORTED_CRV:
68
+ raise UnsupportedAlgorithmError(
69
+ f"key {kid!r} declares kty={kty!r} crv={crv!r}; only "
70
+ f"kty={SUPPORTED_KTY!r} crv={SUPPORTED_CRV!r} is supported"
71
+ )
72
+ x = entry.get("x")
73
+ if not x:
74
+ raise ValueError(f"key {kid!r} is missing required 'x' (public key) field")
75
+ result[kid] = KeyEntry(kid=kid, pubkey=_b64url_decode(x), alg="Ed25519", raw=entry)
76
+ return result
77
+
78
+
79
+ def resolve_key(registry: Mapping[str, KeyEntry], kid: str) -> KeyEntry:
80
+ """Resolve `kid` to a `KeyEntry`, raising `UnknownKeyError` if absent."""
81
+ try:
82
+ return registry[kid]
83
+ except KeyError as exc:
84
+ raise UnknownKeyError(f"unknown key id: {kid!r}") from exc
85
+
86
+
87
+ def fetch_key_registry(
88
+ url: str = "https://defaultverifier.com/.well-known/sar-keys.json",
89
+ timeout: float = 10.0,
90
+ ) -> Dict[str, KeyEntry]:
91
+ """Fetch and parse a `.well-known/sar-keys.json` registry over HTTPS.
92
+
93
+ Convenience wrapper around `parse_key_registry` using only the
94
+ standard library. Callers who already have the document (e.g. from a
95
+ pinned snapshot) should call `parse_key_registry` directly instead.
96
+ """
97
+ with urllib.request.urlopen(url, timeout=timeout) as response:
98
+ document = json.load(response)
99
+ return parse_key_registry(document)
File without changes
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: sar-envelope
3
+ Version: 0.1.0
4
+ Summary: Portable SAR (Settlement Attestation Receipt) v0.1 envelope primitives: JCS canonicalization, receipt ID derivation, Ed25519 signature-input construction, and .well-known/sar-keys.json key discovery.
5
+ License: MIT
6
+ Project-URL: Homepage, https://defaultverifier.com
7
+ Project-URL: Fixtures, https://github.com/nutstrut/sar-envelope/tree/main/py/fixtures
8
+ Keywords: sar,jcs,rfc8785,canonicalization,ed25519,receipts
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Security :: Cryptography
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: jcs>=0.2.1
16
+ Requires-Dist: cryptography>=3.4
17
+
18
+ # sar-envelope (Python)
19
+
20
+ Envelope primitives for the **portable SAR v0.1 signed core** — the
21
+ six-field `task_id_hash, verdict, confidence, reason_code, ts,
22
+ verifier_kid` contract used across SAR (Settlement Attestation Receipt)
23
+ implementations, including independent implementations on non-EVM/non-
24
+ wallet rails.
25
+
26
+ This package implements exactly four things:
27
+
28
+ 1. **JCS (RFC 8785) canonicalization** — `canonicalize(value) -> bytes`.
29
+ 2. **Deterministic receipt/claim ID derivation** —
30
+ `receipt_id(core) = "sha256:" + hex(sha256(JCS(core)))`.
31
+ 3. **Ed25519 signature-input construction** — `signing_input(core)`
32
+ returns the exact 32 bytes an Ed25519 key signs (the sha256 digest of
33
+ the JCS-canonicalized core, not the raw canonical JSON bytes).
34
+ 4. **Key discovery** against a published `.well-known/sar-keys.json`
35
+ registry (JWK format) — `parse_key_registry`, `resolve_key`,
36
+ `fetch_key_registry`, including `kid` resolution and algorithm binding
37
+ (only `kty=OKP, crv=Ed25519` is recognized).
38
+
39
+ It does **not** verify signatures, run a trust registry, implement a CLI,
40
+ or encode any issuer-specific extension (e.g. a wallet-bound counterparty
41
+ binding). `counterparty` and `_ext` are never part of the six-field
42
+ signed core this package computes over.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install sar-envelope
48
+ ```
49
+
50
+ ## Example
51
+
52
+ This reproduces fixture `01_valid_portable` from the published conformance
53
+ corpus (see [Fixtures](#fixtures) below) byte-for-byte:
54
+
55
+ ```python
56
+ from sar_envelope import extract_core, receipt_id, signing_input
57
+
58
+ receipt = {
59
+ "receipt_version": "0.1",
60
+ "task_id_hash": "sha256:fixture-portable-task-0001",
61
+ "verdict": "PASS",
62
+ "confidence": 1,
63
+ "reason_code": "SPEC_MATCH",
64
+ "ts": "2026-07-23T00:00:00Z",
65
+ "verifier_kid": "fixture-portable-kid-01",
66
+ "sig_alg": "Ed25519",
67
+ }
68
+
69
+ core = extract_core(receipt)
70
+ rid = receipt_id(core)
71
+ assert rid == "sha256:be6c760480f4127a842d3321774680531caa4a9acf16684c067086f83a8d8b6f"
72
+
73
+ # The bytes an Ed25519 private key signs for this receipt:
74
+ digest = signing_input(core) # 32 bytes; receipt_id hex-encodes this same value
75
+ ```
76
+
77
+ Key discovery against the real published registry format:
78
+
79
+ ```python
80
+ from sar_envelope import fetch_key_registry, resolve_key
81
+
82
+ registry = fetch_key_registry("https://defaultverifier.com/.well-known/sar-keys.json")
83
+ entry = resolve_key(registry, "sar-prod-ed25519-01")
84
+ entry.pubkey # raw 32-byte Ed25519 public key
85
+ entry.alg # "Ed25519" — only OKP/Ed25519 registry entries are returned
86
+ ```
87
+
88
+ ## Fixtures
89
+
90
+ The test suite in this package runs against the canonical Portable SAR
91
+ v0.1 conformance corpus, bundled at `fixtures/portable-sar-fixtures.json`
92
+ and `fixtures/portable-sar-fixture-keys.json`:
93
+ https://github.com/nutstrut/sar-envelope/tree/main/py/fixtures
94
+
95
+ ## Scope and versioning
96
+
97
+ Implements **SAR v0.1**, portable six-field signed-core profile only.
98
+ Pre-1.0 (`0.1.0`): the API may change before a 1.0 release. This package
99
+ computes canonical bytes, receipt IDs, and signature input deterministically
100
+ from data you supply — it does not itself authenticate a producer's
101
+ identity, prove human authority over an action, or provide any
102
+ end-to-end verification guarantee. Signature verification and trust-registry
103
+ policy (pinning, revocation, refresh) are the caller's responsibility and
104
+ are out of scope for this package.
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/sar_envelope/__init__.py
4
+ src/sar_envelope/canonicalize.py
5
+ src/sar_envelope/core.py
6
+ src/sar_envelope/keys.py
7
+ src/sar_envelope/py.typed
8
+ src/sar_envelope.egg-info/PKG-INFO
9
+ src/sar_envelope.egg-info/SOURCES.txt
10
+ src/sar_envelope.egg-info/dependency_links.txt
11
+ src/sar_envelope.egg-info/requires.txt
12
+ src/sar_envelope.egg-info/top_level.txt
13
+ tests/test_fixtures.py
@@ -0,0 +1,2 @@
1
+ jcs>=0.2.1
2
+ cryptography>=3.4
@@ -0,0 +1 @@
1
+ sar_envelope
@@ -0,0 +1,111 @@
1
+ """Conformance tests: sar_envelope must reproduce the published fixture corpus.
2
+
3
+ Fixture source: default-settlement-verifier/portable/fixtures/ (the canonical
4
+ Portable SAR v0.1 conformance corpus). Copied verbatim into ../fixtures/.
5
+ """
6
+ import json
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from sar_envelope import (
12
+ CORE_FIELDS,
13
+ CoreFieldError,
14
+ UnknownKeyError,
15
+ UnsupportedAlgorithmError,
16
+ extract_core,
17
+ parse_key_registry,
18
+ receipt_id,
19
+ resolve_key,
20
+ signing_input,
21
+ )
22
+
23
+ FIXTURES_DIR = Path(__file__).resolve().parent.parent / "fixtures"
24
+
25
+ with open(FIXTURES_DIR / "portable-sar-fixtures.json") as f:
26
+ RECEIPT_FIXTURES = json.load(f)
27
+
28
+ with open(FIXTURES_DIR / "portable-sar-fixture-keys.json") as f:
29
+ FIXTURE_KEY_POLICY = json.load(f)
30
+
31
+ with open(FIXTURES_DIR / "sample-well-known-sar-keys.json") as f:
32
+ WELL_KNOWN_REGISTRY = json.load(f)
33
+
34
+ # Fixtures whose six-field core hashes to their declared receipt_id.
35
+ # The rest are deliberately-invalid verifier test cases (tampered core,
36
+ # wallet-bound envelopes with a different core, etc.) and are asserted
37
+ # to NOT match, proving this isn't a trivial pass-through.
38
+ EXPECTED_MATCH = {
39
+ "01_valid_portable": True,
40
+ "02_unsigned_counterparty": True,
41
+ "03_with_ext": True,
42
+ "04_tampered_signed_field": False,
43
+ "05_substituted_unsigned_metadata": True,
44
+ "05b_stripped_unsigned_metadata": True,
45
+ "06_unknown_key": True,
46
+ "07_spoofed_key": True,
47
+ "08_invalid_signature": True,
48
+ "09_malformed_version": True,
49
+ "09b_malformed_alg": True,
50
+ "10_valid_walletbound_native": False,
51
+ "11_walletbound_presented_as_portable": False,
52
+ "12_metadata_grafted_walletbound": False,
53
+ "13_portable_presented_as_walletbound_boundary_case": True,
54
+ "14_ambiguous_envelope": True,
55
+ }
56
+
57
+
58
+ @pytest.mark.parametrize("name", sorted(EXPECTED_MATCH))
59
+ def test_receipt_id_matches_fixture_where_expected(name):
60
+ receipt = RECEIPT_FIXTURES[name]
61
+ core = extract_core(receipt)
62
+ computed = receipt_id(core)
63
+ should_match = EXPECTED_MATCH[name]
64
+ if should_match:
65
+ assert computed == receipt["receipt_id"], name
66
+ else:
67
+ assert computed != receipt["receipt_id"], name
68
+
69
+
70
+ def test_signing_input_is_digest_bytes_matching_receipt_id():
71
+ receipt = RECEIPT_FIXTURES["01_valid_portable"]
72
+ core = extract_core(receipt)
73
+ digest = signing_input(core)
74
+ assert isinstance(digest, bytes)
75
+ assert len(digest) == 32
76
+ assert receipt["receipt_id"] == "sha256:" + digest.hex()
77
+
78
+
79
+ def test_extract_core_rejects_missing_field():
80
+ receipt = dict(RECEIPT_FIXTURES["01_valid_portable"])
81
+ del receipt["verifier_kid"]
82
+ with pytest.raises(CoreFieldError):
83
+ extract_core(receipt)
84
+
85
+
86
+ def test_extract_core_drops_counterparty_and_ext():
87
+ receipt = RECEIPT_FIXTURES["03_with_ext"]
88
+ core = extract_core(receipt)
89
+ assert set(core.keys()) == set(CORE_FIELDS)
90
+ assert "_ext" not in core
91
+ assert "counterparty" not in core
92
+
93
+
94
+ def test_key_registry_parses_well_known_format():
95
+ registry = parse_key_registry(WELL_KNOWN_REGISTRY)
96
+ assert "sar-prod-ed25519-01" in registry
97
+ entry = resolve_key(registry, "sar-prod-ed25519-01")
98
+ assert entry.alg == "Ed25519"
99
+ assert len(entry.pubkey) == 32
100
+
101
+
102
+ def test_key_registry_unknown_kid_raises():
103
+ registry = parse_key_registry(WELL_KNOWN_REGISTRY)
104
+ with pytest.raises(UnknownKeyError):
105
+ resolve_key(registry, "does-not-exist")
106
+
107
+
108
+ def test_key_registry_rejects_unsupported_algorithm():
109
+ bad_doc = {"keys": [{"kid": "bad-1", "kty": "RSA", "crv": None, "x": "abc"}]}
110
+ with pytest.raises(UnsupportedAlgorithmError):
111
+ parse_key_registry(bad_doc)