Skip to content

fix(attestation): verify attested signatures from did:web authorities#43

Merged
frrist merged 1 commit into
mainfrom
frrist/fix-attestation-didweb-authority
Jun 30, 2026
Merged

fix(attestation): verify attested signatures from did:web authorities#43
frrist merged 1 commit into
mainfrom
frrist/fix-attestation-didweb-authority

Conversation

@frrist

@frrist frrist commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

Fixes attested-signature verification when the attesting authority is a
did:web service
(e.g. did:web:upload). This is the bug behind the
post–Attested-Signatures guppy login failure, where claiming the account
delegation failed with:

proof … does not have a valid signature from "did:mailto:…"
  Tried: did:mailto:…#did:web:upload: signature mismatch

Root cause

attestation/verifier.go's Verifier.Verify re-validated the inner attestation
invocation via:

validator.ValidateInvocation(v.ctx, inv)   // no options

ValidateInvocation takes its DID resolver from options, and with none it
defaults to key.Resolverdid:key only. So for a did:web authority it
can't resolve the authority's document, verifyTokenSignature fails, and the
attestation is rejected as a signature mismatch.

did:key authorities resolve fine under the default, so the existing
TestSigner (which uses a did:key authority) passed and the gap shipped. The
Verifier already holds the resolved authorityVerifier, but Verify never
used it.

Fix

Verify the attestation invocation's signature directly with the
already-resolved authorityVerifier, instead of re-resolving the authority:

- if validator.ValidateInvocation(v.ctx, inv) != nil {
+ if !token.VerifySignature(inv, v.authorityVerifier) {
      return false
  }

Testing

  • New TestSigner_WebAuthority exercises a did:web authority resolved via its
    generated DID document — the path the existing did:key test never covered.
    Without this fix it fails with the same did:mailto:…#did:web:…: signature mismatch error seen in the running stack; with it, it passes.
  • Full libforge test suite passes.

Confirmed end-to-end in smelt

Verified against the full forge stack (smelt), not just unit tests:

  • With every service on latest main, guppy login fails with the exact
    signature mismatch above.
  • The verifier in the login path is sprue (upload)/access/claim has
    audience did:web:upload, so sprue validates the attested account-delegation
    proof, and the InvalidSignature is returned in sprue's receipt (guppy only
    relays it). Rebuilding guppy alone against this fix left login failing
    identically; rebuilding sprue against it is what fixed it.
  • With sprue rebuilt against this branch and deployed to smelt, guppy login
    succeeds: Successfully logged in as test@example.com! (exit 0).

Rollout — all services must update after merge

This is a verification-side fix in a shared library, so merging it is not
enough on its own: every service that validates attested signatures must bump
its libforge dependency and rebuild to pick it up — confirmed for sprue /
upload
(login path), and likewise needed for delegator, indexer, and
piri for their attested flows. Services that only sign or serve their
own did:web document are unaffected, but bumping uniformly is simplest.

Notes

  • Separately, identity.DIDDocument() emits a malformed verification-method id
    (#%23key-0 instead of #key-0, from doc.Fragment("#key-0")). It's cosmetic
    for verification (matching is by VM Material, not id) and is not fixed
    here; worth a follow-up for spec-correct did:web documents.

🤖 Generated with Claude Code

The attestation Verifier re-validated the attestation invocation with
validator.ValidateInvocation(v.ctx, inv) and no options. ValidateInvocation
reads its DID resolver from options, defaulting to key.Resolver (did:key
only) — so it could not resolve a did:web authority (e.g. did:web:upload),
and attested-signature verification failed with "signature mismatch". did:key
authorities (and the existing TestSigner) resolved fine, which is why this
slipped through.

Verify the attestation invocation's signature with the authority verifier the
Verifier already holds (v.authorityVerifier) instead of re-resolving the
authority. Add TestSigner_WebAuthority covering a did:web authority resolved
via its DID document; without this change it fails with the same
"signature mismatch" seen in the wild (e.g. `guppy login`).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@frrist frrist requested review from Peeja and alanshaw June 27, 2026 04:34
@frrist frrist self-assigned this Jun 27, 2026
@frrist frrist added the bug Something isn't working label Jun 27, 2026
Comment thread attestation/verifier.go
Comment on lines +57 to +62
// Verify the attestation invocation was actually signed by the authority,
// using the already-resolved authority verifier directly. Previously this
// re-validated via validator.ValidateInvocation with no options, which
// defaults to a did:key-only resolver and therefore fails to resolve a
// did:web authority — the cause of "signature mismatch" for did:web services
// (e.g. did:web:upload) while did:key authorities (and unit tests) passed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment isn't quite right. This didn't re-validate the (outer) token, it validated the signature, as an invocation, for the first time.

Technically, only verifying the signature leaves out a few ways the invocation could be invalid, like expiration, but we're not supposed to use any of those ways for these invocations. I think ideally this eventually change from using invocations to using assertions (or maybe they'll be called "attestations", which would certainly be appropriate), which would be a different kind of token and would specifically not have expiration dates and other things that aren't relevant.

More to the point, v.authorityVerifier isn't used for anything yet, so I'm pretty sure this is exactly why I added it, I just failed to do exactly this part. So good catch, thank you! 😅

@frrist frrist merged commit 2b55dbc into main Jun 30, 2026
7 checks passed
frrist pushed a commit to fil-forge/piri that referenced this pull request Jul 2, 2026
Bump ucantone 7985ec0 -> a8f24fe (adds acceptance of the DID Core v1
`@context`, https://www.w3.org/ns/did/v1, alongside v1.1) and libforge
eb26d871 -> 2b55dbcf (fil-forge/libforge#43, attested did:web signatures).

Without the ucantone bump, piri rejects DID documents whose `@context`
is the older v1 form with:

  publishing location commitment: unable to resolve "did:web:delegator":
  parsing DID document JSON: @context must list ".../did/v1.1" first

This blocks /blob/accept (and therefore every upload) against services
that still serve a v1-context did:web document (e.g. the delegator),
which is what the smelt e2e suite hit once the attested-signature login
fix (libforge#43) was in place.

Dependency-only change: builds, vets, and the fx app graph validation
(pkg/fx/app) pass unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yUxrDH8gWAA4ufwzuq657
frrist pushed a commit to fil-forge/indexing-service that referenced this pull request Jul 2, 2026
Bump ucantone 7985ec0 -> a8f24fe (accepts the DID Core v1 `@context`
alongside v1.1) and libforge eb26d871 -> 2b55dbcf (fil-forge/libforge#43,
attested did:web signatures).

Part of the cross-service rollout for the attested-signatures / UCAN
Principal Clarification migration: any service that resolves did:web
documents needs the ucantone bump to accept v1-context DID documents,
and the libforge#43 verifier fix for attested did:web signatures.

Dependency-only change: builds and vets clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yUxrDH8gWAA4ufwzuq657
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants