iricity-frontier-shared 0.5.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.
- frontier_shared/__init__.py +128 -0
- frontier_shared/base58.py +47 -0
- frontier_shared/canonical.py +131 -0
- frontier_shared/conformance.py +267 -0
- frontier_shared/credential.py +306 -0
- frontier_shared/envelope.py +228 -0
- frontier_shared/hashing.py +52 -0
- frontier_shared/keys.py +235 -0
- frontier_shared/receipt.py +277 -0
- frontier_shared/vectors/v3/vectors.json +2114 -0
- frontier_shared/vectors/v3/vectors.lock.json +5 -0
- iricity_frontier_shared-0.5.0.dist-info/METADATA +118 -0
- iricity_frontier_shared-0.5.0.dist-info/RECORD +17 -0
- iricity_frontier_shared-0.5.0.dist-info/WHEEL +5 -0
- iricity_frontier_shared-0.5.0.dist-info/entry_points.txt +2 -0
- iricity_frontier_shared-0.5.0.dist-info/licenses/LICENSE +178 -0
- iricity_frontier_shared-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Frontier State Protocol primitives, Python implementation.
|
|
2
|
+
|
|
3
|
+
Byte-for-byte compatible with the JavaScript reference (`@iricity/frontier-shared`),
|
|
4
|
+
proven by the shared conformance vectors in ``frontier_shared/vectors``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .base58 import base58_decode, base58_encode, multibase_decode_base58btc, multibase_encode_base58btc
|
|
8
|
+
from .canonical import MAX_DEPTH, canonical_json, format_number
|
|
9
|
+
from .credential import (
|
|
10
|
+
AUTHORIZATION_CREDENTIAL_TYPE,
|
|
11
|
+
AUTHORIZATION_VOCAB,
|
|
12
|
+
CREDENTIAL_CONTEXT_V2,
|
|
13
|
+
CREDENTIAL_SUBJECT_FIELDS,
|
|
14
|
+
CRYPTOSUITE,
|
|
15
|
+
DATA_INTEGRITY_PROOF,
|
|
16
|
+
PROOF_PURPOSE,
|
|
17
|
+
credential_hash_data,
|
|
18
|
+
credential_subject_from_authorization,
|
|
19
|
+
issue_authorization_credential,
|
|
20
|
+
verify_authorization_credential,
|
|
21
|
+
verify_credential,
|
|
22
|
+
)
|
|
23
|
+
from .envelope import (
|
|
24
|
+
ENVELOPE_DOMAIN_TAG,
|
|
25
|
+
ENVELOPE_VERSION,
|
|
26
|
+
TIMESTAMP_PATTERN,
|
|
27
|
+
create_envelope,
|
|
28
|
+
envelope_hash,
|
|
29
|
+
envelope_header,
|
|
30
|
+
envelope_id_from_hash,
|
|
31
|
+
envelope_signing_input,
|
|
32
|
+
is_timestamp,
|
|
33
|
+
verify_envelope,
|
|
34
|
+
)
|
|
35
|
+
from .hashing import HASH_PREFIX, hash_bytes, hash_payload, hash_record, hash_text, is_hash_ref, sha256_hex, sha256_hex_bytes
|
|
36
|
+
from .keys import (
|
|
37
|
+
ED25519_MULTICODEC_PREFIX,
|
|
38
|
+
KeyPair,
|
|
39
|
+
decode_signature,
|
|
40
|
+
did_from_public_key_bytes,
|
|
41
|
+
encode_signature,
|
|
42
|
+
fingerprint_did,
|
|
43
|
+
is_did_key,
|
|
44
|
+
is_valid_signature_text,
|
|
45
|
+
public_key_bytes_from_did,
|
|
46
|
+
public_key_multibase_from_did,
|
|
47
|
+
resolve_signer,
|
|
48
|
+
sign_bytes,
|
|
49
|
+
verify_bytes,
|
|
50
|
+
)
|
|
51
|
+
from .receipt import (
|
|
52
|
+
RECEIPT_DOMAIN_TAG,
|
|
53
|
+
RECEIPT_SIGNATURE_ALGORITHMS,
|
|
54
|
+
RECEIPT_VERSION,
|
|
55
|
+
compute_receipt_hash,
|
|
56
|
+
create_receipt_record,
|
|
57
|
+
derive_receipt_id,
|
|
58
|
+
receipt_core,
|
|
59
|
+
receipt_signing_input,
|
|
60
|
+
verify_receipt_chain,
|
|
61
|
+
verify_receipt_record,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
__version__ = "0.5.0"
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"__version__",
|
|
68
|
+
"issue_authorization_credential",
|
|
69
|
+
"verify_credential",
|
|
70
|
+
"verify_authorization_credential",
|
|
71
|
+
"credential_hash_data",
|
|
72
|
+
"credential_subject_from_authorization",
|
|
73
|
+
"CREDENTIAL_CONTEXT_V2",
|
|
74
|
+
"AUTHORIZATION_VOCAB",
|
|
75
|
+
"AUTHORIZATION_CREDENTIAL_TYPE",
|
|
76
|
+
"CREDENTIAL_SUBJECT_FIELDS",
|
|
77
|
+
"CRYPTOSUITE",
|
|
78
|
+
"DATA_INTEGRITY_PROOF",
|
|
79
|
+
"PROOF_PURPOSE",
|
|
80
|
+
"canonical_json",
|
|
81
|
+
"format_number",
|
|
82
|
+
"MAX_DEPTH",
|
|
83
|
+
"hash_text",
|
|
84
|
+
"is_valid_signature_text",
|
|
85
|
+
"is_timestamp",
|
|
86
|
+
"TIMESTAMP_PATTERN",
|
|
87
|
+
"envelope_id_from_hash",
|
|
88
|
+
"derive_receipt_id",
|
|
89
|
+
"HASH_PREFIX",
|
|
90
|
+
"sha256_hex",
|
|
91
|
+
"sha256_hex_bytes",
|
|
92
|
+
"hash_payload",
|
|
93
|
+
"hash_bytes",
|
|
94
|
+
"hash_record",
|
|
95
|
+
"is_hash_ref",
|
|
96
|
+
"base58_encode",
|
|
97
|
+
"base58_decode",
|
|
98
|
+
"multibase_encode_base58btc",
|
|
99
|
+
"multibase_decode_base58btc",
|
|
100
|
+
"ED25519_MULTICODEC_PREFIX",
|
|
101
|
+
"KeyPair",
|
|
102
|
+
"resolve_signer",
|
|
103
|
+
"did_from_public_key_bytes",
|
|
104
|
+
"public_key_bytes_from_did",
|
|
105
|
+
"public_key_multibase_from_did",
|
|
106
|
+
"is_did_key",
|
|
107
|
+
"fingerprint_did",
|
|
108
|
+
"sign_bytes",
|
|
109
|
+
"verify_bytes",
|
|
110
|
+
"encode_signature",
|
|
111
|
+
"decode_signature",
|
|
112
|
+
"ENVELOPE_VERSION",
|
|
113
|
+
"ENVELOPE_DOMAIN_TAG",
|
|
114
|
+
"create_envelope",
|
|
115
|
+
"verify_envelope",
|
|
116
|
+
"envelope_header",
|
|
117
|
+
"envelope_hash",
|
|
118
|
+
"envelope_signing_input",
|
|
119
|
+
"RECEIPT_VERSION",
|
|
120
|
+
"RECEIPT_DOMAIN_TAG",
|
|
121
|
+
"RECEIPT_SIGNATURE_ALGORITHMS",
|
|
122
|
+
"create_receipt_record",
|
|
123
|
+
"verify_receipt_record",
|
|
124
|
+
"verify_receipt_chain",
|
|
125
|
+
"receipt_core",
|
|
126
|
+
"receipt_signing_input",
|
|
127
|
+
"compute_receipt_hash",
|
|
128
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Base58btc (Bitcoin alphabet), as used by multibase prefix 'z'."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = ["base58_encode", "base58_decode", "multibase_encode_base58btc", "multibase_decode_base58btc"]
|
|
6
|
+
|
|
7
|
+
ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
8
|
+
_INDEX = {char: index for index, char in enumerate(ALPHABET)}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def base58_encode(data: bytes) -> str:
|
|
12
|
+
data = bytes(data)
|
|
13
|
+
if not data:
|
|
14
|
+
return ""
|
|
15
|
+
value = int.from_bytes(data, "big")
|
|
16
|
+
encoded = ""
|
|
17
|
+
while value > 0:
|
|
18
|
+
value, remainder = divmod(value, 58)
|
|
19
|
+
encoded = ALPHABET[remainder] + encoded
|
|
20
|
+
leading = len(data) - len(data.lstrip(b"\x00"))
|
|
21
|
+
return "1" * leading + encoded
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def base58_decode(text: str) -> bytes:
|
|
25
|
+
if not isinstance(text, str):
|
|
26
|
+
raise TypeError("base58_decode expects a string")
|
|
27
|
+
if text == "":
|
|
28
|
+
return b""
|
|
29
|
+
value = 0
|
|
30
|
+
for char in text:
|
|
31
|
+
digit = _INDEX.get(char)
|
|
32
|
+
if digit is None:
|
|
33
|
+
raise ValueError(f"base58_decode: invalid character {char!r}")
|
|
34
|
+
value = value * 58 + digit
|
|
35
|
+
body = b"" if value == 0 else value.to_bytes((value.bit_length() + 7) // 8, "big")
|
|
36
|
+
leading = len(text) - len(text.lstrip("1"))
|
|
37
|
+
return b"\x00" * leading + body
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def multibase_encode_base58btc(data: bytes) -> str:
|
|
41
|
+
return "z" + base58_encode(data)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def multibase_decode_base58btc(text: str) -> bytes:
|
|
45
|
+
if not isinstance(text, str) or not text.startswith("z"):
|
|
46
|
+
raise ValueError('expected a multibase base58btc string starting with "z"')
|
|
47
|
+
return base58_decode(text[1:])
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Canonical JSON per RFC 8785 (JSON Canonicalization Scheme).
|
|
2
|
+
|
|
3
|
+
Rules implemented:
|
|
4
|
+
- object members sorted by the UTF-16 code units of their names
|
|
5
|
+
- numbers serialized with the ECMAScript Number-to-string algorithm; every number
|
|
6
|
+
is a double, so integers beyond 2**53 are rounded exactly as JavaScript rounds them
|
|
7
|
+
- strings escaped exactly as ECMAScript JSON.stringify escapes them
|
|
8
|
+
- no insignificant whitespace
|
|
9
|
+
|
|
10
|
+
Rejected, because a canonical form must be unambiguous and a verifier must not be
|
|
11
|
+
crashable: non-finite floats, non-string keys, non-JSON values, nesting deeper than
|
|
12
|
+
MAX_DEPTH.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import math
|
|
18
|
+
from decimal import Decimal
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
__all__ = ["canonical_json", "format_number", "MAX_DEPTH"]
|
|
22
|
+
|
|
23
|
+
MAX_DEPTH = 256
|
|
24
|
+
_MAX_SAFE_INTEGER = 2**53
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def format_number(value: float | int) -> str:
|
|
28
|
+
"""ECMAScript Number::toString for a finite number (ECMA-262 section 6.1.6.1.20)."""
|
|
29
|
+
if isinstance(value, bool):
|
|
30
|
+
raise TypeError("booleans are not numbers")
|
|
31
|
+
if isinstance(value, int):
|
|
32
|
+
if abs(value) < _MAX_SAFE_INTEGER:
|
|
33
|
+
return str(value)
|
|
34
|
+
# JSON has only doubles: a larger integer literal is the nearest double.
|
|
35
|
+
value = float(value)
|
|
36
|
+
if not isinstance(value, float):
|
|
37
|
+
raise TypeError(f"not a number: {type(value).__name__}")
|
|
38
|
+
if math.isnan(value) or math.isinf(value):
|
|
39
|
+
raise ValueError("canonical_json: non-finite number")
|
|
40
|
+
if value == 0.0:
|
|
41
|
+
return "0"
|
|
42
|
+
|
|
43
|
+
sign = "-" if value < 0 else ""
|
|
44
|
+
# repr() gives the shortest digit string that round-trips, the same digits
|
|
45
|
+
# ECMAScript chooses. Decimal exposes them as (digits, exponent).
|
|
46
|
+
tup = Decimal(repr(abs(value))).as_tuple()
|
|
47
|
+
digits = list(tup.digits)
|
|
48
|
+
exponent = int(tup.exponent)
|
|
49
|
+
while len(digits) > 1 and digits[-1] == 0:
|
|
50
|
+
digits.pop()
|
|
51
|
+
exponent += 1
|
|
52
|
+
k = len(digits)
|
|
53
|
+
n = k + exponent # value = 0.d1d2...dk x 10^n
|
|
54
|
+
s = "".join(str(d) for d in digits)
|
|
55
|
+
|
|
56
|
+
if k <= n <= 21:
|
|
57
|
+
body = s + "0" * (n - k)
|
|
58
|
+
elif 0 < n <= 21:
|
|
59
|
+
body = s[:n] + "." + s[n:]
|
|
60
|
+
elif -6 < n <= 0:
|
|
61
|
+
body = "0." + "0" * (-n) + s
|
|
62
|
+
else:
|
|
63
|
+
e = n - 1
|
|
64
|
+
exp = ("+" if e >= 0 else "-") + str(abs(e))
|
|
65
|
+
body = (s if k == 1 else s[0] + "." + s[1:]) + "e" + exp
|
|
66
|
+
return sign + body
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
_SHORT_ESCAPES = {
|
|
70
|
+
'"': '\\"',
|
|
71
|
+
"\\": "\\\\",
|
|
72
|
+
"\b": "\\b",
|
|
73
|
+
"\f": "\\f",
|
|
74
|
+
"\n": "\\n",
|
|
75
|
+
"\r": "\\r",
|
|
76
|
+
"\t": "\\t",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _escape_string(text: str) -> str:
|
|
81
|
+
out = ['"']
|
|
82
|
+
for ch in text:
|
|
83
|
+
esc = _SHORT_ESCAPES.get(ch)
|
|
84
|
+
if esc is not None:
|
|
85
|
+
out.append(esc)
|
|
86
|
+
continue
|
|
87
|
+
code = ord(ch)
|
|
88
|
+
if code < 0x20 or 0xD800 <= code <= 0xDFFF:
|
|
89
|
+
out.append("\\u%04x" % code)
|
|
90
|
+
else:
|
|
91
|
+
out.append(ch)
|
|
92
|
+
out.append('"')
|
|
93
|
+
return "".join(out)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _key_order(name: Any) -> bytes:
|
|
97
|
+
# Lexicographic order of UTF-16BE bytes equals UTF-16 code unit order.
|
|
98
|
+
if not isinstance(name, str):
|
|
99
|
+
raise TypeError("canonical_json: object keys must be strings")
|
|
100
|
+
return name.encode("utf-16-be", "surrogatepass")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _serialize(value: Any, path: str, depth: int) -> str:
|
|
104
|
+
if depth > MAX_DEPTH:
|
|
105
|
+
raise TypeError(f"canonical_json: nesting deeper than {MAX_DEPTH} at {path}")
|
|
106
|
+
if value is None:
|
|
107
|
+
return "null"
|
|
108
|
+
if value is True:
|
|
109
|
+
return "true"
|
|
110
|
+
if value is False:
|
|
111
|
+
return "false"
|
|
112
|
+
if isinstance(value, str):
|
|
113
|
+
return _escape_string(value)
|
|
114
|
+
if isinstance(value, (int, float)):
|
|
115
|
+
try:
|
|
116
|
+
return format_number(value)
|
|
117
|
+
except ValueError as error:
|
|
118
|
+
raise ValueError(f"{error} at {path}") from None
|
|
119
|
+
if isinstance(value, (list, tuple)):
|
|
120
|
+
return "[" + ",".join(_serialize(item, f"{path}[{index}]", depth + 1) for index, item in enumerate(value)) + "]"
|
|
121
|
+
if isinstance(value, dict):
|
|
122
|
+
members = []
|
|
123
|
+
for key in sorted(value.keys(), key=_key_order):
|
|
124
|
+
members.append(_escape_string(key) + ":" + _serialize(value[key], f"{path}.{key}", depth + 1))
|
|
125
|
+
return "{" + ",".join(members) + "}"
|
|
126
|
+
raise TypeError(f"canonical_json: unsupported value {type(value).__name__} at {path}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def canonical_json(value: Any) -> str:
|
|
130
|
+
"""Return the RFC 8785 canonical JSON text of a JSON-compatible Python value."""
|
|
131
|
+
return _serialize(value, "$", 0)
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Conformance runner: prove this implementation reproduces the published vectors byte for byte.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
frontier-shared-conformance # bundled vectors
|
|
6
|
+
frontier-shared-conformance path/to/vectors.json
|
|
7
|
+
python -m frontier_shared.conformance
|
|
8
|
+
|
|
9
|
+
An implementation conforms only when every suite passes and none is skipped.
|
|
10
|
+
Exit status 0 when conformant, 1 otherwise.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from importlib import resources
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from .base58 import base58_decode, base58_encode
|
|
22
|
+
from .canonical import canonical_json
|
|
23
|
+
from .envelope import create_envelope, verify_envelope
|
|
24
|
+
from .hashing import hash_payload
|
|
25
|
+
from .keys import KeyPair, did_from_public_key_bytes, public_key_bytes_from_did
|
|
26
|
+
from .receipt import create_receipt_record, verify_receipt_record
|
|
27
|
+
from .credential import credential_hash_data, issue_authorization_credential, verify_authorization_credential, verify_credential
|
|
28
|
+
|
|
29
|
+
__all__ = ["VECTORS_VERSION", "SUITES", "load_vectors", "bundled_vectors_path", "bundled_lock_path", "run_conformance", "format_report", "main"]
|
|
30
|
+
|
|
31
|
+
VECTORS_VERSION = "3"
|
|
32
|
+
SUITES = ["canonical_json", "hash_payload", "base58btc", "did_key", "ed25519", "envelope", "receipt", "credential", "rejections"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def bundled_vectors_path() -> Path:
|
|
36
|
+
return Path(str(resources.files("frontier_shared") / "vectors" / f"v{VECTORS_VERSION}" / "vectors.json"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def bundled_lock_path() -> Path:
|
|
40
|
+
return Path(str(resources.files("frontier_shared") / "vectors" / f"v{VECTORS_VERSION}" / "vectors.lock.json"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_vectors(path: str | Path | None = None) -> dict:
|
|
44
|
+
target = Path(path) if path else bundled_vectors_path()
|
|
45
|
+
with open(target, "r", encoding="utf-8") as handle:
|
|
46
|
+
return json.load(handle)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _signer(v: dict) -> dict:
|
|
50
|
+
return {"private_key_seed_hex": v["signer_seed_hex"]}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_conformance(vectors: dict | None = None) -> dict:
|
|
54
|
+
if vectors is None:
|
|
55
|
+
vectors = load_vectors()
|
|
56
|
+
results: list[dict] = []
|
|
57
|
+
|
|
58
|
+
def record(suite: str, case_id: str, status: str, detail: str | None = None) -> None:
|
|
59
|
+
entry = {"suite": suite, "id": case_id, "status": status}
|
|
60
|
+
if detail:
|
|
61
|
+
entry["detail"] = detail
|
|
62
|
+
results.append(entry)
|
|
63
|
+
|
|
64
|
+
def check(suite: str, case_id: str, fn) -> None:
|
|
65
|
+
try:
|
|
66
|
+
outcome = fn()
|
|
67
|
+
if outcome is True:
|
|
68
|
+
record(suite, case_id, "passed")
|
|
69
|
+
else:
|
|
70
|
+
record(suite, case_id, "failed", outcome if isinstance(outcome, str) else "mismatch")
|
|
71
|
+
except Exception as error: # noqa: BLE001 - every failure is a finding
|
|
72
|
+
record(suite, case_id, "failed", f"{type(error).__name__}: {error}")
|
|
73
|
+
|
|
74
|
+
def suite(name: str) -> list:
|
|
75
|
+
cases = vectors.get(name)
|
|
76
|
+
if not isinstance(cases, list) or not cases:
|
|
77
|
+
record(name, "*", "failed", f'vectors file has no "{name}" suite')
|
|
78
|
+
return []
|
|
79
|
+
return cases
|
|
80
|
+
|
|
81
|
+
for v in suite("canonical_json"):
|
|
82
|
+
def canonical_case(v=v):
|
|
83
|
+
actual = canonical_json(v["input"])
|
|
84
|
+
if actual != v["expected_canonical"]:
|
|
85
|
+
return f"expected {v['expected_canonical']} got {actual}"
|
|
86
|
+
digest = hash_payload(v["input"])
|
|
87
|
+
return True if digest == v["expected_hash"] else f"hash mismatch: {digest}"
|
|
88
|
+
check("canonical_json", v["id"], canonical_case)
|
|
89
|
+
|
|
90
|
+
for v in suite("hash_payload"):
|
|
91
|
+
def hash_case(v=v):
|
|
92
|
+
actual = hash_payload(v["input"])
|
|
93
|
+
return True if actual == v["expected_hash"] else f"expected {v['expected_hash']} got {actual}"
|
|
94
|
+
check("hash_payload", v["id"], hash_case)
|
|
95
|
+
|
|
96
|
+
for v in suite("base58btc"):
|
|
97
|
+
def base58_case(v=v):
|
|
98
|
+
encoded = base58_encode(bytes.fromhex(v["input_hex"]))
|
|
99
|
+
if encoded != v["expected"]:
|
|
100
|
+
return f"encode: expected {v['expected']} got {encoded}"
|
|
101
|
+
decoded = base58_decode(v["expected"]).hex()
|
|
102
|
+
return True if decoded == v["input_hex"] else f"decode: expected {v['input_hex']} got {decoded}"
|
|
103
|
+
check("base58btc", v.get("id") or v["input_hex"] or "empty", base58_case)
|
|
104
|
+
|
|
105
|
+
for v in suite("did_key"):
|
|
106
|
+
def did_case(v=v):
|
|
107
|
+
did = did_from_public_key_bytes(bytes.fromhex(v["public_key_hex"]))
|
|
108
|
+
if did != v["expected_did"]:
|
|
109
|
+
return f"encode: expected {v['expected_did']} got {did}"
|
|
110
|
+
raw = public_key_bytes_from_did(v["expected_did"]).hex()
|
|
111
|
+
return True if raw == v["public_key_hex"] else f"decode: expected {v['public_key_hex']} got {raw}"
|
|
112
|
+
check("did_key", v["expected_did"], did_case)
|
|
113
|
+
|
|
114
|
+
for v in suite("ed25519"):
|
|
115
|
+
def ed25519_case(v=v):
|
|
116
|
+
pair = KeyPair.from_seed(v["seed_hex"])
|
|
117
|
+
if pair.public_key_bytes.hex() != v["public_key_hex"]:
|
|
118
|
+
return "public key mismatch"
|
|
119
|
+
actual = pair.sign(bytes.fromhex(v["message_hex"])).hex()
|
|
120
|
+
return True if actual == v["expected_signature_hex"] else f"signature mismatch: {actual}"
|
|
121
|
+
check("ed25519", v["id"], ed25519_case)
|
|
122
|
+
|
|
123
|
+
for v in suite("envelope"):
|
|
124
|
+
def envelope_case(v=v):
|
|
125
|
+
detached = bool(v.get("detached"))
|
|
126
|
+
env = create_envelope(
|
|
127
|
+
payload=v["payload"],
|
|
128
|
+
payload_type=v["payload_type"],
|
|
129
|
+
signer=_signer(v),
|
|
130
|
+
created_at=v["created_at"],
|
|
131
|
+
previous_envelope_hash=v["previous_envelope_hash"],
|
|
132
|
+
detached=detached,
|
|
133
|
+
)
|
|
134
|
+
e = v["expected"]
|
|
135
|
+
for field in ("signer", "payload_hash", "envelope_hash", "envelope_id"):
|
|
136
|
+
if env[field] != e[field]:
|
|
137
|
+
return f"{field}: expected {e[field]} got {env[field]}"
|
|
138
|
+
if env["signature"]["value"] != e["signature_value"]:
|
|
139
|
+
return f"signature: {env['signature']['value']}"
|
|
140
|
+
if detached and "payload" in env:
|
|
141
|
+
return "detached envelope must not carry a payload"
|
|
142
|
+
verification = verify_envelope(env, payload=v["payload"]) if detached else verify_envelope(env)
|
|
143
|
+
if not verification["verified"]:
|
|
144
|
+
return "self-verification failed: " + "; ".join(verification["errors"])
|
|
145
|
+
if detached and not verify_envelope(env)["verified"]:
|
|
146
|
+
return "detached envelope must verify without its payload"
|
|
147
|
+
return True
|
|
148
|
+
check("envelope", v["id"], envelope_case)
|
|
149
|
+
|
|
150
|
+
for v in suite("receipt"):
|
|
151
|
+
def receipt_case(v=v):
|
|
152
|
+
if v.get("legacy"):
|
|
153
|
+
verification = verify_receipt_record(v["receipt"], record=v["record"])
|
|
154
|
+
if verification["verified"] != v["expected"]["verified"]:
|
|
155
|
+
return f"legacy: expected verified={v['expected']['verified']} got {verification['verified']}: " + "; ".join(verification["errors"])
|
|
156
|
+
return True
|
|
157
|
+
issuer: Any = _signer(v) if v.get("signer_seed_hex") else v["issuer"]
|
|
158
|
+
receipt = create_receipt_record(record=v["record"], previous_hash=v["previous_hash"], issuer=issuer, timestamp=v["timestamp"])
|
|
159
|
+
e = v["expected"]
|
|
160
|
+
for field in ("receipt_id", "record_hash", "previous_hash", "receipt_hash", "issuer", "signature", "signature_algorithm", "verification_status"):
|
|
161
|
+
if receipt.get(field) != e.get(field):
|
|
162
|
+
return f"{field}: expected {e.get(field)} got {receipt.get(field)}"
|
|
163
|
+
verification = verify_receipt_record(receipt, record=v["record"])
|
|
164
|
+
return True if verification["verified"] else "self-verification failed: " + "; ".join(verification["errors"])
|
|
165
|
+
check("receipt", v["id"], receipt_case)
|
|
166
|
+
|
|
167
|
+
for v in suite("credential"):
|
|
168
|
+
def credential_case(v=v):
|
|
169
|
+
if v.get("authorization_record"):
|
|
170
|
+
bound = verify_authorization_credential(v["authorization_record"], at=v["at"])
|
|
171
|
+
if bound["verified"] != v["expected"]["verified"]:
|
|
172
|
+
return f"expected verified={v['expected']['verified']}: " + "; ".join(bound["errors"])
|
|
173
|
+
if bound["issuer"] != v["expected"]["issuer"]:
|
|
174
|
+
return f"issuer: expected {v['expected']['issuer']} got {bound['issuer']}"
|
|
175
|
+
return True
|
|
176
|
+
credential = issue_authorization_credential(v["authorization"], _signer(v), created=v["created"], id=v.get("credential_id"))
|
|
177
|
+
e = v["expected"]
|
|
178
|
+
if credential["id"] != e["id"]:
|
|
179
|
+
return f"id: expected {e['id']} got {credential['id']}"
|
|
180
|
+
if credential["issuer"] != e["issuer"]:
|
|
181
|
+
return f"issuer: expected {e['issuer']} got {credential['issuer']}"
|
|
182
|
+
if credential["proof"]["verificationMethod"] != e["verification_method"]:
|
|
183
|
+
return f"verificationMethod: {credential['proof']['verificationMethod']}"
|
|
184
|
+
if credential["proof"]["proofValue"] != e["proof_value"]:
|
|
185
|
+
return f"proofValue: expected {e['proof_value']} got {credential['proof']['proofValue']}"
|
|
186
|
+
hash_data = credential_hash_data(credential, credential["proof"]).hex()
|
|
187
|
+
if hash_data != e["hash_data_hex"]:
|
|
188
|
+
return f"hash_data: expected {e['hash_data_hex']} got {hash_data}"
|
|
189
|
+
if canonical_json(credential["credentialSubject"]) != canonical_json(e["credential_subject"]):
|
|
190
|
+
return "credentialSubject differs"
|
|
191
|
+
verification = verify_credential(credential, at=v["at"])
|
|
192
|
+
return True if verification["verified"] else "self-verification failed: " + "; ".join(verification["errors"])
|
|
193
|
+
check("credential", v["id"], credential_case)
|
|
194
|
+
|
|
195
|
+
for v in suite("rejections"):
|
|
196
|
+
def rejection_case(v=v):
|
|
197
|
+
options = v.get("options") or {}
|
|
198
|
+
if v["kind"] == "credential":
|
|
199
|
+
kwargs = {}
|
|
200
|
+
if "at" in options:
|
|
201
|
+
kwargs["at"] = options["at"]
|
|
202
|
+
if "expected_issuer" in options:
|
|
203
|
+
kwargs["expected_issuer"] = options["expected_issuer"]
|
|
204
|
+
verification = verify_credential(v["object"], **kwargs)
|
|
205
|
+
return True if verification["verified"] is False else "tampered credential was accepted"
|
|
206
|
+
if v["kind"] == "authorization":
|
|
207
|
+
verification = verify_authorization_credential(v["object"], at=options.get("at"))
|
|
208
|
+
return True if verification["verified"] is False else "unbound authorization was accepted"
|
|
209
|
+
if v["kind"] == "envelope":
|
|
210
|
+
kwargs = {}
|
|
211
|
+
if "payload" in options:
|
|
212
|
+
kwargs["payload"] = options["payload"]
|
|
213
|
+
if "expected_signer" in options:
|
|
214
|
+
kwargs["expected_signer"] = options["expected_signer"]
|
|
215
|
+
if "allowed_payload_types" in options:
|
|
216
|
+
kwargs["allowed_payload_types"] = options["allowed_payload_types"]
|
|
217
|
+
verification = verify_envelope(v["object"], **kwargs)
|
|
218
|
+
return True if verification["verified"] is False else "tampered envelope was accepted"
|
|
219
|
+
if v["kind"] == "receipt":
|
|
220
|
+
kwargs = {}
|
|
221
|
+
if "record" in options:
|
|
222
|
+
kwargs["record"] = options["record"]
|
|
223
|
+
if "previous_receipt" in options:
|
|
224
|
+
kwargs["previous_receipt"] = options["previous_receipt"]
|
|
225
|
+
if "expected_issuer" in options:
|
|
226
|
+
kwargs["expected_issuer"] = options["expected_issuer"]
|
|
227
|
+
verification = verify_receipt_record(v["object"], **kwargs)
|
|
228
|
+
return True if verification["verified"] is False else "tampered receipt was accepted"
|
|
229
|
+
return f"unknown rejection kind {v['kind']}"
|
|
230
|
+
check("rejections", v["id"], rejection_case)
|
|
231
|
+
|
|
232
|
+
passed = sum(1 for r in results if r["status"] == "passed")
|
|
233
|
+
failed = sum(1 for r in results if r["status"] == "failed")
|
|
234
|
+
skipped = sum(1 for r in results if r["status"] == "skipped")
|
|
235
|
+
return {
|
|
236
|
+
"vectors_version": vectors.get("vectors_version"),
|
|
237
|
+
"conformant": failed == 0 and skipped == 0 and passed > 0,
|
|
238
|
+
"passed": passed,
|
|
239
|
+
"failed": failed,
|
|
240
|
+
"skipped": skipped,
|
|
241
|
+
"results": results,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def format_report(report: dict) -> str:
|
|
246
|
+
lines = [
|
|
247
|
+
f"conformance vectors {report['vectors_version']}: {report['passed']} passed, "
|
|
248
|
+
f"{report['failed']} failed, {report['skipped']} skipped"
|
|
249
|
+
]
|
|
250
|
+
for r in report["results"]:
|
|
251
|
+
if r["status"] != "passed":
|
|
252
|
+
detail = f": {r['detail']}" if r.get("detail") else ""
|
|
253
|
+
lines.append(f" [{r['status']}] {r['suite']}/{r['id']}{detail}")
|
|
254
|
+
lines.append("RESULT: conformant" if report["conformant"] else "RESULT: not conformant")
|
|
255
|
+
return "\n".join(lines)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def main(argv: list[str] | None = None) -> int:
|
|
259
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
260
|
+
vectors = load_vectors(args[0]) if args else load_vectors()
|
|
261
|
+
report = run_conformance(vectors)
|
|
262
|
+
print(format_report(report))
|
|
263
|
+
return 0 if report["conformant"] else 1
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
if __name__ == "__main__":
|
|
267
|
+
raise SystemExit(main())
|