feat(content-auth): attribute uploads by asserted user id, not signature - #476
Open
rickyrombo wants to merge 3 commits into
Open
feat(content-auth): attribute uploads by asserted user id, not signature#476rickyrombo wants to merge 3 commits into
rickyrombo wants to merge 3 commits into
Conversation
…hain Consensus can now gate a track's cid fields on a validator attesting that the claiming wallet uploaded those bytes. This produces that attestation. Verify an EIP-712 signature at tus upload creation for audio, and set Upload.UserWallet from the recovered address. It was previously read straight out of client-supplied tus metadata under a key the SDK never sent, so it was NULL on every SDK upload and forgeable on the rest — harmless while nothing read it, not harmless now that it decides who may claim the resulting cids. Typed data rather than a hashed JSON payload. It removes JSON canonicalization from the protocol entirely — the type definition is the encoding, so there is no key ordering or number formatting for client and server to agree on and later drift apart over — and it gives domain separation, which the previous scheme lacked: a signature produced for any other purpose over the same two fields would have been replayable as an upload authorization. It also matches how the SDK already signs entity-manager writes, so both sides use standard calls rather than a hand-rolled hashing scheme. The payload covers the user id and a timestamp, not the cid: at tus-create time the bytes have not been sent and nobody knows the cid yet. Both fields also travel unsigned in tus metadata because the verifier needs them to rebuild the typed data, but they are reproduced inside it, so tampering only breaks recovery. Stale and future-dated signatures are rejected — a captured one would otherwise be a standing credential to upload as someone else. Only audio is covered. Images are served unauthenticated, so there is no gating story to protect, and requiring signatures would break signup, which uploads a profile picture before the account has a user id. After transcoding, submit a ContentAttestation over all three cids — the original, the 320 transcode that becomes track_cid, and the preview. The original matters most: it is the download path, the lossless master. Both sides derive the signed bytes from one shared constructor so they cannot drift on field order or address casing. Content authorization runs off its own flag rather than ProgrammableDistributionEnabled. That flag governs DDEX; tying them together meant this bypass could only be closed on networks that also enable an unrelated subsystem with no production consumers, which is backwards for a fix to the ordinary track-upload path. Attestation requires a verified signature, not merely a wallet. The legacy POST /uploads and gRPC paths still populate UserWallet from an unverified X-User-Wallet-Addr header, so keying off the wallet alone would let a forged header mint a claim over someone else's content. Those paths are otherwise left alone; the header fallback is a pre-existing hole worth closing separately. Attestation failures are logged, not fatal. A dropped attestation costs the uploader the ability to claim those cids, which re-uploading fixes; failing the transcode would throw away the transcode too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
POST /generate_preview takes any cid on the network and produces new bytes with no upload behind them, so there is no uploader to credit — but the result still needs a claim. Previews stream publicly, so a preview cid anyone could name on their own track would let an attacker tile a gated track at 30-second offsets and reassemble it. The caller now signs the request with the same EIP-712 payload tus uploads use, and the node refuses unless that user already claims the source cid. The preview is then attested to them. Crediting the caller without the source check is the attack; crediting them with it grants nothing they did not already hold. Authorization runs before the blob pull and the ffmpeg run, so the endpoint also stops doing unbounded work for unauthenticated callers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the EIP-712 request-signature scheme with unauthenticated user-id assertion, for both tus uploads and generate_preview. The signature was not what made claims safe, and the assertion is not what makes them unsafe: - A claim cannot be stolen by asserting someone else's id: asserting user X only ever credits X, and exercising a claim — naming the cid on a track — happens in an entity-manager write, which is signed and grant-checked. - A claim cannot cover content the caller does not hold: upload claims derive from bytes the node received, and preview claims require the asserted user to already claim the source cid. - A minted preview cid is worthless to the requester: audio is never served by bare cid (serveBlob blocks it), and discovery only signs cidstream URLs it resolved from a track record. That serving invariant already guards gated tracks directly, so previews sit behind the same door as the masters they are cut from. What the signature bought — spam pricing and non-forgeable attribution — is priced by years of running these endpoints unguarded without abuse, and its cost was real: an OAuth client-side signing path that does not exist, a wallet-binding step that is easy to get wrong, and a freshness window to maintain. The EIP-712 scheme remains in history as the v2 contingency if attestation spam materializes. Consequences carried through here: - tus creates require a userId metadata key on audio when content auth is enabled (fail at create, not at publish); malformed ids are rejected even when it is not, so a bad assertion cannot masquerade as no assertion. - generate_preview takes ?userId= and refuses users that do not claim the source cid. Requests are refused before transcode work is done. - ContentAttestation carries an empty uploader address and signature, and consensus no longer requires them; the fields stay in the payload derivation so populated values remain covered by the validator signature. This also fixes preview attestations, which were built without an uploader and would have been rejected by the old required-field check. - The standing constraint is documented at the top of upload_auth.go: claims are authorization material, not user activity. Nothing may treat them as evidence a user did something without adding real authentication. Pre-existing environment-dependent failures (TestUploadFile, TestRepair, TestPollDelistStatuses) fail identically with and without this change; all other mediorum and core server tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #459 (base branch
feat/content-auth-tx). Replaces the mediorum half of the signature design — #460 and the unpushed preview branch — and includes their commits; #460 can be closed in favor of this. Client counterpart: AudiusProject/apps#14555.What changed vs. the signed design
The EIP-712 request-signature scheme is removed from both tus uploads and
generate_preview; attribution is an asserteduserId(tus metadata key / query parameter).upload_request_eip712.go, the freshness window, and the TS-fixture interop test are deleted.Why the assertion is sufficient (full reasoning at the top of
upload_auth.go):generate_previewrefuses users that do not already claim the source cid, and a minted preview cid is worthless to the requester:serveBlobnever serves audio by bare cid and discovery only signs track-resolved cidstream URLs. That serving invariant is load-bearing for gated content generally — if audio ever becomes fetchable by bare cid, gated tracks leak directly, previews or not.The signed design also had two defects this replaces outright: the recovered wallet was never bound to the asserted userId (making the signature decorative — any keypair could sign for any user, which on the preview endpoint reopened the exact bypass it exists to close), and preview attestations were built without an uploader address while consensus rejected empty ones.
Behavior
userIdwhenContentAuthEnabled(fail at create, not at publish); malformed ids are rejected even when it is off, so a bad assertion cannot pass as no assertion. Images unchanged.generate_preview?userId=— required under content auth, claim-checked against the source cid before any transcode work.ContentAttestationnow carries an emptyUploaderAddress/UploaderSignature; consensus accepts that (fields stay in the payload derivation so populated values remain signature-covered). Claims were already keyed on(cid, userId)— the projection never read the wallet.upload_auth.goand pinned by tests: claims are authorization material, not user activity. Nothing may treat "user X claims cid C" as evidence X did anything without adding real authentication.Testing
pkg/core/server: all pass, including a new test pinning that an empty uploader address is not a rejection reasonpkg/mediorum/server: auth/preview/attestation tests rewritten for the assertion model and pass;TestUploadFile/TestRepair/TestPollDelistStatusesfail identically with and without this change (pre-existing, environment-dependent multi-node tests)go build ./...clean; proto regen is comment-onlyFollow-ups worth separate issues: a regression test pinning the audio-never-by-bare-cid serving invariant, and per-IP rate limits on
generate_preview(it triggers network blob pulls, ffmpeg, and a blocking chain write).🤖 Generated with Claude Code