"""BringPass 0.3 experimental message reference. NOT a production identity system. Transport-free. Requires cryptography. All authority/pinning ports are local trusted inputs, never fields accepted from a claimant. Memory stores are for testing only. """ from __future__ import annotations from dataclasses import dataclass, field import hashlib import secrets import struct import time from typing import Callable from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat VERSION = "0.3" SUITE = "Ed25519" OPS = frozenset({"REGISTER", "USE", "REVOKE", "RETRACT"}) DOMAIN = b"BringPass/reference/0.3\x00" class Rejected(ValueError): pass def require(ok: bool, why: str) -> None: if not ok: raise Rejected(why) def encode(fields: list[str]) -> bytes: """Canonical signed bytes, NOT an NFC/APDU or network framing specification.""" require(isinstance(fields, list) and 1 <= len(fields) <= 32, "field count") out = bytearray(DOMAIN + struct.pack(">H", len(fields))) for value in fields: require(isinstance(value, str), "all fields must be strings") b = value.encode("utf-8", errors="strict") require(len(b) <= 4096, "field too large") out += struct.pack(">I", len(b)) + b require(len(out) <= 16384, "message too large") return bytes(out) def raw_public(key: Ed25519PrivateKey) -> str: return key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw).hex() def hex_value(value: str, size: int) -> bool: return isinstance(value, str) and len(value) == 2*size and all(c in "0123456789abcdef" for c in value) @dataclass(frozen=True) class Message: fields: tuple[str, ...] signature: str def as_dict(self) -> dict: return {"fields": list(self.fields), "signature": self.signature} @classmethod def parse(cls, obj: dict) -> "Message": require(isinstance(obj, dict) and set(obj) == {"fields", "signature"}, "envelope") encode(obj["fields"]) require(hex_value(obj["signature"], 64), "signature encoding") return cls(tuple(obj["fields"]), obj["signature"]) def sign(key: Ed25519PrivateKey, fields: list[str]) -> Message: return Message(tuple(fields), key.sign(encode(fields)).hex()) def verify(public: str, message: Message) -> None: require(hex_value(public, 32) and hex_value(message.signature, 64), "key/signature encoding") try: Ed25519PublicKey.from_public_bytes(bytes.fromhex(public)).verify( bytes.fromhex(message.signature), encode(list(message.fields))) except (InvalidSignature, ValueError) as exc: raise Rejected("signature") from exc def digest(message: Message) -> str: return hashlib.sha256(encode(list(message.fields)) + bytes.fromhex(message.signature)).hexdigest() def header(message: Message, kind: str, length: int) -> tuple[str, ...]: f = message.fields require(len(f) == length and f[:3] == (kind, VERSION, SUITE), "version, suite or kind") require(f[3] in OPS and hex_value(f[4], 16), "operation/transaction") return f @dataclass(frozen=True) class Authority: """Supplied by the provider's existing trusted desk/session/recovery adapter. DO NOT construct from client JSON. A real adapter must also enforce freshness, account ownership, step-up, CSRF/origin and binding to the actual interaction. USE may be public; management operations require independent authority. """ account: str operations: frozenset[str] context: str @dataclass class Relation: account: str state: str = "active" revision: int = 0 entitled: bool = True keys: dict[str, tuple[str, str]] = field(default_factory=dict) class Provider: def __init__(self, provider: str, key: Ed25519PrivateKey | None = None, clock: Callable[[], int] = lambda: int(time.time())): self.provider, self.key, self.clock = provider, key or Ed25519PrivateKey.generate(), clock self.relations: dict[str, Relation] = {} self.pending: dict[str, tuple[Message, Authority]] = {} self.completed: dict[str, tuple[str, str, Message]] = {} def challenge(self, op: str, rid: str, target: str, authority: Authority) -> Message: require(op in OPS and op in authority.operations, "management authority required") require(bool(authority.context) and (bool(authority.account) or op == "USE"), "trusted context / management account required") require(hex_value(rid, 16) or (op == "USE" and rid == ""), "relationship") require((op in {"REGISTER", "USE"} and target == "") or (op == "REVOKE" and hex_value(target, 16)) or (op == "RETRACT" and target == "*"), "target") tx = secrets.token_hex(16) q = sign(self.key, ["CHALLENGE", VERSION, SUITE, op, tx, self.provider, rid, target, secrets.token_hex(32), str(self.clock()+120), authority.context]) self.pending[tx] = (q, authority) return q def finish(self, proof: Message, trusted_context: str) -> tuple[Message, bool]: """Return (signed receipt, newly_committed). Never actuate twice on retry. Production: lock/recheck membership + consume challenge + commit state + persist receipt in ONE durable transaction. This sample is single-threaded. """ f = header(proof, "PROOF", 11) _, _, _, op, tx, provider, rid, cid, target, qhash, public = f require(provider == self.provider and hex_value(rid, 16) and hex_value(cid, 16), "scope") phash = digest(proof) if tx in self.completed: oldhash, context, receipt = self.completed[tx] require(oldhash == phash and context == trusted_context, "retry differs") return receipt, False require(tx in self.pending, "unknown transaction") q, authority = self.pending[tx] c = q.fields require(c[3] == op and c[5] == provider and c[7] == target and digest(q) == qhash, "challenge binding") require((c[6] == rid or (op == "USE" and c[6] == "")), "relationship binding") require(c[10] == trusted_context == authority.context, "session/desk binding") require(self.clock() <= int(c[9]) and op in authority.operations, "expired/unauthorised") relation = self.relations.get(rid) if relation is not None: require(relation.state == "active" and ((op == "USE" and not authority.account) or relation.account == authority.account), "relationship disabled or wrong account") if op == "REGISTER": require(hex_value(public, 32), "new key required") require(relation is None or cid not in relation.keys, "credential ID already used") verify(public, proof) # proof of the new key, NOT proof of account ownership if relation is None: relation = Relation(authority.account) self.relations[rid] = relation relation.keys[cid] = (public, "active") relation.revision += 1 state = "registered" else: require(public == "" and relation is not None, "do not supply a replacement verification key") require(cid in relation.keys and relation.keys[cid][1] == "active", "actor revoked/unknown") verify(relation.keys[cid][0], proof) if op == "USE": require(relation.entitled, "not currently entitled") state = "accepted" elif op == "REVOKE": require(target in relation.keys, "unknown revocation target") relation.keys[target] = (relation.keys[target][0], "revoked") relation.revision += 1 state = "revoked" else: relation.state = "retracted" relation.keys = {k: (v[0], "revoked") for k,v in relation.keys.items()} relation.revision += 1 state = "retracted" receipt = sign(self.key, ["RESULT", VERSION, SUITE, op, tx, provider, rid, cid, target, phash, "OK", state, str(relation.revision), "online-store"]) self.completed[tx] = (phash, trusted_context, receipt) del self.pending[tx] return receipt, True def revoke_via_recovery(self, rid: str, target: str, authority: Authority) -> Message: """No lost-device signature. Trusted recovery/session re-verifies the account. This is a provider-local administrative action, NOT an unauthenticated wire command. The receipt is returned to that trusted management channel, not to Token.accept(). """ require("REVOKE" in authority.operations and bool(authority.context), "recovery authority") r = self.relations.get(rid) require(r is not None and r.account == authority.account and r.state == "active", "account") require(target in r.keys, "target") r.keys[target] = (r.keys[target][0], "revoked") r.revision += 1 return sign(self.key, ["RESULT", VERSION, SUITE, "REVOKE", secrets.token_hex(16), self.provider, rid, "", target, "", "OK", "revoked", str(r.revision), "online-store"]) class Token: def __init__(self, provider: str, pinned_provider_key: str, rid: str | None = None, key: Ed25519PrivateKey | None = None): self.provider, self.pin = provider, pinned_provider_key self.rid, self.cid = rid or secrets.token_hex(16), secrets.token_hex(16) self.key = key or Ed25519PrivateKey.generate() self.state, self.revision = "pending", 0 self.awaiting: dict[str, Message] = {} def respond(self, challenge: Message, approved: bool) -> Message: """approved comes from a trusted local user-verification/consent path; never from a reader-controlled bit. Simple presence is not strong user verification. """ c = header(challenge, "CHALLENGE", 11) verify(self.pin, challenge) require(c[5] == self.provider and (c[6] == self.rid or (c[3] == "USE" and c[6] == "")), "provider/relationship") require(hex_value(c[8], 32) and c[9].isdigit() and bool(c[10]), "challenge fields") require(approved and self.state not in {"revoked", "retracted", "forgotten"}, "local consent/state") op, target = c[3], c[7] require((op in {"REGISTER", "USE"} and target == "") or (op == "REVOKE" and hex_value(target, 16)) or (op == "RETRACT" and target == "*"), "target") require(op == "REGISTER" or self.state == "active", "not enrolled") proof = sign(self.key, ["PROOF", VERSION, SUITE, op, c[4], self.provider, self.rid, self.cid, target, digest(challenge), raw_public(self.key) if op == "REGISTER" else ""]) self.awaiting[c[4]] = proof return proof def accept(self, receipt: Message) -> None: f = header(receipt, "RESULT", 14) verify(self.pin, receipt) require(f[4] in self.awaiting, "no matching local transaction") p = self.awaiting[f[4]] require(f[3:9] == p.fields[3:9] and f[9] == digest(p) and f[10] == "OK", "receipt binding") require(f[13] == "online-store" and f[12].isdigit() and int(f[12]) >= self.revision, "receipt scope/revision") expected = {"REGISTER":"registered", "USE":"accepted", "REVOKE":"revoked", "RETRACT":"retracted"}[f[3]] require(f[11] == expected, "receipt state") self.revision = int(f[12]) if f[3] == "REGISTER": self.state = "active" elif f[3] == "RETRACT": self.state = "retracted" elif f[3] == "REVOKE" and f[8] == self.cid: self.state = "revoked" # Keep awaiting proof for idempotent receipt acceptance. Production uses bounded durable storage. def forget_locally(self) -> None: self.state = "forgotten" # No remote revocation occurred. Memory is not secure erasure. self.awaiting.clear() # An old receipt must not resurrect a locally removed profile. @dataclass(frozen=True) class LegacyProfile: """Local owner-approved profile. This object is NEVER transmitted to the reader.""" name: str medium: str value: str selection: str = "explicit" authority: str = "provider-approval-required" def present(self, owner_unlocked: bool, selected: bool) -> str: require(owner_unlocked and selected and self.selection == "explicit", "select/unlock locally") require(self.medium in {"barcode", "qr", "nfc-uid", "nfc-application"}, "unsupported medium") require(self.authority == "provider-approved", "not approved for enrolment/use") require(bool(self.value), "empty value") # Only the pre-agreed value goes to a renderer/driver. No BringPass command. # An NFC driver must implement the real existing application; UID is a read-only constraint. return self.value