sar-envelope 0.1.0__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,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)
sar_envelope/core.py ADDED
@@ -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)
sar_envelope/keys.py ADDED
@@ -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)
sar_envelope/py.typed ADDED
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,9 @@
1
+ sar_envelope/__init__.py,sha256=_2gHkh1R4Vm60Ba36UChDhPyejuQy3kX5UErfly75Uc,1388
2
+ sar_envelope/canonicalize.py,sha256=aUi7Vp730NI1tB0X2HX5GKvuWkIuRsi6inphOtuAjRc,546
3
+ sar_envelope/core.py,sha256=iDO9W3kPPMTKytr3P_ehf-1lqj4Aa8VFq8QJJkvp2Sc,2892
4
+ sar_envelope/keys.py,sha256=EbAHYm-6tiGItknLxvsU2WptmlK3ogkVMNIlBA2Qbjs,3648
5
+ sar_envelope/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ sar_envelope-0.1.0.dist-info/METADATA,sha256=TY7w02bK6Ul6U3LEf7ppojDopGq9nJjJnYb66ilKOCs,4125
7
+ sar_envelope-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
8
+ sar_envelope-0.1.0.dist-info/top_level.txt,sha256=I-c7AYJRq8qDlcsH71lMLrfqGsxMeAHPm54jCAn1TU0,13
9
+ sar_envelope-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ sar_envelope