diff --git a/didresolver/cacheresolver_test.go b/didresolver/cacheresolver_test.go index 07b9bc6..fb4a665 100644 --- a/didresolver/cacheresolver_test.go +++ b/didresolver/cacheresolver_test.go @@ -271,28 +271,28 @@ func TestCachedResolver_WithMapResolver(t *testing.T) { // Test alice aliceDID, err := did.Parse("did:web:alice.example.com") require.NoError(t, err) - aliceKey, err := verifier.Parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK") - require.NoError(t, err) + + // MapResolver now wraps verifiers as the requested DID — see + // ucantone/ucan/token/token.go for why. The cached verifier's DID() + // should match the input DID, not the underlying did:key. // First call - should hit MapResolver result1, err1 := cachedResolver.Resolve(t.Context(), aliceDID) require.Nil(t, err1) - require.Equal(t, aliceKey, result1) + require.Equal(t, aliceDID, result1.DID()) // Second call - should use cache (we can't directly verify this without instrumentation) result2, err2 := cachedResolver.Resolve(t.Context(), aliceDID) require.Nil(t, err2) - require.Equal(t, aliceKey, result2) + require.Equal(t, aliceDID, result2.DID()) // Test bob while alice is still cached bobDID, err := did.Parse("did:web:bob.example.com") require.NoError(t, err) - bobKey, err := verifier.Parse("did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6") - require.NoError(t, err) result3, err3 := cachedResolver.Resolve(t.Context(), bobDID) require.Nil(t, err3) - require.Equal(t, bobKey, result3) + require.Equal(t, bobDID, result3.DID()) // Wait for cache to expire time.Sleep(250 * time.Millisecond) @@ -300,7 +300,7 @@ func TestCachedResolver_WithMapResolver(t *testing.T) { // Alice's entry should have expired, this should hit MapResolver again result4, err4 := cachedResolver.Resolve(t.Context(), aliceDID) require.Nil(t, err4) - require.Equal(t, aliceKey, result4) + require.Equal(t, aliceDID, result4.DID()) // Test non-existent DID unknownDID, err := did.Parse("did:web:unknown.example.com") diff --git a/didresolver/httpresolver.go b/didresolver/httpresolver.go index 6db6347..e81aa63 100644 --- a/didresolver/httpresolver.go +++ b/didresolver/httpresolver.go @@ -11,6 +11,7 @@ import ( "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/principal/ed25519/verifier" + pverifier "github.com/fil-forge/ucantone/principal/verifier" "github.com/fil-forge/ucantone/ucan" verrs "github.com/fil-forge/ucantone/validator/errors" "github.com/gobwas/glob" @@ -223,7 +224,16 @@ func (r *HTTPResolver) Resolve(ctx context.Context, input did.DID) (ucan.Verifie return nil, verrs.NewDIDKeyResolutionError(input, fmt.Errorf("parsing multibase key: %w", err)) } - return didKey, nil + // token.VerifySignature compares the token's Issuer DID against the + // verifier's DID — if the issuer is did:web:foo and we return an unwrapped + // did:key verifier, that equality check fails and the signature is + // rejected before the bytes are even examined. Wrap so the verifier + // announces the originally-requested DID. + wrapped, err := pverifier.Wrap(didKey, input) + if err != nil { + return nil, verrs.NewDIDKeyResolutionError(input, fmt.Errorf("wrapping verifier as %s: %w", input, err)) + } + return wrapped, nil } func fetchDIDDocument(ctx context.Context, endpoint url.URL) (*Document, error) { diff --git a/didresolver/httpresolver_test.go b/didresolver/httpresolver_test.go index 9c00595..0abb3a4 100644 --- a/didresolver/httpresolver_test.go +++ b/didresolver/httpresolver_test.go @@ -12,6 +12,7 @@ import ( "github.com/fil-forge/libforge/didresolver" "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/principal" "github.com/stretchr/testify/require" ) @@ -279,9 +280,22 @@ func TestHTTPResolver_ResolveDIDKey(t *testing.T) { } } else { require.Nil(t, unresolvedErr) - expectedDID, err := did.Parse(tc.expectedDIDKey) + // The resolver wraps the underlying did:key verifier so it + // announces the originally-requested DID — required for + // ucantone token.VerifySignature, which compares the token's + // issuer DID against the verifier's DID before checking + // signature bytes. + require.Equal(t, inputDID, result.DID()) + // expectedDIDKey identifies the underlying did:key the + // resolver should have extracted from the document; reach + // through Unwrap() to assert it. + expectedDIDKey, err := did.Parse(tc.expectedDIDKey) require.NoError(t, err) - require.Equal(t, expectedDID, result.DID()) + unwrapper, ok := result.(interface { + Unwrap() principal.Verifier + }) + require.True(t, ok, "resolver should return a wrapped verifier") + require.Equal(t, expectedDIDKey, unwrapper.Unwrap().DID()) } }) } @@ -505,9 +519,16 @@ func TestHTTPResolver_ResolveDIDKey_ContextFormats(t *testing.T) { result, unresolvedErr := resolver.Resolve(t.Context(), didWeb) require.Nil(t, unresolvedErr) - expectedDID, err := did.Parse(tc.expectedDIDKey) + // Resolver wraps the underlying did:key as the requested did:web — + // see ucantone/ucan/token/token.go for why this matters. + require.Equal(t, didWeb, result.DID()) + expectedDIDKey, err := did.Parse(tc.expectedDIDKey) require.NoError(t, err) - require.Equal(t, expectedDID, result.DID()) + unwrapper, ok := result.(interface { + Unwrap() principal.Verifier + }) + require.True(t, ok, "resolver should return a wrapped verifier") + require.Equal(t, expectedDIDKey, unwrapper.Unwrap().DID()) }) } } diff --git a/didresolver/mapresolver.go b/didresolver/mapresolver.go index a947e3a..6adffae 100644 --- a/didresolver/mapresolver.go +++ b/didresolver/mapresolver.go @@ -6,6 +6,7 @@ import ( "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/principal/ed25519/verifier" + pverifier "github.com/fil-forge/ucantone/principal/verifier" "github.com/fil-forge/ucantone/ucan" verrs "github.com/fil-forge/ucantone/validator/errors" ) @@ -33,10 +34,23 @@ func NewMapResolver(smap map[string]string) (*MapResolver, error) { return nil, err } // TODO: multiple verification methods when https://github.com/fil-forge/ucantone/pull/7 lands - dv, err := verifier.Parse(v) + didKey, err := verifier.Parse(v) if err != nil { return nil, err } + // token.VerifySignature compares the token's Issuer DID against the + // verifier's DID. If a did:web (or any non-key DID) maps to a did:key + // verifier, the equality check fails and signature verification is + // rejected before the bytes are even examined. Wrap the verifier so + // it announces the requested DID. did:key inputs are stored unwrapped. + var dv ucan.Verifier = didKey + if dk.Method() != "key" { + wrapped, err := pverifier.Wrap(didKey, dk) + if err != nil { + return nil, fmt.Errorf("wrapping verifier as %s: %w", dk, err) + } + dv = wrapped + } dmap[dk] = dv } return &MapResolver{Mapping: dmap}, nil diff --git a/didresolver/mapresolver_test.go b/didresolver/mapresolver_test.go index 4dba48d..5fa39d7 100644 --- a/didresolver/mapresolver_test.go +++ b/didresolver/mapresolver_test.go @@ -22,7 +22,10 @@ func TestPrincipalResolver(t *testing.T) { resolved, err := ppr.Resolve(t.Context(), p0) require.NoError(t, err) - require.Equal(t, r, resolved.DID()) + // Resolver wraps the underlying did:key verifier so it announces the + // requested did:web — required for ucantone token.VerifySignature, which + // compares issuer DID against verifier DID before checking signature bytes. + require.Equal(t, p0, resolved.DID()) // cannot resolve DID not in mapping _, err = ppr.Resolve(t.Context(), p1) diff --git a/didresolver/tieredresolver_test.go b/didresolver/tieredresolver_test.go index 714455e..559826c 100644 --- a/didresolver/tieredresolver_test.go +++ b/didresolver/tieredresolver_test.go @@ -154,15 +154,17 @@ func TestTieredResolver_ResolveDIDKey(t *testing.T) { Tiers: []didresolver.DIDVerifierResolverFunc{mapA.Resolve, mapB.Resolve}, } - // Resolves via the first tier + // Resolves via the first tier. MapResolver wraps the did:key verifier + // as the requested did:web so token.VerifySignature's issuer-vs-verifier + // DID equality check passes — see ucantone/ucan/token/token.go. resA, err := resolver.Resolve(t.Context(), didA) require.NoError(t, err) - require.Equal(t, keyA, resA) + require.Equal(t, didA, resA.DID()) // Falls through to the second tier resB, err := resolver.Resolve(t.Context(), didB) require.NoError(t, err) - require.Equal(t, keyB, resB) + require.Equal(t, didB, resB.DID()) // Not resolvable by any tier _, err = resolver.Resolve(t.Context(), didC)