N-AALP conformance adapter contract¶
This is the contract every N-AALP reference SDK implements to be graded against the shared conformance corpus. It is byte-compatible with the mechanism the sibling protocol N-PAMP uses, so an implementer who has written one can write the other.
The naalp-conform runner (harness/runner/) drives the corpus
(vectors/conformance/corpus.json, assembled by tools/conformance_corpus.py from the per-family
non-circular oracles) through one adapter at a time, launched as a child process, and grades
each answer against the corpus's committed expected value. No adapter is ever compared to another
for the deterministic ops — every expected byte traces to an RFC / FIPS / NIST vector or a
from-scratch constructor (F3, non-circular). The single exception, the deterministic ML-DSA
signature, is graded by the separate cross-language consensus gate (tools/crypto_consensus.py).
Target languages (10)¶
| language | adapter dir | status |
|---|---|---|
| Go | harness/adapters/go/ |
wired |
| Rust | harness/adapters/rust/ |
wired |
| Python | harness/adapters/python/ |
planned |
| TypeScript | harness/adapters/typescript/ |
planned |
| C# | harness/adapters/csharp/ |
planned |
| Java | harness/adapters/java/ |
planned |
| Kotlin | harness/adapters/kotlin/ |
planned |
| PHP | harness/adapters/php/ |
planned |
| Ruby | harness/adapters/ruby/ |
planned |
| Swift | harness/adapters/swift_adapter/ |
wired |
wired = the adapter exists, builds, and passes the corpus in CI. planned = the adapter and its
SDK are built and graded under task #20. The authoritative {language -> build + launch} table is
harness/adapters.json (and, for CI, the matrix in .github/workflows/conformance.yml).
Wire protocol¶
Framing, both directions, over the child's stdin/stdout:
The adapter must flush stdout after every response (line-buffered stdout desyncs the pipe on some platforms).
Request (runner -> adapter):
Response (adapter -> runner) — exactly one of these three keys:
{ "out": { ...fields... } } // success: the produced value(s)
{ "error": "<reason>" } // the input was rejected, or processing failed
{ "skipped": "<why>" } // this op is not implemented by this SDK
Encoding rules
- Every byte-valued field is a lowercase hex string (
bytes_hex,pk_hex,id_hex, ...). The runner compares hex case-insensitively, but emit lowercase. - 64-bit counters (
seq,at,not_after,through_offset,offset, ...) may be a JSON number or a decimal string; adapters must parse either. Emit them however is natural for the value's magnitude (all current corpus values are < 2^53, so a JSON number is safe). skippedvserroris load-bearing: returnskippedwhen the SDK cannot do the op (e.g. no deterministic ML-DSA library), anderrorwhen the input should be rejected (a MUST-reject case) or processing genuinely failed. Grading treats them oppositely (below).
Grading (per case, from the corpus result)¶
result |
Pass when… |
|---|---|
valid |
adapter returns out and every key in expected matches |
invalid |
adapter returns error (a MUST-reject case) |
acceptable |
the call succeeds (either out or error is fine) |
| any | skipped -> Unimplemented (tracked, does not fail CI) |
matchExpected is a subset match: only keys present in expected are checked; extra keys in
out are ignored. Strings compare case-insensitively; numbers, bools, arrays (element-wise), and
nested maps compare structurally. A transport/pipe failure is a hard Fail and aborts the run. Exit
code is 1 iff any case Failed, else 0.
The op set (31 ops)¶
Byte fields are hex. -> shows in fields then out fields. Ops group by spine construction.
Pure (every language; SHA-256/384 + canonical CBOR only)¶
| op | in -> out |
|---|---|
sha384 |
msg_hex -> digest_hex (FIPS 180-4 KAT anchor) |
cbor.encode |
value (tagged) -> bytes_hex (deterministic CBOR, RFC 8949) |
cbor.decode |
bytes_hex -> {} or error (reject non-canonical) |
content.id |
body_hex -> id_hex (multihash(0x20,0x30, SHA-384(body))) |
cose.tbs |
protected_hex,payload_hex -> tobesigned_hex (RFC 9052 Sig_structure) |
signerid |
alg,pubkey_hex -> signer_id (multiformats PeerHandle form) |
nfc.check |
utf8_hex -> {ok:true} or error (reject non-NFC) |
effect.normalize |
value -> effect (0..3; unknown -> 3 destructive) |
effect.authorize |
granted,effect -> allow (bool; the §6.1 lattice) |
effect.safety_label |
risk,scope -> cbor_hex (safety-label map bytes) |
approval.body |
approves_hex,approver,grant,nonce_hex,not_after -> body_hex |
approval.id |
(same in) -> id_hex (content id of the approval body) |
ledger.entry |
seq,prev_hex,approval_id_hex,by -> body_hex |
receipt.body |
prev_hex,obj_hex,seq,at -> body_hex |
receipt.head |
body_hex -> head_hex (SHA-384 of the receipt body) |
causal.verify |
nodes:[{id_hex,causes_hex,position?}] -> {valid:true} or error |
delivery.update |
obj_hex,stage,at -> body_hex |
stream.digest |
chunks:[{offset,data_hex}] -> digest_hex (rolling SHA-384, offset order) |
stream.open |
stream_id_hex,effect,approval_hex?,substream -> body_hex |
stream.commit |
stream_id_hex,digest_hex -> body_hex |
stream.checkpoint |
stream_id_hex,through_offset,digest_so_far_hex -> body_hex |
transport.emit |
transport,sensitive,require_peer_auth -> result (ok / error kind) |
carriage.body |
protocol_id,class,content_type,correlation_hex,method,foreign_hex -> body_hex |
channels.lookup |
channel,kind -> name,effect,variable or error (UnknownKind) |
channels.effect_check |
channel,kind,effect -> {ok:true} or error |
federation.reconcile |
nodes:[{id_hex,causes_hex}] -> order:[id_hex,…] (deterministic merge) |
federation.record |
authorities:[str],order:[id_hex] -> body_hex |
Crypto (skippable where no deterministic FIPS 204 / Ed25519 library exists)¶
| op | in -> out |
|---|---|
mldsa.keygen |
param(ML-DSA-65/-87),seed_hex -> pk_hex (NIST ACVP seed->pk KAT) |
ed25519.sign |
sk_hex(32-byte seed),msg_hex -> sig_hex (RFC 8032 KAT) |
cose.sign1 |
alg,seed_hex,protected_hex,payload_hex -> obj_hex (consensus-graded) |
cose.verify1 |
alg,pubkey_hex,obj_hex -> valid (bool) |
mldsa.keygen is the ML-DSA-availability probe: an SDK whose language has no deterministic
FIPS 204 library returns skipped here, and its cose.sign1/cose.verify1 too. The runner records
those as Unimplemented (never a false green); the SDK is still fully graded on all the pure ops. Its
ML-DSA gap is tracked per language in harness/adapters.json.
The tagged-value form (cbor.encode input)¶
A logical CBOR value is a two-element array [tag, payload], so the encoder under test — not
the corpus author's language — produces the bytes:
| tag | payload | CBOR |
|---|---|---|
"u" |
a non-negative integer | unsigned int (major 0) |
"b" |
lowercase hex string | byte string (major 2) |
"s" |
a UTF-8 string | text string (major 3) |
"arr" |
[value, …] |
array (major 4) |
"map" |
[[key, value], …] |
map (major 5), canonical |
A conforming encoder emits map keys in canonical (bytewise-ascending) order regardless of input order.
Running¶
Build the two reference adapters, then grade each:
# runner
( cd harness/runner && GOWORK=off go build -o naalp-conform ./ )
# go adapter
( cd harness/adapters/go && GOWORK=off go build -o naalp-adapter-go ./ )
# rust adapter
( cd harness/adapters/rust && cargo build --release )
./harness/runner/naalp-conform run --testee "./harness/adapters/go/naalp-adapter-go"
./harness/runner/naalp-conform run --testee "./harness/adapters/rust/target/release/naalp-adapter-rust"
# cross-language deterministic ML-DSA byte-parity
python tools/crypto_consensus.py \
go=./harness/adapters/go/naalp-adapter-go \
rust=./harness/adapters/rust/target/release/naalp-adapter-rust
bash harness/run.sh runs the whole conformance suite (two-implementation parity + CDDL + registry
drift + this cross-language gate) and is what CI runs. naalp-conform vectors dumps the embedded
corpus.
Adding a new-language adapter¶
- Build the SDK for the language (the spine: deterministic CBOR, content-id, COSE ToBeSigned, signer-id, effect, approval, audit, delivery, streaming, carriage, channels, federation; plus ML-DSA/Ed25519 where a conformant library exists).
- Write
harness/adapters/<lang>/implementing this protocol — read the 4-byte-LE request loop, dispatch onop, call the SDK, write the framed response, flush. - Return
skippedfor any op the SDK genuinely cannot do (record the reason inharness/adapters.json); implement every pure op. - Add the
{build, launch}entry toharness/adapters.jsonand the CI matrix. naalp-conform run --testee "<launch>"must pass everyvalid/invalidcase (skips allowed only for the crypto ops when the language lacks a library); the consensus gate must agree oncose.sign1where implemented.