A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
at refactor 214 lines 6.9 kB view raw
1// Package atproto implements a Ratify verifier plugin for ATProto signatures. 2package atproto 3 4import ( 5 "context" 6 "encoding/json" 7 "fmt" 8 "time" 9 10 "github.com/ratify-project/ratify/pkg/common" 11 "github.com/ratify-project/ratify/pkg/ocispecs" 12 "github.com/ratify-project/ratify/pkg/referrerstore" 13 "github.com/ratify-project/ratify/pkg/verifier" 14) 15 16const ( 17 // VerifierName is the name of this verifier 18 VerifierName = "atproto" 19 20 // VerifierType is the type of this verifier 21 VerifierType = "atproto" 22 23 // ATProtoSignatureArtifactType is the OCI artifact type for ATProto signatures 24 ATProtoSignatureArtifactType = "application/vnd.atproto.signature.v1+json" 25) 26 27// ATProtoVerifier implements the Ratify ReferenceVerifier interface for ATProto signatures. 28type ATProtoVerifier struct { 29 name string 30 config ATProtoConfig 31 resolver *Resolver 32 verifier *SignatureVerifier 33 trustStore *TrustStore 34} 35 36// ATProtoConfig holds configuration for the ATProto verifier. 37type ATProtoConfig struct { 38 // TrustPolicyPath is the path to the trust policy YAML file 39 TrustPolicyPath string `json:"trustPolicyPath"` 40 41 // DIDResolverTimeout is the timeout for DID resolution 42 DIDResolverTimeout time.Duration `json:"didResolverTimeout"` 43 44 // PDSTimeout is the timeout for PDS XRPC calls 45 PDSTimeout time.Duration `json:"pdsTimeout"` 46 47 // CacheEnabled enables caching of DID documents and public keys 48 CacheEnabled bool `json:"cacheEnabled"` 49 50 // CacheTTL is the cache TTL for DID documents and public keys 51 CacheTTL time.Duration `json:"cacheTTL"` 52} 53 54// ATProtoSignature represents the ATProto signature metadata stored in the OCI artifact. 55type ATProtoSignature struct { 56 Type string `json:"$type"` 57 Version string `json:"version"` 58 Subject struct { 59 Digest string `json:"digest"` 60 MediaType string `json:"mediaType"` 61 } `json:"subject"` 62 ATProto struct { 63 DID string `json:"did"` 64 Handle string `json:"handle"` 65 PDSEndpoint string `json:"pdsEndpoint"` 66 RecordURI string `json:"recordUri"` 67 CommitCID string `json:"commitCid"` 68 SignedAt time.Time `json:"signedAt"` 69 } `json:"atproto"` 70 Signature struct { 71 Algorithm string `json:"algorithm"` 72 KeyID string `json:"keyId"` 73 PublicKeyMultibase string `json:"publicKeyMultibase"` 74 } `json:"signature"` 75} 76 77// NewATProtoVerifier creates a new ATProto verifier instance. 78func NewATProtoVerifier(name string, config ATProtoConfig) (*ATProtoVerifier, error) { 79 // Load trust policy 80 trustStore, err := LoadTrustStore(config.TrustPolicyPath) 81 if err != nil { 82 return nil, fmt.Errorf("failed to load trust policy: %w", err) 83 } 84 85 // Create resolver with caching 86 resolver := NewResolver(config.DIDResolverTimeout, config.CacheEnabled, config.CacheTTL) 87 88 // Create signature verifier 89 verifier := NewSignatureVerifier(config.PDSTimeout) 90 91 return &ATProtoVerifier{ 92 name: name, 93 config: config, 94 resolver: resolver, 95 verifier: verifier, 96 trustStore: trustStore, 97 }, nil 98} 99 100// Name returns the name of this verifier. 101func (v *ATProtoVerifier) Name() string { 102 return v.name 103} 104 105// Type returns the type of this verifier. 106func (v *ATProtoVerifier) Type() string { 107 return VerifierType 108} 109 110// CanVerify returns true if this verifier can verify the given artifact type. 111func (v *ATProtoVerifier) CanVerify(artifactType string) bool { 112 return artifactType == ATProtoSignatureArtifactType 113} 114 115// VerifyReference verifies an ATProto signature artifact. 116func (v *ATProtoVerifier) VerifyReference( 117 ctx context.Context, 118 subjectRef common.Reference, 119 referenceDesc ocispecs.ReferenceDescriptor, 120 store referrerstore.ReferrerStore, 121) (verifier.VerifierResult, error) { 122 // 1. Fetch signature blob from store 123 sigBlob, err := store.GetBlobContent(ctx, subjectRef, referenceDesc.Digest) 124 if err != nil { 125 return v.failureResult(fmt.Sprintf("failed to fetch signature blob: %v", err)), err 126 } 127 128 // 2. Parse ATProto signature metadata 129 var sigData ATProtoSignature 130 if err := json.Unmarshal(sigBlob, &sigData); err != nil { 131 return v.failureResult(fmt.Sprintf("failed to parse signature metadata: %v", err)), err 132 } 133 134 // Validate signature format 135 if err := v.validateSignature(&sigData); err != nil { 136 return v.failureResult(fmt.Sprintf("invalid signature format: %v", err)), err 137 } 138 139 // 3. Check trust policy first (fail fast if DID not trusted) 140 if !v.trustStore.IsTrusted(sigData.ATProto.DID, time.Now()) { 141 return v.failureResult(fmt.Sprintf("DID %s not in trusted list", sigData.ATProto.DID)), 142 fmt.Errorf("untrusted DID") 143 } 144 145 // 4. Resolve DID to public key 146 pubKey, err := v.resolver.ResolveDIDToPublicKey(ctx, sigData.ATProto.DID) 147 if err != nil { 148 return v.failureResult(fmt.Sprintf("failed to resolve DID: %v", err)), err 149 } 150 151 // 5. Fetch repository commit from PDS 152 commit, err := v.verifier.FetchCommit(ctx, sigData.ATProto.PDSEndpoint, 153 sigData.ATProto.DID, sigData.ATProto.CommitCID) 154 if err != nil { 155 return v.failureResult(fmt.Sprintf("failed to fetch commit: %v", err)), err 156 } 157 158 // 6. Verify K-256 signature 159 if err := v.verifier.VerifySignature(pubKey, commit); err != nil { 160 return v.failureResult(fmt.Sprintf("signature verification failed: %v", err)), err 161 } 162 163 // 7. Success - return detailed result 164 return verifier.VerifierResult{ 165 IsSuccess: true, 166 Name: v.name, 167 Type: v.Type(), 168 Message: fmt.Sprintf("Successfully verified ATProto signature for DID %s", sigData.ATProto.DID), 169 Extensions: map[string]interface{}{ 170 "did": sigData.ATProto.DID, 171 "handle": sigData.ATProto.Handle, 172 "signedAt": sigData.ATProto.SignedAt, 173 "commitCid": sigData.ATProto.CommitCID, 174 "pdsEndpoint": sigData.ATProto.PDSEndpoint, 175 }, 176 }, nil 177} 178 179// validateSignature validates the signature metadata format. 180func (v *ATProtoVerifier) validateSignature(sig *ATProtoSignature) error { 181 if sig.Type != "io.atcr.atproto.signature" { 182 return fmt.Errorf("invalid signature type: %s", sig.Type) 183 } 184 if sig.ATProto.DID == "" { 185 return fmt.Errorf("missing DID") 186 } 187 if sig.ATProto.PDSEndpoint == "" { 188 return fmt.Errorf("missing PDS endpoint") 189 } 190 if sig.ATProto.CommitCID == "" { 191 return fmt.Errorf("missing commit CID") 192 } 193 if sig.Signature.Algorithm != "ECDSA-K256-SHA256" { 194 return fmt.Errorf("unsupported signature algorithm: %s", sig.Signature.Algorithm) 195 } 196 return nil 197} 198 199// failureResult creates a failure result with the given message. 200func (v *ATProtoVerifier) failureResult(message string) verifier.VerifierResult { 201 return verifier.VerifierResult{ 202 IsSuccess: false, 203 Name: v.name, 204 Type: v.Type(), 205 Message: message, 206 Extensions: map[string]interface{}{ 207 "error": message, 208 }, 209 } 210} 211 212// TODO: Implement resolver.go with DID resolution logic 213// TODO: Implement crypto.go with K-256 signature verification 214// TODO: Implement config.go with trust policy loading