// Package atproto implements a Ratify verifier plugin for ATProto signatures. package atproto import ( "context" "encoding/json" "fmt" "time" "github.com/ratify-project/ratify/pkg/common" "github.com/ratify-project/ratify/pkg/ocispecs" "github.com/ratify-project/ratify/pkg/referrerstore" "github.com/ratify-project/ratify/pkg/verifier" ) const ( // VerifierName is the name of this verifier VerifierName = "atproto" // VerifierType is the type of this verifier VerifierType = "atproto" // ATProtoSignatureArtifactType is the OCI artifact type for ATProto signatures ATProtoSignatureArtifactType = "application/vnd.atproto.signature.v1+json" ) // ATProtoVerifier implements the Ratify ReferenceVerifier interface for ATProto signatures. type ATProtoVerifier struct { name string config ATProtoConfig resolver *Resolver verifier *SignatureVerifier trustStore *TrustStore } // ATProtoConfig holds configuration for the ATProto verifier. type ATProtoConfig struct { // TrustPolicyPath is the path to the trust policy YAML file TrustPolicyPath string `json:"trustPolicyPath"` // DIDResolverTimeout is the timeout for DID resolution DIDResolverTimeout time.Duration `json:"didResolverTimeout"` // PDSTimeout is the timeout for PDS XRPC calls PDSTimeout time.Duration `json:"pdsTimeout"` // CacheEnabled enables caching of DID documents and public keys CacheEnabled bool `json:"cacheEnabled"` // CacheTTL is the cache TTL for DID documents and public keys CacheTTL time.Duration `json:"cacheTTL"` } // ATProtoSignature represents the ATProto signature metadata stored in the OCI artifact. type ATProtoSignature struct { Type string `json:"$type"` Version string `json:"version"` Subject struct { Digest string `json:"digest"` MediaType string `json:"mediaType"` } `json:"subject"` ATProto struct { DID string `json:"did"` Handle string `json:"handle"` PDSEndpoint string `json:"pdsEndpoint"` RecordURI string `json:"recordUri"` CommitCID string `json:"commitCid"` SignedAt time.Time `json:"signedAt"` } `json:"atproto"` Signature struct { Algorithm string `json:"algorithm"` KeyID string `json:"keyId"` PublicKeyMultibase string `json:"publicKeyMultibase"` } `json:"signature"` } // NewATProtoVerifier creates a new ATProto verifier instance. func NewATProtoVerifier(name string, config ATProtoConfig) (*ATProtoVerifier, error) { // Load trust policy trustStore, err := LoadTrustStore(config.TrustPolicyPath) if err != nil { return nil, fmt.Errorf("failed to load trust policy: %w", err) } // Create resolver with caching resolver := NewResolver(config.DIDResolverTimeout, config.CacheEnabled, config.CacheTTL) // Create signature verifier verifier := NewSignatureVerifier(config.PDSTimeout) return &ATProtoVerifier{ name: name, config: config, resolver: resolver, verifier: verifier, trustStore: trustStore, }, nil } // Name returns the name of this verifier. func (v *ATProtoVerifier) Name() string { return v.name } // Type returns the type of this verifier. func (v *ATProtoVerifier) Type() string { return VerifierType } // CanVerify returns true if this verifier can verify the given artifact type. func (v *ATProtoVerifier) CanVerify(artifactType string) bool { return artifactType == ATProtoSignatureArtifactType } // VerifyReference verifies an ATProto signature artifact. func (v *ATProtoVerifier) VerifyReference( ctx context.Context, subjectRef common.Reference, referenceDesc ocispecs.ReferenceDescriptor, store referrerstore.ReferrerStore, ) (verifier.VerifierResult, error) { // 1. Fetch signature blob from store sigBlob, err := store.GetBlobContent(ctx, subjectRef, referenceDesc.Digest) if err != nil { return v.failureResult(fmt.Sprintf("failed to fetch signature blob: %v", err)), err } // 2. Parse ATProto signature metadata var sigData ATProtoSignature if err := json.Unmarshal(sigBlob, &sigData); err != nil { return v.failureResult(fmt.Sprintf("failed to parse signature metadata: %v", err)), err } // Validate signature format if err := v.validateSignature(&sigData); err != nil { return v.failureResult(fmt.Sprintf("invalid signature format: %v", err)), err } // 3. Check trust policy first (fail fast if DID not trusted) if !v.trustStore.IsTrusted(sigData.ATProto.DID, time.Now()) { return v.failureResult(fmt.Sprintf("DID %s not in trusted list", sigData.ATProto.DID)), fmt.Errorf("untrusted DID") } // 4. Resolve DID to public key pubKey, err := v.resolver.ResolveDIDToPublicKey(ctx, sigData.ATProto.DID) if err != nil { return v.failureResult(fmt.Sprintf("failed to resolve DID: %v", err)), err } // 5. Fetch repository commit from PDS commit, err := v.verifier.FetchCommit(ctx, sigData.ATProto.PDSEndpoint, sigData.ATProto.DID, sigData.ATProto.CommitCID) if err != nil { return v.failureResult(fmt.Sprintf("failed to fetch commit: %v", err)), err } // 6. Verify K-256 signature if err := v.verifier.VerifySignature(pubKey, commit); err != nil { return v.failureResult(fmt.Sprintf("signature verification failed: %v", err)), err } // 7. Success - return detailed result return verifier.VerifierResult{ IsSuccess: true, Name: v.name, Type: v.Type(), Message: fmt.Sprintf("Successfully verified ATProto signature for DID %s", sigData.ATProto.DID), Extensions: map[string]interface{}{ "did": sigData.ATProto.DID, "handle": sigData.ATProto.Handle, "signedAt": sigData.ATProto.SignedAt, "commitCid": sigData.ATProto.CommitCID, "pdsEndpoint": sigData.ATProto.PDSEndpoint, }, }, nil } // validateSignature validates the signature metadata format. func (v *ATProtoVerifier) validateSignature(sig *ATProtoSignature) error { if sig.Type != "io.atcr.atproto.signature" { return fmt.Errorf("invalid signature type: %s", sig.Type) } if sig.ATProto.DID == "" { return fmt.Errorf("missing DID") } if sig.ATProto.PDSEndpoint == "" { return fmt.Errorf("missing PDS endpoint") } if sig.ATProto.CommitCID == "" { return fmt.Errorf("missing commit CID") } if sig.Signature.Algorithm != "ECDSA-K256-SHA256" { return fmt.Errorf("unsupported signature algorithm: %s", sig.Signature.Algorithm) } return nil } // failureResult creates a failure result with the given message. func (v *ATProtoVerifier) failureResult(message string) verifier.VerifierResult { return verifier.VerifierResult{ IsSuccess: false, Name: v.name, Type: v.Type(), Message: message, Extensions: map[string]interface{}{ "error": message, }, } } // TODO: Implement resolver.go with DID resolution logic // TODO: Implement crypto.go with K-256 signature verification // TODO: Implement config.go with trust policy loading