Skip to content

rapp1: conform packages/rapp1 to RAPP/1 rev-17 - #473

Merged
kody-w merged 7 commits into
mainfrom
rapp1/rev17-conformance
Sep 28, 2026
Merged

kody-w merged 7 commits into
mainfrom
rapp1/rev17-conformance

Conversation

@kody-w

@kody-w kody-w commented Sep 28, 2026

Copy link
Copy Markdown
Owner

RAPP/1 rev-17 is accepted (kody-w/rapp-1 @ f6bafe7, frame 84f1975d…). Its reference implementation and 428-vector
suite are authoritative, and five clean-room implementations agree with the reference on 60,000 differential fuzz
probes. This PR brings @rapp-work/rapp1 into agreement. rev-17 changes no wire form: the rev-14 authority fixture
still verifies, and no committed RAPP/1 artifact is touched. The repository has no vendored copy of the reference.

What changes and why

src/json.ts

  • assertUnicode (used by parseJson, snapshotJson and canonicalJson) now refuses Unicode noncharacters, as it
    already refused unpaired surrogates. That covers U+FDD0–U+FDEF and every U+xxFFFE/U+xxFFFF, in strings and member
    names (§4 (b), E-3).
  • Each hash tag now belongs to one function (§5, E-7). No caller in the repository uses another tag.
    • hashValue accepts only the H tags: rapp/1:particle, rapp/1:wave, rapp/1:egg-manifest,
      rapp/1:sealed-aad and rapp/1:sealed-key-request.
    • hashBytes accepts only the Hb tags: rapp/1:egg, rapp/1:rappid, rapp/1:grail and rapp/1:seal.
    • Any other tag, or a tag used with the other function, is refused.

src/identity.ts

  • isUtc now accepts years 0000–9999 (§7.4, E-1). Before, it refused year 0000.
  • streamFamily no longer caps a swarm-stream label at 64 characters. §6.1.1 (swarm-stream = "net:" lclabel) sets
    no bound there; instance and kind labels keep 1–64.
  • keyedIdentity refuses a key that is not a §10 key (§6.2, E-8). It accepts only an Ed25519 key whose point
    decodes (RFC 8032 §5.1.3), or a P-256 key with an uncompressed point.

src/signature.ts

  • New export parseDetachedJws. It checks the §10 detached compact form and protected-header profile, with no key
    lookup or cryptography:

    • canonical unpadded base64url;
    • header octets equal to canonical(header), with exactly alg, b64, crit and kid;
    • a signature of exactly 64 octets.

    verifyFrameSignature now uses it.

  • Ed25519 verification is now the cofactorless RFC 8032 §5.1.7 check with S < L. A public key that decodes is not
    refused for its order (§10, E-20).

    • A signature OpenSSL accepts is accepted.
    • A signature OpenSSL refuses, which it does for small-order points, is re-checked with exact integer arithmetic.
      Only public values are involved.

src/frame.ts

  • §7.5 step 1 now checks that a present sig has the §10 form and header profile (E-10). Before, a malformed sig
    reached step 6.
  • Step 1 now checks the *.re-genesis payload shape {migrated_from: {stream_id, terminal_seal, terminal_seq}}
    (§12.1, E-22). Step 4 refuses a re-genesis frame that is not a genesis. A valid re-genesis frame is still refused at
    step 6, because this package deliberately has no owner-authorization flow (unchanged).
  • scanFrameJson and scanChain now refuse octets that are not a §4 value before the checklist (step: null), not at
    step 1. This covers invalid JSON, over 1 MiB, depth over 64 and similar.
    • A §4 value in non-canonical form is still refused at step 1, because stored frames are never repaired (package
      rule, unchanged).
  • buildFrame refuses, and never normalizes, a payload member name at any depth that is not NFC or that holds a code
    point unassigned in the running Unicode version (§4, E-5, E-6).

Tests and docs

  • tests/protocol.test.ts: three expectations now follow rev-17.

    • Year 0000 is a valid utc.
    • A malformed sig is refused at step 1 as signature-format.
    • net: plus 65 characters is a swarm stream; net:a--b is not.

    A new rev-17 clarifications block covers E-3, E-5/E-6, E-7, E-8, E-10, E-20 and E-22, plus the step-null and
    non-canonical cases.

  • README.md: one paragraph lists the rev-17 rules.

Before and after

Measured with the estate kit (run_unit.py): the rev-17 vectors of each verb the adapter claims, then 3 x 20,000 differential fuzz probes against the reference (seeds 4, 5, 17). The adapter (adapters/openrappter/adapter.mjs) imports the package build packages/rapp1/dist, which is what the repository's callers import. Before = 38d43ab, after = fc44e80. All four units reach the target: 0 failing applicable vectors and 0 fuzz disagreements.

