diff --git a/attestation/signer_test.go b/attestation/signer_test.go index e9e5d09..3c6f309 100644 --- a/attestation/signer_test.go +++ b/attestation/signer_test.go @@ -2,6 +2,7 @@ package attestation_test import ( "bytes" + "context" "testing" "github.com/ipfs/go-cid" @@ -21,6 +22,58 @@ import ( "github.com/fil-forge/libforge/testutil" ) +// TestChainedAttestation verifies that attested signatures chain correctly when +// the authority is itself a did:mailto attested by a root key. In other words, +// this demonstrates that the signature is validated as an invocation using the +// same DID resolvers and verifier factories that the top `ValidateToken` got. +func TestChainedAttestation(t *testing.T) { + // root signs with real ed25519 keys (did:key) + root := testutil.RandomIssuer(t) + + // service is a did:mailto DID that signs via attestation from root + serviceDID, err := did.Parse("did:mailto:service.example.com:svc") + require.NoError(t, err) + service := attestation.Attest(t.Context(), serviceDID, root) + + // alice is a did:mailto DID that signs via attestation from service + aliceDID, err := did.Parse("did:mailto:example.com:alice") + require.NoError(t, err) + alice := attestation.Attest(t.Context(), aliceDID, service) + + del, err := delegation.Delegate( + alice, + testutil.RandomDID(t), + alice.DID(), + command.MustParse("/example/command"), + ) + require.NoError(t, err) + + encoded, err := delegation.Encode(del) + require.NoError(t, err) + + decoded, err := delegation.Decode(encoded) + require.NoError(t, err) + + // The resolver needs to know both mailto DIDs with their distinct authorities. + resolver := did.ResolverMap{ + "key": key.Resolver, + "mailto": did.ResolverFunc(func(ctx context.Context, d did.DID) (did.Document, error) { + authority := serviceDID // default: alice and others attested by service + if d == serviceDID { + authority = root.DID() // service itself is attested by root + } + return didmailto.NewResolver(authority).Resolve(ctx, d) + }), + } + factories := validator.DefaultFactories() + factories[attestation.Type] = attestation.NewVerifierFactory(resolver, factories) + err = validator.ValidateToken(t.Context(), decoded, + validator.WithDIDResolver(resolver), + validator.WithVerifierFactories(factories), + ) + require.NoError(t, err) +} + func TestSigner(t *testing.T) { authority := testutil.RandomIssuer(t) alice, err := did.Parse("did:mailto:example.com:alice") diff --git a/attestation/verificationmethod.go b/attestation/verificationmethod.go index 0d55eec..4742de2 100644 --- a/attestation/verificationmethod.go +++ b/attestation/verificationmethod.go @@ -39,7 +39,10 @@ func NewVerifierFactory(resolver did.Resolver, verifierFactories map[string]vali if err != nil { return nil, fmt.Errorf("failed to derive multi-verifier: %w", err) } - return AttestedVerifier(ctx, authorityDid, v), nil + return AttestedVerifier(ctx, authorityDid, v, + validator.WithDIDResolver(resolver), + validator.WithVerifierFactories(verifierFactories), + ), nil } } diff --git a/attestation/verifier.go b/attestation/verifier.go index e2e0fee..ef40fa3 100644 --- a/attestation/verifier.go +++ b/attestation/verifier.go @@ -21,6 +21,7 @@ type Verifier struct { ctx context.Context authorityID did.DID authorityVerifier ucan.Verifier + validationOptions []validator.Option } var _ ucan.Verifier = Verifier{} @@ -54,17 +55,18 @@ func (v Verifier) Verify(msg []byte, sig []byte) bool { return false } - if validator.ValidateInvocation(v.ctx, inv) != nil { + if validator.ValidateInvocation(v.ctx, inv, v.validationOptions...) != nil { return false } return true } -func AttestedVerifier(ctx context.Context, authorityID did.DID, authority ucan.Verifier) ucan.Verifier { +func AttestedVerifier(ctx context.Context, authorityID did.DID, authority ucan.Verifier, options ...validator.Option) ucan.Verifier { return Verifier{ ctx: ctx, authorityID: authorityID, authorityVerifier: authority, + validationOptions: options, } }