Files
amethyst/commons
Claude a8d11e4c58 feat(marmot): current-profile image crypto and Nostr transport corrections
Finishes Stage 3 and lands most of Stage 4.

Image crypto (0x8002). GroupBlossomImageCrypto implements the current-profile
scheme, which breaks from MIP-01 in three ways: image_key IS the AEAD key rather
than an HKDF seed, image_upload_key IS the Blossom-auth secret rather than a
seed, and the AAD is domain-separated and binds the media type where MIP-01 used
an empty AAD. That last one closes a real hole — with no AAD a blob could be
replayed as a different media type. MarmotMediaType implements the frozen
canonicalization; it uses ASCII case folding explicitly, because a locale-aware
lowercase would map a dotted capital I to a dotless one and change the AAD
bytes. Decryption verifies the content hash before attempting the AEAD: the blob
is addressed by hash, so a store returning different bytes is broken or hostile,
and finding out through an authentication failure loses that distinction. The
MIP-01 scheme stays for groups already on disk and nothing falls back between
them, because the component id is the version.

Transport. kind:30443 gains a current-profile builder emitting the required tag
set and omitting the two tags the spec forbids: encoding (a receiver decodes
each field by the rule that defines it, never by a negotiated marker) and relays
(discovery is the author's NIP-65 write set). Validation is profile-aware, told
apart by the presence of app_components rather than a version tag.

KeyPackage relay discovery moves to the NIP-65 write set. publishRelaysFor no
longer prefers a kind:10051 list — publishing only where a now-removed list
points would make us invisible to a conformant peer, which looks in the NIP-65
set and nowhere else. The legacy list is unioned in rather than substituted, so
peers that have not migrated keep finding us.

Deduplication now uses SHA-256 over the recovered MLS bytes rather than the
Nostr event id, which the transport spec forbids as a dedup key. The old scheme
collapsed nothing it was supposed to: relays redeliver, and every transport copy
of one MLS message carries its own fresh ephemeral pubkey and therefore a
different event id — so cross-relay duplicates always got through, and a hostile
republisher could mint unlimited distinct ids for a single message. Dedup
necessarily moved after outer decryption, since there is nothing to hash before
that, and OutboundGroupEvent now carries the id so a publisher suppresses its own
echo by MLS identity.

Also enforces the KeyPackage Lifetime bound (present, current, at most
7,261,200 seconds) and validates the embedded account identity proof on inbound
current-profile KeyPackages — the app_components tag is only an advertisement, so
the decoded LeafNode is what decides.

Fifteen new tests. Full quartz jvmTest: 4,557 tests, 0 failures. commons and cli
compile.

Left for Stage 6: bounded retained-candidate trial decryption for kind:445. Its
rule is defined over the retained-state set convergence owns, so it cannot land
ahead of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kCuA6tc4JQzHPCDd39GHq
2026-09-08 16:40:18 +00:00
..