unit vectors before (pass/applicable) vectors after fuzz before (disagreeing/probes) fuzz after
json-ts (json.ts) 53/53 53/53 9455/60000 0/60000
identity-ts (identity.ts) 60/60 60/60 368/60000 0/60000
frame-ts (frame.ts) 86/90 90/90 606/60000 0/60000
signature-ts (signature.ts) 0/0 8/8 4163/60000 0/60000

The kit scores n/a answers as neither pass nor fail:

unit n/a vectors before n/a vectors after n/a fuzz probes before n/a fuzz probes after
json-ts 0 0 0 0
identity-ts 2 2 4531 4531
frame-ts 29 29 57805 56591
signature-ts 8 0 14996 0

These are inputs the unit never receives:

  • identity-ts rappid-mint: keyedIdentity takes a KeyObject, so it never sees octets node:crypto cannot
    import as a key. mintIdentity draws its octets from randomUUID(), which only yields UUIDv4 octets.
  • frame-ts:
    • registry_hex: the package takes no §13 registry document. Its kind registry is the compiled-in rev-14
      table (RAPP1_AUTHORITY.kindFamilies). Its keys are host-selected RegistryKey records, and translating
      §13.3 entries into them is host work.
    • With no registry, refusals decided by that compiled-in table (unregistered-kind, kind-family): the
      package has no registry-less mode.
    • frame-build with a given sig, or with a seq/prev/prev_wave that buildFrame does not take: it signs
      with a FrameSigner and derives the chain fields from a head.
    • trust mode on a signed frame: the unit verifies only against host-selected keys.
  • signature-ts before: jws-parse was n/a because the package exported no parser.

Most frame fuzz probes carry a registry, so the fuzzer's frame-verify coverage is mostly n/a. The vectors cover
the registry-less checklist.

Tests

I installed only what these packages need: npm ci --ignore-scripts, 102 packages, no electron or browsers.
node_modules was deleted afterwards.

command before (38d43ab) after (fc44e80)
npm run typecheck --workspace @rapp-work/rapp1 exit 0 exit 0
npm test --workspace @rapp-work/rapp1 90/90 passed 95/95 passed
npm test --workspace @rapp-work/security 26/26 26/26
npm test --workspace @rapp-work/domain 87/87 87/87
npm test --workspace @rapp-work/workspace-store 55/55 55/55
node scripts/check-legacy-absence.mjs exit 0 exit 0
node scripts/check-release-constitution.mjs exit 0 exit 0

The apps and tests/acceptance need the full install (electron) and were not run. Their callers use only
hashValue(PARTICLE_DOMAIN, ...), an H tag.

Left open

  • Registry model (design, unchanged). The package applies only its compiled-in rev-14 kind table and
    host-selected key records. A kind that an estate registers after rev-14 is refused as unregistered-kind, but
    a §7.5 consumer holding that registry would accept it. §13.3 tombstones and re-anchors reach the package only
    as revoked_utc and superseded_utc on a RegistryKey.
  • Re-genesis (design, unchanged). A well-formed, owner-signed *.re-genesis frame is still refused at step
    6, because the package deliberately has no owner-authorization flow (README).
  • Canonical storage (design, unchanged). scanFrameJson and scanChain refuse a §4 value presented in
    non-canonical text at step 1, because a stored frame is never repaired. §4 accepts insignificant whitespace in
    input text, so a frame another producer emits with whitespace would be refused here. No registry-less vector or
    fuzz probe disagreed on this.
  • Stored data. parseJson now refuses noncharacters in every JSON text it reads, including workspace files.
  • No .github/workflows change is needed. The repository has no vendored reference copy, and no committed
    RAPP/1 artifact was changed.

Part of the RAPP/1 rev-17 estate conformance pass: rev-17 is accepted in kody-w/rapp-1#46 (f6bafe7), where the reference and five independent implementations agree on all 428 vectors and 60,000 fuzz probes. The before/after numbers above come from the same vectors and fuzzer, run against this repository's own code.

kody-w and others added 7 commits September 28, 2026 09:00
…3, E-7)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…r Ed25519/P-256 (rev-17 E-1, E-8)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…NFC payload names, cofactorless Ed25519 (rev-17 E-5, E-6, E-10, E-20, E-22)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…before step 1, non-canonical §4 text at step 1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…(rev-17 E-8)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kody-w
kody-w merged commit 07c9af3 into main Sep 28, 2026
12 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant