Skip to content

Commit 7e965d8

Browse files
frristclaude
andcommitted
fix(didresolver): wrap resolved verifier as requested DID
`token.VerifySignature` in `ucantone/ucan/token/token.go` compares `tok.Issuer()` against `verifier.DID()` before checking signature bytes. HTTPResolver and MapResolver previously returned an unwrapped did:key verifier when asked to resolve a did:web — so the equality check failed and signatures were rejected with `InvalidSignature` before they were ever examined. Wrap the underlying did:key verifier so its DID() matches the originally requested DID. did:key inputs to MapResolver remain unwrapped (the keys are already self-describing). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0f7fa19 commit 7e965d8

6 files changed

Lines changed: 68 additions & 18 deletions

File tree

didresolver/cacheresolver_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -271,36 +271,36 @@ func TestCachedResolver_WithMapResolver(t *testing.T) {
271271
// Test alice
272272
aliceDID, err := did.Parse("did:web:alice.example.com")
273273
require.NoError(t, err)
274-
aliceKey, err := verifier.Parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")
275-
require.NoError(t, err)
274+
275+
// MapResolver now wraps verifiers as the requested DID — see
276+
// ucantone/ucan/token/token.go for why. The cached verifier's DID()
277+
// should match the input DID, not the underlying did:key.
276278

277279
// First call - should hit MapResolver
278280
result1, err1 := cachedResolver.Resolve(t.Context(), aliceDID)
279281
require.Nil(t, err1)
280-
require.Equal(t, aliceKey, result1)
282+
require.Equal(t, aliceDID, result1.DID())
281283

282284
// Second call - should use cache (we can't directly verify this without instrumentation)
283285
result2, err2 := cachedResolver.Resolve(t.Context(), aliceDID)
284286
require.Nil(t, err2)
285-
require.Equal(t, aliceKey, result2)
287+
require.Equal(t, aliceDID, result2.DID())
286288

287289
// Test bob while alice is still cached
288290
bobDID, err := did.Parse("did:web:bob.example.com")
289291
require.NoError(t, err)
290-
bobKey, err := verifier.Parse("did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6")
291-
require.NoError(t, err)
292292

293293
result3, err3 := cachedResolver.Resolve(t.Context(), bobDID)
294294
require.Nil(t, err3)
295-
require.Equal(t, bobKey, result3)
295+
require.Equal(t, bobDID, result3.DID())
296296

297297
// Wait for cache to expire
298298
time.Sleep(250 * time.Millisecond)
299299

300300
// Alice's entry should have expired, this should hit MapResolver again
301301
result4, err4 := cachedResolver.Resolve(t.Context(), aliceDID)
302302
require.Nil(t, err4)
303-
require.Equal(t, aliceKey, result4)
303+
require.Equal(t, aliceDID, result4.DID())
304304

305305
// Test non-existent DID
306306
unknownDID, err := did.Parse("did:web:unknown.example.com")

didresolver/httpresolver.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/fil-forge/ucantone/did"
1313
"github.com/fil-forge/ucantone/principal/ed25519/verifier"
14+
pverifier "github.com/fil-forge/ucantone/principal/verifier"
1415
"github.com/fil-forge/ucantone/ucan"
1516
verrs "github.com/fil-forge/ucantone/validator/errors"
1617
"github.com/gobwas/glob"
@@ -223,7 +224,16 @@ func (r *HTTPResolver) Resolve(ctx context.Context, input did.DID) (ucan.Verifie
223224
return nil, verrs.NewDIDKeyResolutionError(input, fmt.Errorf("parsing multibase key: %w", err))
224225
}
225226

226-
return didKey, nil
227+
// token.VerifySignature compares the token's Issuer DID against the
228+
// verifier's DID — if the issuer is did:web:foo and we return an unwrapped
229+
// did:key verifier, that equality check fails and the signature is
230+
// rejected before the bytes are even examined. Wrap so the verifier
231+
// announces the originally-requested DID.
232+
wrapped, err := pverifier.Wrap(didKey, input)
233+
if err != nil {
234+
return nil, verrs.NewDIDKeyResolutionError(input, fmt.Errorf("wrapping verifier as %s: %w", input, err))
235+
}
236+
return wrapped, nil
227237
}
228238

229239
func fetchDIDDocument(ctx context.Context, endpoint url.URL) (*Document, error) {

didresolver/httpresolver_test.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/fil-forge/libforge/didresolver"
1414
"github.com/fil-forge/ucantone/did"
15+
"github.com/fil-forge/ucantone/principal"
1516
"github.com/stretchr/testify/require"
1617
)
1718

