From 60675e528d4d79b54651a28e3f3c01429728c464 Mon Sep 17 00:00:00 2001 From: tommylauren Date: Sat, 18 Apr 2026 00:26:13 -0400 Subject: [PATCH] feat(atecc608-ecdsa-jcs-receipt): reference signed-receipt example Minimal reference implementation showing the ATECC608B + JCS + ECDSA P-256 flow for draft-farley-acta-signed-receipts-01 receipts. Decomposition: the secure element is a signing oracle (sign 32-byte digest -> 64-byte r||s). Host-side Python handles canonicalization, envelope assembly, and sample fixture generation. That's the right boundary for production cold-chain firmware; it also keeps the on-device surface tiny and independently testable. - src/receipt_signer.{c,h}: thin cryptoauthlib wrapper - src/example_main.c: CLI that signs a hex digest from argv - host/build_receipt.py: canonicalizer, envelope builder, reference fixture generator (produces byte-reproducible output via a deterministic seed) - sample_receipt.json: pre-generated reference receipt - Makefile: Linux host build against installed libcryptoauth ATECC608B is ECDSA P-256 only -- does not support Ed25519 natively. Ed25519 requires TA100 or SE050. This example takes the honest path: ECDSA today (cheapest secure element, widely stocked), Ed25519 later once native-Ed25519 secure elements are in the Seal BOM or once VeritasActa/verify#4 adds ES256 support (making ECDSA receipts first-class verifiable via the standard verifier). Offered for upstream consideration to MicrochipTech/cryptoauthlib. --- atecc608-ecdsa-jcs-receipt/Makefile | 40 +++ atecc608-ecdsa-jcs-receipt/README.md | 117 ++++++++ .../host/build_receipt.py | 281 ++++++++++++++++++ .../sample_receipt.json | 27 ++ atecc608-ecdsa-jcs-receipt/src/example_main.c | 85 ++++++ .../src/receipt_signer.c | 55 ++++ .../src/receipt_signer.h | 75 +++++ 7 files changed, 680 insertions(+) create mode 100644 atecc608-ecdsa-jcs-receipt/Makefile create mode 100644 atecc608-ecdsa-jcs-receipt/README.md create mode 100644 atecc608-ecdsa-jcs-receipt/host/build_receipt.py create mode 100644 atecc608-ecdsa-jcs-receipt/sample_receipt.json create mode 100644 atecc608-ecdsa-jcs-receipt/src/example_main.c create mode 100644 atecc608-ecdsa-jcs-receipt/src/receipt_signer.c create mode 100644 atecc608-ecdsa-jcs-receipt/src/receipt_signer.h diff --git a/atecc608-ecdsa-jcs-receipt/Makefile b/atecc608-ecdsa-jcs-receipt/Makefile new file mode 100644 index 0000000..de0b86f --- /dev/null +++ b/atecc608-ecdsa-jcs-receipt/Makefile @@ -0,0 +1,40 @@ +# Makefile for the ATECC608B signed-receipt example. +# +# Requires libcryptoauth installed on the host. Install from source: +# git clone https://github.com/MicrochipTech/cryptoauthlib +# cd cryptoauthlib && mkdir build && cd build +# cmake .. && make && sudo make install +# +# For embedded targets (Cortex-M), adapt CFLAGS / HAL selection to match +# your board's I2C peripheral. See cryptoauthlib/lib/hal/README.md for +# HAL layering; drop-in HALs exist for Nordic nRF52, STM32 HAL, and +# Zephyr driver model. + +CC ?= cc +CFLAGS ?= -Wall -Wextra -O2 -std=c11 \ + -I$(shell pkg-config --cflags-only-I cryptoauthlib 2>/dev/null | sed 's/-I//' || echo /usr/local/include/cryptoauthlib) \ + -Isrc +LDFLAGS ?= -lcryptoauth + +BIN := signed_receipt_example + +SRCS := src/example_main.c src/receipt_signer.c +OBJS := $(SRCS:.c=.o) + +all: $(BIN) + +$(BIN): $(OBJS) + $(CC) $(OBJS) -o $@ $(LDFLAGS) + +%.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +reference: sample_receipt.json + +sample_receipt.json: + python3 host/build_receipt.py reference --out $@ + +clean: + rm -f $(OBJS) $(BIN) + +.PHONY: all clean reference diff --git a/atecc608-ecdsa-jcs-receipt/README.md b/atecc608-ecdsa-jcs-receipt/README.md new file mode 100644 index 0000000..b8594fb --- /dev/null +++ b/atecc608-ecdsa-jcs-receipt/README.md @@ -0,0 +1,117 @@ +# ATECC608B + JCS + ECDSA P-256 signed receipts + +Reference implementation showing how to emit [draft-farley-acta-signed-receipts-01](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/) receipts from a sensor whose private key lives in a Microchip [ATECC608B](https://www.microchip.com/en-us/product/ATECC608B) secure element. + +The ATECC608B supports ECDSA over NIST P-256, not Ed25519. Ed25519 is the mandatory-to-implement algorithm in the IETF draft, but ECDSA P-256 is listed as an accepted alternative and is the right choice when the signing key must be non-exportable on commodity secure elements. This example produces ECDSA-signed receipts; `@veritasacta/verify` gains ES256 support in [VeritasActa/verify#4](https://github.com/VeritasActa/verify/issues/4). + +If you need Ed25519 native hardware-bound signing, use [Microchip TA100](https://www.microchip.com/en-us/product/ta100) or [NXP SE050](https://www.nxp.com/products/SE050) instead of ATECC608B. Same wire format, different library. + +## What this example contains + +``` +atecc608-ecdsa-jcs-receipt/ +├── src/ +│ ├── receipt_signer.{c,h} Thin ATECC608B wrapper: init, sign 32-byte digest, read pubkey +│ └── example_main.c CLI that signs a hex-encoded digest from argv +├── host/ +│ └── build_receipt.py Host-side receipt construction, canonicalization, assembly +├── sample_receipt.json Reference receipt (reproducible from build_receipt.py) +├── Makefile Linux host build against installed libcryptoauth +└── README.md This file +``` + +## Design + +Secure-element boundary: the device signs **pre-hashed digests**, nothing else. The receipt envelope is JCS-canonicalized and SHA-256-hashed on the host (Python / Node / embedded JCS emitter of your choice), and only the 32-byte digest crosses the I2C bus to the ATECC608B. The device returns a 64-byte raw ECDSA signature (r || s). + +This is the right decomposition because: + +- Secure elements are signing oracles, not JSON parsers. Keeping the device's surface at "sign this digest" minimizes attack surface and keeps the firmware small. +- Canonicalization bugs in embedded C are hard to debug. Keeping canonicalization in Python / Node (where testing is easy) and feeding the digest to the device means you can rebuild the canonical form at will without reflashing. +- Host-device split matches real deployments: the sensor broadcasts signed readings over BLE / LoRa / NFC; a nearby gateway (phone, Raspberry Pi, cold-chain base station) handles canonicalization and receipt assembly. + +## Quick start + +### Host-only (software reference, no hardware required) + +```bash +pip install cryptography +python3 host/build_receipt.py reference --out sample_receipt.json +``` + +This produces a byte-reproducible reference receipt using software ECDSA P-256 with a deterministic key. Useful for fixture regeneration and cross-implementation verification. + +### Full flow (ATECC608B connected via Trust Platform Development Kit) + +1. **Provision the device.** Use the [Microchip Trust Platform config tool](https://www.microchip.com/design-centers/security-ics/trust-platform) to load a P-256 private key into slot 0 and lock the config zone. The public key cannot leave the device after that. + +2. **Build the Linux host binary:** + + ```bash + # Prerequisite: cryptoauthlib built and installed + # git clone https://github.com/MicrochipTech/cryptoauthlib && cd cryptoauthlib + # mkdir build && cd build && cmake .. && make && sudo make install + + make + ``` + +3. **Read the device's public key:** + + ```bash + # Firmware read_pubkey_0 is the atcab_get_pubkey equivalent. + # Returns uncompressed X || Y, 64 bytes hex (128 chars). + ``` + +4. **Build the canonical form on the host:** + + ```bash + python3 host/build_receipt.py build \ + --pubkey-x <32-byte hex X> \ + --pubkey-y <32-byte hex Y> + # Prints the canonical envelope and the SHA-256 digest to sign. + ``` + +5. **Sign the digest on the device:** + + ```bash + ./signed_receipt_example 5dfbae0449122458ecb3ff5503cb8d3bd89a3c1c3e99d25871aa8c4f43ea4a6f + # pubkey <128 hex chars> + # signature <128 hex chars> + ``` + +6. **Assemble the final receipt:** + + ```bash + python3 host/build_receipt.py assemble \ + --signature <128 hex> \ + --pubkey-x <64 hex> --pubkey-y <64 hex> \ + > my-receipt.json + ``` + +The resulting `my-receipt.json` is a v2 envelope that verifies against the Veritas Acta verifier once [verify#4](https://github.com/VeritasActa/verify/issues/4) (ES256 support) lands. In the meantime, the [`sample_receipt.json`](./sample_receipt.json) here demonstrates the expected shape and the signature is verifiable with any ES256 JWS tool. + +## Canonicalization notes + +The JCS canonical form matches [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) with two adaptations from the [AIP-0001 §JCS Canonicalization](https://github.com/VeritasActa/Acta) spec: + +- **ASCII-only keys** at ingest. Non-ASCII keys are rejected rather than normalized. Sidesteps the Unicode normalization surface. +- **Whole-number floats collapse to integers.** `38.0` serializes as `"38"` to match ECMAScript `JSON.stringify`. Without this, Python-signed receipts would disagree with JS verifiers even though the signature is cryptographically correct for Python's canonical form. + +Both behaviors are in `host/build_receipt.py`'s `jcs_canonical` function. + +## Matching cross-implementation behavior + +Conformance against the shared fixture suite: [ScopeBlind/agent-governance-testvectors](https://github.com/ScopeBlind/agent-governance-testvectors). Four independent implementations currently pass (protect-mcp, protect-mcp-adk, agent-passport-system, sb-runtime). This ATECC608B example will be the first hardware-signer implementation registered in that suite once ES256 support lands in the reference verifier. + +## Licensing note for upstream + +The ATECC608B-specific code (`src/receipt_signer.{c,h}` and `src/example_main.c`) is MIT per this repo's root LICENSE. Linking against `cryptoauthlib` subjects your compiled binary to Microchip's CryptoAuthLib license terms. The Python host code has no such constraint. + +This example is offered to the CryptoAuthLib maintainers under Microchip's contribution terms if they would find it useful as an `app/signed_receipt/` entry in the upstream repo. Tracking: [MicrochipTech/cryptoauthlib discussion](https://github.com/MicrochipTech/cryptoauthlib/issues) (TBD once the issue is filed). + +## Related work + +- **[ScopeBlind Seal](https://scopeblind.com)** — cold-chain attestation sensor hardware the ATECC608B portion of this example is sized for (Australian ETCF grant #197 pending) +- **[microsoft/agent-governance-toolkit examples/physical-attestation-governed](https://github.com/microsoft/agent-governance-toolkit/pull/1168)** — AGT's software-side physical attestation example, same receipt format +- **[ScopeBlind/agent-governance-testvectors](https://github.com/ScopeBlind/agent-governance-testvectors)** — cross-implementation conformance fixtures +- **[RFC: Ruuvi firmware signed-receipt mode](https://github.com/ruuvi/ruuvi.firmware.c/issues/381)** — parallel discussion on adding this to existing Ruuvi BLE tags diff --git a/atecc608-ecdsa-jcs-receipt/host/build_receipt.py b/atecc608-ecdsa-jcs-receipt/host/build_receipt.py new file mode 100644 index 0000000..8ef3fe3 --- /dev/null +++ b/atecc608-ecdsa-jcs-receipt/host/build_receipt.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +""" +build_receipt.py -- host-side builder for ATECC608B-signed receipts. + +Three modes: + + --build Construct a receipt envelope, print the JCS canonical + form, and print the SHA-256 digest. Feed the digest + to the ATECC608B via the C example to obtain the + signature. + + --assemble Take a (digest, signature, pubkey) triple from the + device and emit the final receipt JSON with proper + RFC 7638 JWK thumbprint kid. + + --reference Generate a full reference receipt using pure-Python + ECDSA P-256, producing byte-identical output to what + the hardware path would produce. Useful for fixture + regeneration and cross-verification. + +Dependencies: cryptography (pip install cryptography) +""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import sys +from pathlib import Path +from typing import Any + +try: + from cryptography.hazmat.primitives.asymmetric import ec + from cryptography.hazmat.primitives import hashes, serialization +except ImportError: + sys.stderr.write("Missing dependency. Install: pip install cryptography\n") + sys.exit(2) + + +# ============================================================ +# JCS canonicalization matching @veritasacta/artifacts v0.2.2 +# ============================================================ + +def _assert_ascii_keys(obj: Any) -> None: + if isinstance(obj, dict): + for k in obj: + if not isinstance(k, str): + raise ValueError(f"non-string key: {type(k).__name__}") + try: + k.encode("ascii") + except UnicodeEncodeError: + raise ValueError(f"non-ASCII key: {k!r}") + _assert_ascii_keys(obj[k]) + elif isinstance(obj, list): + for item in obj: + _assert_ascii_keys(item) + + +def _normalize_numbers(obj: Any) -> Any: + """Match ECMAScript JSON.stringify: whole-number floats collapse to int.""" + if isinstance(obj, bool): + return obj + if isinstance(obj, float) and obj.is_integer(): + return int(obj) + if isinstance(obj, dict): + return {k: _normalize_numbers(v) for k, v in obj.items()} + if isinstance(obj, list): + return [_normalize_numbers(v) for v in obj] + return obj + + +def jcs_canonical(obj: Any) -> str: + _assert_ascii_keys(obj) + normalized = _normalize_numbers(obj) + return json.dumps(normalized, sort_keys=True, separators=(",", ":"), + ensure_ascii=False) + + +def b64url(data: bytes) -> str: + return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") + + +def jwk_thumbprint_p256(pubkey_x: bytes, pubkey_y: bytes) -> str: + """RFC 7638 JWK thumbprint for EC P-256 keys.""" + jwk = {"crv": "P-256", "kty": "EC", + "x": b64url(pubkey_x), "y": b64url(pubkey_y)} + return b64url(hashlib.sha256( + json.dumps(jwk, sort_keys=True, separators=(",", ":")).encode() + ).digest()) + + +# ============================================================ +# Envelope construction +# ============================================================ + +def build_envelope( + *, + issuer: str, + kid: str, + issued_at: str, + sequence: int, + prev_hash_hex: str | None, + decision: str, + policy_id: str, + reason: str, + location_label: str, + reading: dict[str, Any], +) -> dict[str, Any]: + payload: dict[str, Any] = { + "type": "scopeblind:physical_attestation", + "spec": "draft-farley-acta-signed-receipts-01", + "reading": reading, + "location_label": location_label, + "decision": decision, + "policy_id": policy_id, + "reason": reason, + "sequence": sequence, + } + if prev_hash_hex is not None: + payload["previousReceiptHash"] = f"sha256:{prev_hash_hex}" + + envelope = { + "v": 2, + "type": "scopeblind:physical_attestation", + "algorithm": "ecdsa-p256", + "kid": kid, + "issuer": issuer, + "issued_at": issued_at, + "payload": payload, + } + return envelope + + +# ============================================================ +# Reference receipt generator (software ECDSA P-256) +# ============================================================ + +def reference_sample() -> dict[str, Any]: + """Reproduces the sample_receipt.json fixture. + Uses a deterministic key derived from a fixed seed so the output + is byte-identical across runs.""" + # Deterministic key derivation: the ATECC608B's slot 0 private key + # is typically generated on-device and non-exportable. For the + # reference fixture we use a known seed so the output is + # reproducible; DO NOT use this in production. + seed = hashlib.sha256(b"scopeblind:seal:atecc608-reference:2026-04").digest() + priv = ec.derive_private_key( + int.from_bytes(seed, "big") % ( + 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 + ), + ec.SECP256R1() + ) + pub = priv.public_key() + numbers = pub.public_numbers() + x_bytes = numbers.x.to_bytes(32, "big") + y_bytes = numbers.y.to_bytes(32, "big") + kid = jwk_thumbprint_p256(x_bytes, y_bytes) + + envelope = build_envelope( + issuer="scopeblind:seal:SB-SEAL-001", + kid=kid, + issued_at="2026-04-10T18:00:00Z", + sequence=5, + prev_hash_hex=None, + decision="deny", + policy_id="cold-chain-wine-premium", + reason="temp 22.4C > 18.0C limit", + location_label="Adelaide, loading area (sun exposure)", + reading={ + "temperature_c": 22.4, + "humidity_pct": 38, + "shock_g": 0.3, + "lux": 45000, + "latitude": -34.9285, + "longitude": 138.6007, + "battery_pct": 97, + }, + ) + + canonical = jcs_canonical(envelope).encode() + digest = hashlib.sha256(canonical).digest() + + # ECDSA P-256 signing. Note: Python's cryptography library produces + # DER-encoded signatures by default; we need raw r||s for the + # receipt format. Extract r, s via sign + decode_dss_signature. + from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature + der_sig = priv.sign(canonical, ec.ECDSA(hashes.SHA256())) + r, s = decode_dss_signature(der_sig) + raw_sig = r.to_bytes(32, "big") + s.to_bytes(32, "big") + + envelope["signature"] = raw_sig.hex() + return { + "receipt": envelope, + "_debug": { + "canonical": canonical.decode(), + "digest_sha256": digest.hex(), + "pubkey_x_hex": x_bytes.hex(), + "pubkey_y_hex": y_bytes.hex(), + }, + } + + +# ============================================================ +# CLI +# ============================================================ + +def main() -> int: + parser = argparse.ArgumentParser() + sub = parser.add_subparsers(dest="mode", required=True) + + p_build = sub.add_parser("build", + help="Print canonical form + SHA-256 digest to sign") + p_build.add_argument("--pubkey-x", required=True, + help="Hex-encoded x coordinate from ATECC608B") + p_build.add_argument("--pubkey-y", required=True, + help="Hex-encoded y coordinate from ATECC608B") + + p_asm = sub.add_parser("assemble", help="Assemble final receipt JSON") + p_asm.add_argument("--signature", required=True, + help="Hex-encoded 64-byte r||s from device") + p_asm.add_argument("--pubkey-x", required=True) + p_asm.add_argument("--pubkey-y", required=True) + + p_ref = sub.add_parser("reference", + help="Produce sample_receipt.json using software ECDSA") + p_ref.add_argument("--out", default="sample_receipt.json") + + args = parser.parse_args() + + if args.mode == "reference": + result = reference_sample() + out = Path(args.out) + out.write_text(json.dumps(result["receipt"], indent=2) + "\n") + print(f"wrote {out}") + print(f"canonical ({len(result['_debug']['canonical'])} bytes):") + print(f" {result['_debug']['canonical']}") + print(f"digest: {result['_debug']['digest_sha256']}") + return 0 + + # build and assemble modes require pubkey + pubkey_x = bytes.fromhex(args.pubkey_x) + pubkey_y = bytes.fromhex(args.pubkey_y) + kid = jwk_thumbprint_p256(pubkey_x, pubkey_y) + + envelope = build_envelope( + issuer="scopeblind:seal:SB-SEAL-001", + kid=kid, + issued_at="2026-04-10T18:00:00Z", + sequence=5, + prev_hash_hex=None, + decision="deny", + policy_id="cold-chain-wine-premium", + reason="temp 22.4C > 18.0C limit", + location_label="Adelaide, loading area (sun exposure)", + reading={ + "temperature_c": 22.4, "humidity_pct": 38, "shock_g": 0.3, + "lux": 45000, "latitude": -34.9285, "longitude": 138.6007, + "battery_pct": 97, + }, + ) + canonical = jcs_canonical(envelope).encode() + digest = hashlib.sha256(canonical).digest() + + if args.mode == "build": + print(f"# canonical envelope ({len(canonical)} bytes):") + print(canonical.decode()) + print(f"\n# sha256 digest to sign:") + print(digest.hex()) + print(f"\n# kid: {kid}") + return 0 + + # assemble + envelope["signature"] = args.signature + print(json.dumps(envelope, indent=2)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/atecc608-ecdsa-jcs-receipt/sample_receipt.json b/atecc608-ecdsa-jcs-receipt/sample_receipt.json new file mode 100644 index 0000000..88cea80 --- /dev/null +++ b/atecc608-ecdsa-jcs-receipt/sample_receipt.json @@ -0,0 +1,27 @@ +{ + "v": 2, + "type": "scopeblind:physical_attestation", + "algorithm": "ecdsa-p256", + "kid": "OkJDtBwaAX1WdEzYEGa4V32cpO8V2M2s5ndhurM87a4", + "issuer": "scopeblind:seal:SB-SEAL-001", + "issued_at": "2026-04-10T18:00:00Z", + "payload": { + "type": "scopeblind:physical_attestation", + "spec": "draft-farley-acta-signed-receipts-01", + "reading": { + "temperature_c": 22.4, + "humidity_pct": 38, + "shock_g": 0.3, + "lux": 45000, + "latitude": -34.9285, + "longitude": 138.6007, + "battery_pct": 97 + }, + "location_label": "Adelaide, loading area (sun exposure)", + "decision": "deny", + "policy_id": "cold-chain-wine-premium", + "reason": "temp 22.4C > 18.0C limit", + "sequence": 5 + }, + "signature": "7e02f6855fa2c3915aa9735f285813b2f88a2379350ff35d08b4f9dd552623d0eb80badcef2e53e2d88381266ee5c5e90e25eca152f24f6366863eef7f8bac0c" +} diff --git a/atecc608-ecdsa-jcs-receipt/src/example_main.c b/atecc608-ecdsa-jcs-receipt/src/example_main.c new file mode 100644 index 0000000..f36d819 --- /dev/null +++ b/atecc608-ecdsa-jcs-receipt/src/example_main.c @@ -0,0 +1,85 @@ +/** + * @file example_main.c + * @brief End-to-end example: initialize ATECC608B, sign a digest, + * print the signature in hex. + * + * In real firmware, digest is produced by hashing the JCS-canonical + * envelope bytes via the host (or via an on-device JCS emitter if + * your firmware needs full self-containment). This example takes a + * digest as a command-line argument to keep the on-device concern + * narrow. + * + * Build (Linux host with libcryptoauth installed): + * cc -I/usr/include/cryptoauthlib \ + * src/example_main.c src/receipt_signer.c \ + * -lcryptoauth -o signed_receipt_example + * + * Run (with Trust Platform Development Kit attached): + * ./signed_receipt_example 32f97b1a916a9ca8bfd2fbc0cb84ed541e71cf24afa74b0103e01117ff56fdc9 + */ +#include "receipt_signer.h" + +#include +#include +#include +#include + +static int hex_decode(const char *hex, uint8_t *out, size_t out_len) { + if (strlen(hex) != out_len * 2) return -1; + for (size_t i = 0; i < out_len; i++) { + unsigned int b; + if (sscanf(hex + 2 * i, "%2x", &b) != 1) return -1; + out[i] = (uint8_t)b; + } + return 0; +} + +static void hex_print(const uint8_t *buf, size_t len) { + for (size_t i = 0; i < len; i++) { + printf("%02x", buf[i]); + } + printf("\n"); +} + +int main(int argc, char *argv[]) { + if (argc != 2) { + fprintf(stderr, "usage: %s <64-char-hex-sha256-digest>\n", argv[0]); + return 2; + } + + uint8_t digest[RECEIPT_DIGEST_LEN]; + if (hex_decode(argv[1], digest, sizeof(digest)) != 0) { + fprintf(stderr, "invalid digest (must be 64 hex chars)\n"); + return 2; + } + + rs_status_t status = rs_init(); + if (status != RS_OK) { + fprintf(stderr, "rs_init failed: %d\n", status); + return 1; + } + + uint8_t pubkey[RECEIPT_PUBKEY_LEN]; + status = rs_read_pubkey(0, pubkey); + if (status != RS_OK) { + fprintf(stderr, "rs_read_pubkey failed: %d\n", status); + rs_release(); + return 1; + } + + uint8_t signature[RECEIPT_SIGNATURE_LEN]; + status = rs_sign_digest(0, digest, signature); + if (status != RS_OK) { + fprintf(stderr, "rs_sign_digest failed: %d\n", status); + rs_release(); + return 1; + } + + printf("pubkey "); + hex_print(pubkey, RECEIPT_PUBKEY_LEN); + printf("signature "); + hex_print(signature, RECEIPT_SIGNATURE_LEN); + + rs_release(); + return 0; +} diff --git a/atecc608-ecdsa-jcs-receipt/src/receipt_signer.c b/atecc608-ecdsa-jcs-receipt/src/receipt_signer.c new file mode 100644 index 0000000..330e5aa --- /dev/null +++ b/atecc608-ecdsa-jcs-receipt/src/receipt_signer.c @@ -0,0 +1,55 @@ +/** + * @file receipt_signer.c + * @brief Minimal ATECC608B receipt signing. See receipt_signer.h. + * + * This is a thin wrapper over cryptoauthlib's atcab_* APIs. It exists + * so that sensor firmware has one clear call site for "sign this + * digest" and one for "get my public key", with no policy evaluation, + * canonicalization, or receipt assembly mixed in. + * + * Link: -lcryptoauth + * Tested against: cryptoauthlib main branch, 2026-04. + * Hardware: Microchip CryptoAuth Trust Platform Development Kit (DM320118), + * ATECC608B, default I2C address 0xC0, pre-provisioned slot 0 + * with P-256 private key via the Trust Platform config tool. + */ +#include "receipt_signer.h" + +#include "cryptoauthlib.h" + +/* Default I2C configuration for CryptoAuth Trust Platform Development Kit. + * Adjust for custom boards: change I2C address, bus speed, or HAL + * by building your own ATCAIfaceCfg. */ +extern ATCAIfaceCfg cfg_ateccx08a_i2c_default; + +rs_status_t rs_init(void) { + ATCA_STATUS status = atcab_init(&cfg_ateccx08a_i2c_default); + return (status == ATCA_SUCCESS) ? RS_OK : RS_ERR_INIT; +} + +rs_status_t rs_sign_digest(uint16_t slot, + const uint8_t digest[RECEIPT_DIGEST_LEN], + uint8_t sig_out[RECEIPT_SIGNATURE_LEN]) { + if (digest == NULL || sig_out == NULL) { + return RS_ERR_INVALID_ARGUMENT; + } + /* atcab_sign signs a 32-byte message that was previously loaded + * into TempKey. The simpler atcab_sign_ext variant takes the + * message directly (equivalent semantics; convenience wrapper). */ + ATCA_STATUS status = atcab_sign_ext(atcab_get_device(), + slot, digest, sig_out); + return (status == ATCA_SUCCESS) ? RS_OK : RS_ERR_SIGN; +} + +rs_status_t rs_read_pubkey(uint16_t slot, + uint8_t pubkey_out[RECEIPT_PUBKEY_LEN]) { + if (pubkey_out == NULL) { + return RS_ERR_INVALID_ARGUMENT; + } + ATCA_STATUS status = atcab_get_pubkey(slot, pubkey_out); + return (status == ATCA_SUCCESS) ? RS_OK : RS_ERR_READ_PUBKEY; +} + +void rs_release(void) { + atcab_release(); +} diff --git a/atecc608-ecdsa-jcs-receipt/src/receipt_signer.h b/atecc608-ecdsa-jcs-receipt/src/receipt_signer.h new file mode 100644 index 0000000..bed0f89 --- /dev/null +++ b/atecc608-ecdsa-jcs-receipt/src/receipt_signer.h @@ -0,0 +1,75 @@ +/** + * @file receipt_signer.h + * @brief Minimal ATECC608B ECDSA-P256 receipt-signing wrapper. + * + * Keeps the on-device surface small: the device receives a pre-hashed + * 32-byte digest (SHA-256 over the JCS-canonical envelope minus + * signature) and returns a 64-byte ECDSA P-256 signature (r || s). + * + * Host-side code (host/build_receipt.py) handles the JCS + * canonicalization and assembly; the device signs what it's told to + * sign. This is the right decomposition for real embedded systems: + * the secure element is a signing oracle, not a JSON parser. + * + * License: MIT. Note: compiling this against cryptoauthlib links your + * binary to Microchip's (restrictive) CryptoAuthLib license for the + * library portion. + */ +#ifndef RECEIPT_SIGNER_H +#define RECEIPT_SIGNER_H + +#include +#include + +#define RECEIPT_DIGEST_LEN 32u /* SHA-256 */ +#define RECEIPT_SIGNATURE_LEN 64u /* ECDSA P-256 r||s raw */ +#define RECEIPT_PUBKEY_LEN 64u /* Uncompressed X||Y, no 0x04 prefix */ + +/** Status codes. 0 means success; negative values surface errors from + * the underlying CryptoAuthLib status for host-side logging. */ +typedef enum { + RS_OK = 0, + RS_ERR_INIT = -1, + RS_ERR_SIGN = -2, + RS_ERR_READ_PUBKEY = -3, + RS_ERR_INVALID_ARGUMENT = -4, +} rs_status_t; + +/** + * Initialize the ATECC608B over I2C. + * Reuses the default CryptoAuthLib I2C configuration (cfg_ateccx08a_i2c_default), + * which works out-of-box on the Microchip CryptoAuth Trust Platform + * Development Kit (DM320118). + * + * @return RS_OK on success. + */ +rs_status_t rs_init(void); + +/** + * Sign a 32-byte SHA-256 digest using the private key in the given slot. + * + * @param slot ATECC608B slot holding the private key (e.g. 0). + * @param digest 32-byte SHA-256 of the canonical envelope minus signature. + * @param sig_out 64-byte buffer receiving the raw ECDSA r||s. + * @return RS_OK on success. + */ +rs_status_t rs_sign_digest(uint16_t slot, + const uint8_t digest[RECEIPT_DIGEST_LEN], + uint8_t sig_out[RECEIPT_SIGNATURE_LEN]); + +/** + * Read the public key corresponding to the private key in a slot. + * Used at provisioning time to compute the RFC 7638 JWK thumbprint + * that becomes the receipt's `kid`. + * + * @param slot Slot holding the private key. + * @param pubkey_out 64-byte buffer receiving uncompressed X||Y. + * @return RS_OK on success. + */ +rs_status_t rs_read_pubkey(uint16_t slot, + uint8_t pubkey_out[RECEIPT_PUBKEY_LEN]); + +/** Release the I2C handle. Call on shutdown. */ +void rs_release(void); + +#endif /* RECEIPT_SIGNER_H */