···314314 } else {
315315 slog.Debug("Updated profile with hold DID", "component", "appview/callback", "hold_did", holdDID)
316316 }
317317- slog.Debug("Attempting crew registration", "component", "oauth/server", "did", did, "hold_did", holdDID)
318318- storage.EnsureCrewMembership(ctx, client, refresher, holdDID)
319317 } else {
320318 // Already a DID - use it
321319 holdDID = profile.DefaultHold
322320 }
323321 // Register crew regardless of migration (outside the migration block)
322322+ // Run in background to avoid blocking OAuth callback if hold is offline
324323 slog.Debug("Attempting crew registration", "component", "appview/callback", "did", did, "hold_did", holdDID)
325325- storage.EnsureCrewMembership(ctx, client, refresher, holdDID)
324324+ go func(ctx context.Context, client *atproto.Client, refresher *oauth.Refresher, holdDID string) {
325325+ storage.EnsureCrewMembership(ctx, client, refresher, holdDID)
326326+ }(ctx, client, refresher, holdDID)
326327327328 }
328329
+225
examples/plugins/gatekeeper-provider/main.go.temp
···11+// Package main implements an OPA Gatekeeper External Data Provider for ATProto signature verification.
22+package main
33+44+import (
55+ "context"
66+ "encoding/json"
77+ "fmt"
88+ "log"
99+ "net/http"
1010+ "os"
1111+ "time"
1212+)
1313+1414+const (
1515+ // DefaultPort is the default HTTP port
1616+ DefaultPort = "8080"
1717+1818+ // DefaultTrustPolicyPath is the default trust policy file path
1919+ DefaultTrustPolicyPath = "/config/trust-policy.yaml"
2020+)
2121+2222+// Server is the HTTP server for the external data provider.
2323+type Server struct {
2424+ verifier *Verifier
2525+ port string
2626+ httpServer *http.Server
2727+}
2828+2929+// ProviderRequest is the request format from Gatekeeper.
3030+type ProviderRequest struct {
3131+ Keys []string `json:"keys"`
3232+ Values []string `json:"values"`
3333+}
3434+3535+// ProviderResponse is the response format to Gatekeeper.
3636+type ProviderResponse struct {
3737+ SystemError string `json:"system_error,omitempty"`
3838+ Responses []map[string]interface{} `json:"responses"`
3939+}
4040+4141+// VerificationResult holds the result of verifying a single image.
4242+type VerificationResult struct {
4343+ Image string `json:"image"`
4444+ Verified bool `json:"verified"`
4545+ DID string `json:"did,omitempty"`
4646+ Handle string `json:"handle,omitempty"`
4747+ SignedAt time.Time `json:"signedAt,omitempty"`
4848+ CommitCID string `json:"commitCid,omitempty"`
4949+ Error string `json:"error,omitempty"`
5050+}
5151+5252+// NewServer creates a new provider server.
5353+func NewServer(verifier *Verifier, port string) *Server {
5454+ return &Server{
5555+ verifier: verifier,
5656+ port: port,
5757+ }
5858+}
5959+6060+// Start starts the HTTP server.
6161+func (s *Server) Start() error {
6262+ mux := http.NewServeMux()
6363+6464+ // Provider endpoint (called by Gatekeeper)
6565+ mux.HandleFunc("/provide", s.handleProvide)
6666+6767+ // Health check endpoints
6868+ mux.HandleFunc("/health", s.handleHealth)
6969+ mux.HandleFunc("/ready", s.handleReady)
7070+7171+ // Metrics endpoint (Prometheus)
7272+ // TODO: Implement metrics
7373+ // mux.HandleFunc("/metrics", s.handleMetrics)
7474+7575+ s.httpServer = &http.Server{
7676+ Addr: ":" + s.port,
7777+ Handler: mux,
7878+ ReadTimeout: 10 * time.Second,
7979+ WriteTimeout: 30 * time.Second,
8080+ IdleTimeout: 60 * time.Second,
8181+ }
8282+8383+ log.Printf("Starting ATProto signature verification provider on port %s", s.port)
8484+ return s.httpServer.ListenAndServe()
8585+}
8686+8787+// Stop gracefully stops the HTTP server.
8888+func (s *Server) Stop(ctx context.Context) error {
8989+ if s.httpServer != nil {
9090+ return s.httpServer.Shutdown(ctx)
9191+ }
9292+ return nil
9393+}
9494+9595+// handleProvide handles the provider endpoint called by Gatekeeper.
9696+func (s *Server) handleProvide(w http.ResponseWriter, r *http.Request) {
9797+ if r.Method != http.MethodPost {
9898+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
9999+ return
100100+ }
101101+102102+ // Parse request
103103+ var req ProviderRequest
104104+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
105105+ log.Printf("ERROR: failed to parse request: %v", err)
106106+ http.Error(w, fmt.Sprintf("invalid request: %v", err), http.StatusBadRequest)
107107+ return
108108+ }
109109+110110+ log.Printf("INFO: received verification request for %d images", len(req.Values))
111111+112112+ // Verify each image
113113+ responses := make([]map[string]interface{}, 0, len(req.Values))
114114+ for _, image := range req.Values {
115115+ result := s.verifyImage(r.Context(), image)
116116+ responses = append(responses, structToMap(result))
117117+ }
118118+119119+ // Send response
120120+ resp := ProviderResponse{
121121+ Responses: responses,
122122+ }
123123+124124+ w.Header().Set("Content-Type", "application/json")
125125+ if err := json.NewEncoder(w).Encode(resp); err != nil {
126126+ log.Printf("ERROR: failed to encode response: %v", err)
127127+ }
128128+}
129129+130130+// verifyImage verifies a single image.
131131+func (s *Server) verifyImage(ctx context.Context, image string) VerificationResult {
132132+ start := time.Now()
133133+ log.Printf("INFO: verifying image: %s", image)
134134+135135+ // Call verifier
136136+ verified, metadata, err := s.verifier.Verify(ctx, image)
137137+ duration := time.Since(start)
138138+139139+ if err != nil {
140140+ log.Printf("ERROR: verification failed for %s: %v (duration: %v)", image, err, duration)
141141+ return VerificationResult{
142142+ Image: image,
143143+ Verified: false,
144144+ Error: err.Error(),
145145+ }
146146+ }
147147+148148+ if !verified {
149149+ log.Printf("WARN: image %s failed verification (duration: %v)", image, duration)
150150+ return VerificationResult{
151151+ Image: image,
152152+ Verified: false,
153153+ Error: "signature verification failed",
154154+ }
155155+ }
156156+157157+ log.Printf("INFO: image %s verified successfully (DID: %s, duration: %v)",
158158+ image, metadata.DID, duration)
159159+160160+ return VerificationResult{
161161+ Image: image,
162162+ Verified: true,
163163+ DID: metadata.DID,
164164+ Handle: metadata.Handle,
165165+ SignedAt: metadata.SignedAt,
166166+ CommitCID: metadata.CommitCID,
167167+ }
168168+}
169169+170170+// handleHealth handles health check requests.
171171+func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
172172+ w.Header().Set("Content-Type", "application/json")
173173+ json.NewEncoder(w).Encode(map[string]string{
174174+ "status": "ok",
175175+ "version": "1.0.0",
176176+ })
177177+}
178178+179179+// handleReady handles readiness check requests.
180180+func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
181181+ // TODO: Check dependencies (DID resolver, PDS connectivity)
182182+ w.Header().Set("Content-Type", "application/json")
183183+ json.NewEncoder(w).Encode(map[string]string{
184184+ "status": "ready",
185185+ })
186186+}
187187+188188+// structToMap converts a struct to a map for JSON encoding.
189189+func structToMap(v interface{}) map[string]interface{} {
190190+ data, _ := json.Marshal(v)
191191+ var m map[string]interface{}
192192+ json.Unmarshal(data, &m)
193193+ return m
194194+}
195195+196196+func main() {
197197+ // Load configuration
198198+ port := os.Getenv("HTTP_PORT")
199199+ if port == "" {
200200+ port = DefaultPort
201201+ }
202202+203203+ trustPolicyPath := os.Getenv("TRUST_POLICY_PATH")
204204+ if trustPolicyPath == "" {
205205+ trustPolicyPath = DefaultTrustPolicyPath
206206+ }
207207+208208+ // Create verifier
209209+ verifier, err := NewVerifier(trustPolicyPath)
210210+ if err != nil {
211211+ log.Fatalf("FATAL: failed to create verifier: %v", err)
212212+ }
213213+214214+ // Create server
215215+ server := NewServer(verifier, port)
216216+217217+ // Start server
218218+ if err := server.Start(); err != nil && err != http.ErrServerClosed {
219219+ log.Fatalf("FATAL: server error: %v", err)
220220+ }
221221+}
222222+223223+// TODO: Implement verifier.go with ATProto signature verification logic
224224+// TODO: Implement resolver.go with DID resolution
225225+// TODO: Implement crypto.go with K-256 signature verification
+214
examples/plugins/ratify-verifier/verifier.go.temp
···11+// Package atproto implements a Ratify verifier plugin for ATProto signatures.
22+package atproto
33+44+import (
55+ "context"
66+ "encoding/json"
77+ "fmt"
88+ "time"
99+1010+ "github.com/ratify-project/ratify/pkg/common"
1111+ "github.com/ratify-project/ratify/pkg/ocispecs"
1212+ "github.com/ratify-project/ratify/pkg/referrerstore"
1313+ "github.com/ratify-project/ratify/pkg/verifier"
1414+)
1515+1616+const (
1717+ // VerifierName is the name of this verifier
1818+ VerifierName = "atproto"
1919+2020+ // VerifierType is the type of this verifier
2121+ VerifierType = "atproto"
2222+2323+ // ATProtoSignatureArtifactType is the OCI artifact type for ATProto signatures
2424+ ATProtoSignatureArtifactType = "application/vnd.atproto.signature.v1+json"
2525+)
2626+2727+// ATProtoVerifier implements the Ratify ReferenceVerifier interface for ATProto signatures.
2828+type ATProtoVerifier struct {
2929+ name string
3030+ config ATProtoConfig
3131+ resolver *Resolver
3232+ verifier *SignatureVerifier
3333+ trustStore *TrustStore
3434+}
3535+3636+// ATProtoConfig holds configuration for the ATProto verifier.
3737+type ATProtoConfig struct {
3838+ // TrustPolicyPath is the path to the trust policy YAML file
3939+ TrustPolicyPath string `json:"trustPolicyPath"`
4040+4141+ // DIDResolverTimeout is the timeout for DID resolution
4242+ DIDResolverTimeout time.Duration `json:"didResolverTimeout"`
4343+4444+ // PDSTimeout is the timeout for PDS XRPC calls
4545+ PDSTimeout time.Duration `json:"pdsTimeout"`
4646+4747+ // CacheEnabled enables caching of DID documents and public keys
4848+ CacheEnabled bool `json:"cacheEnabled"`
4949+5050+ // CacheTTL is the cache TTL for DID documents and public keys
5151+ CacheTTL time.Duration `json:"cacheTTL"`
5252+}
5353+5454+// ATProtoSignature represents the ATProto signature metadata stored in the OCI artifact.
5555+type ATProtoSignature struct {
5656+ Type string `json:"$type"`
5757+ Version string `json:"version"`
5858+ Subject struct {
5959+ Digest string `json:"digest"`
6060+ MediaType string `json:"mediaType"`
6161+ } `json:"subject"`
6262+ ATProto struct {
6363+ DID string `json:"did"`
6464+ Handle string `json:"handle"`
6565+ PDSEndpoint string `json:"pdsEndpoint"`
6666+ RecordURI string `json:"recordUri"`
6767+ CommitCID string `json:"commitCid"`
6868+ SignedAt time.Time `json:"signedAt"`
6969+ } `json:"atproto"`
7070+ Signature struct {
7171+ Algorithm string `json:"algorithm"`
7272+ KeyID string `json:"keyId"`
7373+ PublicKeyMultibase string `json:"publicKeyMultibase"`
7474+ } `json:"signature"`
7575+}
7676+7777+// NewATProtoVerifier creates a new ATProto verifier instance.
7878+func NewATProtoVerifier(name string, config ATProtoConfig) (*ATProtoVerifier, error) {
7979+ // Load trust policy
8080+ trustStore, err := LoadTrustStore(config.TrustPolicyPath)
8181+ if err != nil {
8282+ return nil, fmt.Errorf("failed to load trust policy: %w", err)
8383+ }
8484+8585+ // Create resolver with caching
8686+ resolver := NewResolver(config.DIDResolverTimeout, config.CacheEnabled, config.CacheTTL)
8787+8888+ // Create signature verifier
8989+ verifier := NewSignatureVerifier(config.PDSTimeout)
9090+9191+ return &ATProtoVerifier{
9292+ name: name,
9393+ config: config,
9494+ resolver: resolver,
9595+ verifier: verifier,
9696+ trustStore: trustStore,
9797+ }, nil
9898+}
9999+100100+// Name returns the name of this verifier.
101101+func (v *ATProtoVerifier) Name() string {
102102+ return v.name
103103+}
104104+105105+// Type returns the type of this verifier.
106106+func (v *ATProtoVerifier) Type() string {
107107+ return VerifierType
108108+}
109109+110110+// CanVerify returns true if this verifier can verify the given artifact type.
111111+func (v *ATProtoVerifier) CanVerify(artifactType string) bool {
112112+ return artifactType == ATProtoSignatureArtifactType
113113+}
114114+115115+// VerifyReference verifies an ATProto signature artifact.
116116+func (v *ATProtoVerifier) VerifyReference(
117117+ ctx context.Context,
118118+ subjectRef common.Reference,
119119+ referenceDesc ocispecs.ReferenceDescriptor,
120120+ store referrerstore.ReferrerStore,
121121+) (verifier.VerifierResult, error) {
122122+ // 1. Fetch signature blob from store
123123+ sigBlob, err := store.GetBlobContent(ctx, subjectRef, referenceDesc.Digest)
124124+ if err != nil {
125125+ return v.failureResult(fmt.Sprintf("failed to fetch signature blob: %v", err)), err
126126+ }
127127+128128+ // 2. Parse ATProto signature metadata
129129+ var sigData ATProtoSignature
130130+ if err := json.Unmarshal(sigBlob, &sigData); err != nil {
131131+ return v.failureResult(fmt.Sprintf("failed to parse signature metadata: %v", err)), err
132132+ }
133133+134134+ // Validate signature format
135135+ if err := v.validateSignature(&sigData); err != nil {
136136+ return v.failureResult(fmt.Sprintf("invalid signature format: %v", err)), err
137137+ }
138138+139139+ // 3. Check trust policy first (fail fast if DID not trusted)
140140+ if !v.trustStore.IsTrusted(sigData.ATProto.DID, time.Now()) {
141141+ return v.failureResult(fmt.Sprintf("DID %s not in trusted list", sigData.ATProto.DID)),
142142+ fmt.Errorf("untrusted DID")
143143+ }
144144+145145+ // 4. Resolve DID to public key
146146+ pubKey, err := v.resolver.ResolveDIDToPublicKey(ctx, sigData.ATProto.DID)
147147+ if err != nil {
148148+ return v.failureResult(fmt.Sprintf("failed to resolve DID: %v", err)), err
149149+ }
150150+151151+ // 5. Fetch repository commit from PDS
152152+ commit, err := v.verifier.FetchCommit(ctx, sigData.ATProto.PDSEndpoint,
153153+ sigData.ATProto.DID, sigData.ATProto.CommitCID)
154154+ if err != nil {
155155+ return v.failureResult(fmt.Sprintf("failed to fetch commit: %v", err)), err
156156+ }
157157+158158+ // 6. Verify K-256 signature
159159+ if err := v.verifier.VerifySignature(pubKey, commit); err != nil {
160160+ return v.failureResult(fmt.Sprintf("signature verification failed: %v", err)), err
161161+ }
162162+163163+ // 7. Success - return detailed result
164164+ return verifier.VerifierResult{
165165+ IsSuccess: true,
166166+ Name: v.name,
167167+ Type: v.Type(),
168168+ Message: fmt.Sprintf("Successfully verified ATProto signature for DID %s", sigData.ATProto.DID),
169169+ Extensions: map[string]interface{}{
170170+ "did": sigData.ATProto.DID,
171171+ "handle": sigData.ATProto.Handle,
172172+ "signedAt": sigData.ATProto.SignedAt,
173173+ "commitCid": sigData.ATProto.CommitCID,
174174+ "pdsEndpoint": sigData.ATProto.PDSEndpoint,
175175+ },
176176+ }, nil
177177+}
178178+179179+// validateSignature validates the signature metadata format.
180180+func (v *ATProtoVerifier) validateSignature(sig *ATProtoSignature) error {
181181+ if sig.Type != "io.atcr.atproto.signature" {
182182+ return fmt.Errorf("invalid signature type: %s", sig.Type)
183183+ }
184184+ if sig.ATProto.DID == "" {
185185+ return fmt.Errorf("missing DID")
186186+ }
187187+ if sig.ATProto.PDSEndpoint == "" {
188188+ return fmt.Errorf("missing PDS endpoint")
189189+ }
190190+ if sig.ATProto.CommitCID == "" {
191191+ return fmt.Errorf("missing commit CID")
192192+ }
193193+ if sig.Signature.Algorithm != "ECDSA-K256-SHA256" {
194194+ return fmt.Errorf("unsupported signature algorithm: %s", sig.Signature.Algorithm)
195195+ }
196196+ return nil
197197+}
198198+199199+// failureResult creates a failure result with the given message.
200200+func (v *ATProtoVerifier) failureResult(message string) verifier.VerifierResult {
201201+ return verifier.VerifierResult{
202202+ IsSuccess: false,
203203+ Name: v.name,
204204+ Type: v.Type(),
205205+ Message: message,
206206+ Extensions: map[string]interface{}{
207207+ "error": message,
208208+ },
209209+ }
210210+}
211211+212212+// TODO: Implement resolver.go with DID resolution logic
213213+// TODO: Implement crypto.go with K-256 signature verification
214214+// TODO: Implement config.go with trust policy loading
+4-1
pkg/appview/middleware/registry.go
···154154 // Auto-reconcile crew membership on first push/pull
155155 // This ensures users can push immediately after docker login without web sign-in
156156 // EnsureCrewMembership is best-effort and logs errors without failing the request
157157+ // Run in background to avoid blocking registry operations if hold is offline
157158 if holdDID != "" && nr.refresher != nil {
158159 slog.Debug("Auto-reconciling crew membership", "component", "registry/middleware", "did", did, "hold_did", holdDID)
159160 client := atproto.NewClient(pdsEndpoint, did, "")
160160- storage.EnsureCrewMembership(ctx, client, nr.refresher, holdDID)
161161+ go func(ctx context.Context, client *atproto.Client, refresher *oauth.Refresher, holdDID string) {
162162+ storage.EnsureCrewMembership(ctx, client, refresher, holdDID)
163163+ }(ctx, client, nr.refresher, holdDID)
161164 }
162165163166 // Get service token for hold authentication