@@ -279,9 +280,22 @@ func TestHTTPResolver_ResolveDIDKey(t *testing.T) {
279280
}
280281
} else {
281282
require.Nil(t, unresolvedErr)
282-
expectedDID, err := did.Parse(tc.expectedDIDKey)
283+
// The resolver wraps the underlying did:key verifier so it
284+
// announces the originally-requested DID — required for
285+
// ucantone token.VerifySignature, which compares the token's
286+
// issuer DID against the verifier's DID before checking
287+
// signature bytes.
288+
require.Equal(t, inputDID, result.DID())
289+
// expectedDIDKey identifies the underlying did:key the
290+
// resolver should have extracted from the document; reach
291+
// through Unwrap() to assert it.
292+
expectedDIDKey, err := did.Parse(tc.expectedDIDKey)
283293
require.NoError(t, err)
284-
require.Equal(t, expectedDID, result.DID())
294+
unwrapper, ok := result.(interface {
295+
Unwrap() principal.Verifier
296+
})
297+
require.True(t, ok, "resolver should return a wrapped verifier")
298+
require.Equal(t, expectedDIDKey, unwrapper.Unwrap().DID())
285299
}
286300
})
287301
}
@@ -505,9 +519,16 @@ func TestHTTPResolver_ResolveDIDKey_ContextFormats(t *testing.T) {
505519
result, unresolvedErr := resolver.Resolve(t.Context(), didWeb)
506520
require.Nil(t, unresolvedErr)
507521

508-
expectedDID, err := did.Parse(tc.expectedDIDKey)
522+
// Resolver wraps the underlying did:key as the requested did:web —
523+
// see ucantone/ucan/token/token.go for why this matters.
524+
require.Equal(t, didWeb, result.DID())
525+
expectedDIDKey, err := did.Parse(tc.expectedDIDKey)
509526
require.NoError(t, err)
510-
require.Equal(t, expectedDID, result.DID())
527+
unwrapper, ok := result.(interface {
528+
Unwrap() principal.Verifier
529+
})
530+
require.True(t, ok, "resolver should return a wrapped verifier")
531+
require.Equal(t, expectedDIDKey, unwrapper.Unwrap().DID())
511532
})
512533
}
513534
}

didresolver/mapresolver.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/fil-forge/ucantone/did"
88
"github.com/fil-forge/ucantone/principal/ed25519/verifier"
9+
pverifier "github.com/fil-forge/ucantone/principal/verifier"
910
"github.com/fil-forge/ucantone/ucan"
1011
verrs "github.com/fil-forge/ucantone/validator/errors"
1112
)
@@ -33,10 +34,23 @@ func NewMapResolver(smap map[string]string) (*MapResolver, error) {
3334
return nil, err
3435
}
3536
// TODO: multiple verification methods when https://github.com/fil-forge/ucantone/pull/7 lands
36-
dv, err := verifier.Parse(v)
37+
didKey, err := verifier.Parse(v)
3738
if err != nil {
3839
return nil, err
3940
}
41+
// token.VerifySignature compares the token's Issuer DID against the
42+
// verifier's DID. If a did:web (or any non-key DID) maps to a did:key
43+
// verifier, the equality check fails and signature verification is
44+
// rejected before the bytes are even examined. Wrap the verifier so
45+
// it announces the requested DID. did:key inputs are stored unwrapped.
46+
var dv ucan.Verifier = didKey
47+
if dk.Method() != "key" {
48+
wrapped, err := pverifier.Wrap(didKey, dk)
49+
if err != nil {
50+
return nil, fmt.Errorf("wrapping verifier as %s: %w", dk, err)
51+
}
52+
dv = wrapped
53+
}
4054
dmap[dk] = dv
4155
}
4256
return &MapResolver{Mapping: dmap}, nil

didresolver/mapresolver_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ func TestPrincipalResolver(t *testing.T) {
2222

2323
resolved, err := ppr.Resolve(t.Context(), p0)
2424
require.NoError(t, err)
25-
require.Equal(t, r, resolved.DID())
25+
// Resolver wraps the underlying did:key verifier so it announces the
26+
// requested did:web — required for ucantone token.VerifySignature, which
27+
// compares issuer DID against verifier DID before checking signature bytes.
28+
require.Equal(t, p0, resolved.DID())
2629

2730
// cannot resolve DID not in mapping
2831
_, err = ppr.Resolve(t.Context(), p1)

didresolver/tieredresolver_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,15 +154,17 @@ func TestTieredResolver_ResolveDIDKey(t *testing.T) {
154154
Tiers: []didresolver.DIDVerifierResolverFunc{mapA.Resolve, mapB.Resolve},
155155
}
156156

157-
// Resolves via the first tier
157+
// Resolves via the first tier. MapResolver wraps the did:key verifier
158+
// as the requested did:web so token.VerifySignature's issuer-vs-verifier
159+
// DID equality check passes — see ucantone/ucan/token/token.go.
158160
resA, err := resolver.Resolve(t.Context(), didA)
159161
require.NoError(t, err)
160-
require.Equal(t, keyA, resA)
162+
require.Equal(t, didA, resA.DID())
161163

162164
// Falls through to the second tier
163165
resB, err := resolver.Resolve(t.Context(), didB)
164166
require.NoError(t, err)
165-
require.Equal(t, keyB, resB)
167+
require.Equal(t, didB, resB.DID())
166168

167169
// Not resolvable by any tier
168170
_, err = resolver.Resolve(t.Context(), didC)

0 commit comments

Comments
 (0)