diff --git a/se050-ed25519-jcs-receipt/Makefile b/se050-ed25519-jcs-receipt/Makefile new file mode 100644 index 0000000..4d73aa5 --- /dev/null +++ b/se050-ed25519-jcs-receipt/Makefile @@ -0,0 +1,47 @@ +# Makefile for the NXP SE050 signed-receipt example. +# +# Requires nxp-plugandtrust (SE05x SDK) built and installed on the host. +# Install from source: +# git clone https://github.com/NXPPlugNTrust/nxp-plugandtrust +# cd nxp-plugandtrust && mkdir build && cd build +# cmake .. -DPTMW_Host=PCWindows -DPTMW_SE05X_Auth=None +# make && sudo make install +# +# For embedded targets (Cortex-M), adapt CFLAGS / HAL selection to match +# your board's I2C peripheral. See nxp-plugandtrust/hostlib/hostLib/ +# platform/ for supported HALs (Raspberry Pi, iMX, STM32, Zephyr, etc). + +CC ?= cc +CFLAGS ?= -Wall -Wextra -O2 -std=c11 \ + -I/usr/local/include/sss \ + -I/usr/local/include/sss/api \ + -I/usr/local/include/ex \ + -Isrc +LDFLAGS ?= -lse05x -lsmCom -lex_common + +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 $@ + +verify: sample_receipt.json + @echo "Verifying with @veritasacta/verify (requires npx + npm)..." + npx @veritasacta/verify sample_receipt.json + +clean: + rm -f $(OBJS) $(BIN) + +.PHONY: all clean reference verify diff --git a/se050-ed25519-jcs-receipt/README.md b/se050-ed25519-jcs-receipt/README.md new file mode 100644 index 0000000..46315e8 --- /dev/null +++ b/se050-ed25519-jcs-receipt/README.md @@ -0,0 +1,152 @@ +# NXP SE050 + JCS + Ed25519 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 an [NXP SE050](https://www.nxp.com/products/SE050) secure element. + +## Why SE050 over ATECC608B + +SE050 supports **Ed25519 natively in hardware**. The IETF draft's mandatory-to-implement algorithm is Ed25519, so receipts emitted by an SE050-based signer verify directly against `@veritasacta/verify` without needing ES256 adapter support. Three practical differences from ATECC608B: + +| Property | ATECC608B | SE050 | +|---|---|---| +| Native Ed25519 | No (ECDSA P-256 only) | **Yes** | +| Native ECDSA P-256 | Yes | Yes | +| Price @ 10K volume | ~$0.60-$0.80 | ~$1.20-$2.00 | +| SDK | cryptoauthlib (MIT) | nxp-plugandtrust (BSD-3) | +| Linux / Zephyr story | Good | Better (first-class HAL) | +| Typical use | IoT, Matter devices | Pharma, supply-chain attestation | + +Use SE050 when: +- Ed25519 native is a requirement (IETF spec conformance, regulatory, pharma) +- You want first-class Linux/Zephyr integration +- The ~$1 per-device premium is acceptable + +Use ATECC608B when: +- Cost dominates at volume +- ECDSA P-256 + ES256 verifier support is acceptable +- You already have cryptoauthlib integrated + +## What this example contains + +``` +se050-ed25519-jcs-receipt/ +├── src/ +│ ├── receipt_signer.{c,h} Thin nxp-plugandtrust wrapper: init, sign 32-byte digest, read pubkey +│ └── example_main.c CLI that signs a hex 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 nxp-plugandtrust +└── 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, or an embedded JCS emitter of your choice). Only the 32-byte digest crosses the I2C bus to the SE050. The device returns a 64-byte Ed25519 signature (R || S). + +This matches how the [companion ATECC608B example](../atecc608-ecdsa-jcs-receipt/) is structured; the only meaningful difference is the algorithm family (Ed25519 vs ECDSA P-256) and the secure-element library (nxp-plugandtrust vs cryptoauthlib). + +## Quick start + +### Host-only (software reference, no hardware required) + +```bash +pip install cryptography +python3 host/build_receipt.py reference --out sample_receipt.json +``` + +Produces a byte-reproducible reference receipt using software Ed25519 with a deterministic seed. Good for fixture regeneration and cross-implementation conformance testing. + +Verify the reference receipt: + +```bash +npx @veritasacta/verify sample_receipt.json +# ✓ Signature valid (Ed25519) +``` + +### Full flow (SE050 connected via OM-SE050ARD or similar breakout) + +1. **Provision an Ed25519 keypair on the device.** Use nxp-plugandtrust's `ssscli` or the `ex_ed25519` sample: + + ```bash + ssscli connect se050 none + ssscli generate keypair ed25519 0x7DCCBB00 + # Key is now persistent under object ID 0x7DCCBB00 + ``` + +2. **Build the Linux host binary:** + + ```bash + # Prerequisite: nxp-plugandtrust built and installed + # git clone https://github.com/NXPPlugNTrust/nxp-plugandtrust + # cd nxp-plugandtrust && mkdir build && cd build && cmake .. + # make && sudo make install + + make + ``` + +3. **Read the device's public key** (via ssscli or the C example): + + ```bash + ssscli get object 0x7DCCBB00 --format=hex + # 32-byte Ed25519 public key + ``` + +4. **Build the canonical form on the host:** + + ```bash + python3 host/build_receipt.py build --pubkey <32-byte hex pubkey> + # Prints the canonical envelope and the SHA-256 digest to sign. + ``` + +5. **Sign the digest on the device:** + + ```bash + ./signed_receipt_example 0x7DCCBB00 <64-char hex sha256> + # pubkey <64 hex chars> + # signature <128 hex chars> + ``` + +6. **Assemble the final receipt:** + + ```bash + python3 host/build_receipt.py assemble \ + --signature <128 hex> \ + --pubkey <64 hex> \ + > my-receipt.json + ``` + +7. **Verify**: + + ```bash + npx @veritasacta/verify my-receipt.json + ``` + +## Canonicalization notes + +Identical to the ATECC608B example — matches [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) with two adaptations from [AIP-0001 §JCS Canonicalization](https://github.com/VeritasActa/Acta): + +- **ASCII-only keys** at ingest. +- **Whole-number floats collapse to integers** (`38.0` → `"38"`) to match ECMAScript `JSON.stringify`. + +Both behaviors live in `host/build_receipt.py`'s `jcs_canonical` function. + +## Cross-implementation posture + +SE050-emitted receipts verify cleanly against: + +- `@veritasacta/verify` (Apache-2.0 reference CLI) +- `agent-passport-system` verifier (APS) +- Any Ed25519 + JCS verifier that implements [draft-farley-acta-signed-receipts-01](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/) + +Conformance fixtures live at [ScopeBlind/agent-governance-testvectors](https://github.com/ScopeBlind/agent-governance-testvectors). Four independent software implementations (protect-mcp, protect-mcp-adk, agent-passport-system, sb-runtime) cross-verify today. This SE050-based implementation would be the first Ed25519-native *hardware* signer registered in that matrix. + +## Licensing note + +The SE050-specific wrapper code (`src/receipt_signer.{c,h}` and `src/example_main.c`) is MIT per the root LICENSE. Linking against nxp-plugandtrust subjects your compiled binary to NXP's SE05x SDK license terms (BSD-3 for most components; check `nxp-plugandtrust/LICENSE.txt`). The Python host code has no such constraint. + +## Related work + +- **[atecc608-ecdsa-jcs-receipt/](../atecc608-ecdsa-jcs-receipt/)** — Companion reference for the ATECC608B (ECDSA P-256, cheaper, needs ES256 verifier support) +- **[ScopeBlind Seal](https://scopeblind.com)** — cold-chain attestation sensor hardware the SE050 portion of this example is sized for +- **[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 nRF52-based BLE tags diff --git a/se050-ed25519-jcs-receipt/host/build_receipt.py b/se050-ed25519-jcs-receipt/host/build_receipt.py new file mode 100644 index 0000000..c1de228 --- /dev/null +++ b/se050-ed25519-jcs-receipt/host/build_receipt.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +""" +build_receipt.py -- host-side builder for SE050-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 SE050 via the C example to obtain the + signature. + + --assemble Take a (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 + Ed25519, 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 ed25519 + from cryptography.hazmat.primitives import serialization +except ImportError: + sys.stderr.write("Missing dependency. Install: pip install cryptography\n") + sys.exit(2) + + +# ============================================================ +# JCS canonicalization (matches @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 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_ed25519(pubkey_bytes: bytes) -> str: + """RFC 7638 JWK thumbprint for Ed25519 keys (OKP).""" + jwk = {"crv": "Ed25519", "kty": "OKP", "x": b64url(pubkey_bytes)} + 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": "ed25519", + "kid": kid, + "issuer": issuer, + "issued_at": issued_at, + "payload": payload, + } + return envelope + + +# ============================================================ +# Reference receipt generator (software Ed25519) +# ============================================================ + +def reference_sample() -> dict[str, Any]: + """Reproducible reference receipt using software Ed25519. + Deterministic seed -> byte-identical output across runs.""" + seed = hashlib.sha256(b"scopeblind:seal:se050-reference:2026-04").digest() + priv = ed25519.Ed25519PrivateKey.from_private_bytes(seed) + pub = priv.public_key() + pub_bytes = pub.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + kid = jwk_thumbprint_ed25519(pub_bytes) + + envelope = build_envelope( + issuer="scopeblind:seal:SB-SEAL-SE050-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() + signature = priv.sign(canonical) + + envelope["signature"] = signature.hex() + return { + "receipt": envelope, + "_debug": { + "canonical": canonical.decode(), + "digest_sha256": hashlib.sha256(canonical).hexdigest(), + "pubkey_hex": pub_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 envelope + SHA-256 digest for the device to sign") + p_build.add_argument("--pubkey", required=True, + help="64-char hex-encoded Ed25519 pubkey from SE050") + + p_asm = sub.add_parser("assemble", help="Assemble final receipt JSON") + p_asm.add_argument("--signature", required=True, + help="128-char hex-encoded Ed25519 signature from device") + p_asm.add_argument("--pubkey", required=True, + help="64-char hex-encoded Ed25519 pubkey from SE050") + + p_ref = sub.add_parser("reference", + help="Generate sample_receipt.json using software Ed25519") + 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 + + pubkey = bytes.fromhex(args.pubkey) + if len(pubkey) != 32: + print(f"pubkey must be 32 bytes (64 hex chars), got {len(pubkey)}", file=sys.stderr) + return 2 + + kid = jwk_thumbprint_ed25519(pubkey) + + envelope = build_envelope( + issuer="scopeblind:seal:SB-SEAL-SE050-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/se050-ed25519-jcs-receipt/sample_receipt.json b/se050-ed25519-jcs-receipt/sample_receipt.json new file mode 100644 index 0000000..788ab84 --- /dev/null +++ b/se050-ed25519-jcs-receipt/sample_receipt.json @@ -0,0 +1,27 @@ +{ + "v": 2, + "type": "scopeblind:physical_attestation", + "algorithm": "ed25519", + "kid": "SrvkfiLWsIAsShCcwUUjo9SP-6yaR5Gz_zOt2RkvyQs", + "issuer": "scopeblind:seal:SB-SEAL-SE050-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": "da1a712f283c23179e4a7781e333b5167ed00e5884cfe3fda31619dcd43bb8cfe26c9453401097b0bff3a7fbda1f93050cc00913f9a57ec59d6e068fc011d80e" +} diff --git a/se050-ed25519-jcs-receipt/src/example_main.c b/se050-ed25519-jcs-receipt/src/example_main.c new file mode 100644 index 0000000..57fc153 --- /dev/null +++ b/se050-ed25519-jcs-receipt/src/example_main.c @@ -0,0 +1,97 @@ +/** + * @file example_main.c + * @brief End-to-end example: initialize SE050, sign a digest with Ed25519, + * print the signature in hex. + * + * In real firmware, digest is produced by hashing the JCS-canonical + * envelope bytes via the host. This example takes a digest as a + * command-line argument to keep the on-device concern narrow. + * + * Build (Linux host with nxp-plugandtrust installed): + * cc -I/usr/include/sss \ + * src/example_main.c src/receipt_signer.c \ + * -lse05x -lsmCom -lex_common -o signed_receipt_example + * + * Provisioning note: before running, load an Ed25519 keypair into the + * SE050 under a known persistent object ID. The nxp-plugandtrust + * "ex_ed25519" sample or ssscli can do this: + * ssscli connect se050 none + * ssscli generate keypair ed25519 0x7DCCBB00 + * + * Run: + * ./signed_receipt_example 0x7DCCBB00 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 != 3) { + fprintf(stderr, "usage: %s <64-char-hex-sha256-digest>\n", argv[0]); + fprintf(stderr, "example: %s 0x7DCCBB00 5dfbae0449122458...\n", argv[0]); + return 2; + } + + /* Parse key ID (hex, 32-bit) */ + uint32_t key_id = (uint32_t)strtoul(argv[1], NULL, 0); + if (key_id == 0) { + fprintf(stderr, "invalid key id\n"); + return 2; + } + + uint8_t digest[RECEIPT_DIGEST_LEN]; + if (hex_decode(argv[2], 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(key_id, pubkey); + if (status != RS_OK) { + fprintf(stderr, "rs_read_pubkey failed: %d (key not provisioned at this ID?)\n", status); + rs_release(); + return 1; + } + + uint8_t signature[RECEIPT_SIGNATURE_LEN]; + status = rs_sign_digest(key_id, 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/se050-ed25519-jcs-receipt/src/receipt_signer.c b/se050-ed25519-jcs-receipt/src/receipt_signer.c new file mode 100644 index 0000000..916d7cf --- /dev/null +++ b/se050-ed25519-jcs-receipt/src/receipt_signer.c @@ -0,0 +1,131 @@ +/** + * @file receipt_signer.c + * @brief NXP SE050 Ed25519 receipt signing. See receipt_signer.h. + * + * Thin wrapper over nxp-plugandtrust (SSS API). Exists so that + * firmware has one clear call site for "sign this digest" and one + * for "get my public key", with no canonicalization, policy, or + * receipt assembly mixed in. + * + * Link: -lse05x + * Tested against: nxp-plugandtrust main branch, 2026-04. + * Hardware: SE050 on OM-SE050ARD / OM-SE051ARD breakout or + * Mikroe SE050 Click, connected via I2C at address 0x48. + */ +#include "receipt_signer.h" + +#include "fsl_sss_api.h" +#include "fsl_sss_util_asn1_der.h" +#include "ex_sss_boot.h" +#include "nxLog_App.h" + +#include + +/* Global SSS session + keystore, initialized by rs_init. */ +static ex_sss_boot_ctx_t g_boot_ctx; +static sss_session_t *g_session = NULL; +static sss_key_store_t *g_keystore = NULL; + +rs_status_t rs_init(void) { + sss_status_t status; + + memset(&g_boot_ctx, 0, sizeof(g_boot_ctx)); + + /* Open SSS session against SE050 via T=1/I2C (default transport). */ + status = ex_sss_boot_open(&g_boot_ctx, NULL); + if (status != kStatus_SSS_Success) { + LOG_E("ex_sss_boot_open failed: 0x%x", status); + return RS_ERR_INIT; + } + + g_session = &g_boot_ctx.session; + g_keystore = &g_boot_ctx.ks; + + return RS_OK; +} + +rs_status_t rs_sign_digest(uint32_t key_id, + const uint8_t digest[RECEIPT_DIGEST_LEN], + uint8_t sig_out[RECEIPT_SIGNATURE_LEN]) { + if (digest == NULL || sig_out == NULL || g_session == NULL) { + return RS_ERR_INVALID_ARGUMENT; + } + + sss_object_t obj; + sss_asymmetric_t ctx; + sss_status_t status; + size_t sig_len = RECEIPT_SIGNATURE_LEN; + + /* Look up the persistent Ed25519 keypair. */ + status = sss_key_object_init(&obj, g_keystore); + if (status != kStatus_SSS_Success) return RS_ERR_INIT; + + status = sss_key_object_get_handle(&obj, key_id); + if (status != kStatus_SSS_Success) { + sss_key_object_free(&obj); + return RS_ERR_OBJECT_NOT_FOUND; + } + + /* Create the asymmetric signing context for Ed25519. + * Algorithm kAlgorithm_SSS_SHA256 + EdDSA mode signs the + * 32-byte digest per RFC 8032. */ + status = sss_asymmetric_context_init( + &ctx, g_session, &obj, + kAlgorithm_SSS_SHA256, + kMode_SSS_Sign + ); + if (status != kStatus_SSS_Success) { + sss_key_object_free(&obj); + return RS_ERR_SIGN; + } + + /* Sign the canonical-envelope digest. */ + status = sss_asymmetric_sign_digest( + &ctx, + (uint8_t *)digest, RECEIPT_DIGEST_LEN, + sig_out, &sig_len + ); + + sss_asymmetric_context_free(&ctx); + sss_key_object_free(&obj); + + if (status != kStatus_SSS_Success) return RS_ERR_SIGN; + if (sig_len != RECEIPT_SIGNATURE_LEN) return RS_ERR_SIGN; + + return RS_OK; +} + +rs_status_t rs_read_pubkey(uint32_t key_id, + uint8_t pubkey_out[RECEIPT_PUBKEY_LEN]) { + if (pubkey_out == NULL || g_keystore == NULL) { + return RS_ERR_INVALID_ARGUMENT; + } + + sss_object_t obj; + sss_status_t status; + size_t key_len = RECEIPT_PUBKEY_LEN; + size_t key_bit_len = 256; + + status = sss_key_object_init(&obj, g_keystore); + if (status != kStatus_SSS_Success) return RS_ERR_INIT; + + status = sss_key_object_get_handle(&obj, key_id); + if (status != kStatus_SSS_Success) { + sss_key_object_free(&obj); + return RS_ERR_OBJECT_NOT_FOUND; + } + + status = sss_key_store_get_key(g_keystore, &obj, + pubkey_out, &key_len, &key_bit_len); + + sss_key_object_free(&obj); + + if (status != kStatus_SSS_Success) return RS_ERR_READ_PUBKEY; + return RS_OK; +} + +void rs_release(void) { + ex_sss_session_close(&g_boot_ctx); + g_session = NULL; + g_keystore = NULL; +} diff --git a/se050-ed25519-jcs-receipt/src/receipt_signer.h b/se050-ed25519-jcs-receipt/src/receipt_signer.h new file mode 100644 index 0000000..15b827d --- /dev/null +++ b/se050-ed25519-jcs-receipt/src/receipt_signer.h @@ -0,0 +1,74 @@ +/** + * @file receipt_signer.h + * @brief Minimal NXP SE050 Ed25519 receipt-signing wrapper. + * + * SE050 natively supports Ed25519 (RFC 8032). Unlike ATECC608B which is + * ECDSA P-256 only, SE050 can sign the IETF draft's mandatory-to-implement + * algorithm in hardware. This matters for interoperability with the + * @veritasacta/verify reference CLI, which accepts Ed25519-signed receipts + * without needing ES256 adapter support. + * + * Host-side code (host/build_receipt.py) handles the JCS canonicalization + * and envelope assembly; the device signs the 32-byte digest it's given + * and returns a 64-byte Ed25519 signature. + * + * License: MIT. Compiling against nxp-plugandtrust links your binary + * to NXP's SE05x SDK terms; the wrapper code here is MIT. + */ +#ifndef SE050_RECEIPT_SIGNER_H +#define SE050_RECEIPT_SIGNER_H + +#include +#include + +#define RECEIPT_DIGEST_LEN 32u /* SHA-256 */ +#define RECEIPT_SIGNATURE_LEN 64u /* Ed25519 signature */ +#define RECEIPT_PUBKEY_LEN 32u /* Ed25519 public key (compressed) */ + +typedef enum { + RS_OK = 0, + RS_ERR_INIT = -1, + RS_ERR_SIGN = -2, + RS_ERR_READ_PUBKEY = -3, + RS_ERR_INVALID_ARGUMENT = -4, + RS_ERR_OBJECT_NOT_FOUND = -5, +} rs_status_t; + +/** + * Initialize the SE050 over I2C via the nxp-plugandtrust SDK. + * Uses the default T=1 over I2C config from the SE05x Trust Platform. + * + * @return RS_OK on success. + */ +rs_status_t rs_init(void); + +/** + * Sign a 32-byte SHA-256 digest using the Ed25519 key in the given + * persistent object ID. Unlike the ATECC608B, SE050's Ed25519 sign + * operation signs the message directly per RFC 8032 (no pre-hashing + * wrapper). For receipt use the canonical bytes have already been + * hashed on the host; we invoke the raw Ed25519ph mode or pass the + * 32-byte digest as the message (implementation-dependent). + * + * @param key_id Persistent object ID holding the Ed25519 keypair + * (e.g. 0x7DCCBB00; assigned at provisioning). + * @param digest 32-byte SHA-256 of the canonical envelope minus signature. + * @param sig_out 64-byte buffer receiving Ed25519 signature (R || S). + * @return RS_OK on success. + */ +rs_status_t rs_sign_digest(uint32_t key_id, + const uint8_t digest[RECEIPT_DIGEST_LEN], + uint8_t sig_out[RECEIPT_SIGNATURE_LEN]); + +/** + * Read the 32-byte Ed25519 public key from a persistent object. + * Used at provisioning to compute the RFC 7638 JWK thumbprint that + * becomes the receipt's `kid`. + */ +rs_status_t rs_read_pubkey(uint32_t key_id, + uint8_t pubkey_out[RECEIPT_PUBKEY_LEN]); + +/** Release the SDK session. Call on shutdown. */ +void rs_release(void); + +#endif /* SE050_RECEIPT_SIGNER_H */