mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-03 08:46:57 +00:00
rename example go files for documentation
This commit is contained in:
@@ -1,225 +0,0 @@
|
||||
// Package main implements an OPA Gatekeeper External Data Provider for ATProto signature verification.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultPort is the default HTTP port
|
||||
DefaultPort = "8080"
|
||||
|
||||
// DefaultTrustPolicyPath is the default trust policy file path
|
||||
DefaultTrustPolicyPath = "/config/trust-policy.yaml"
|
||||
)
|
||||
|
||||
// Server is the HTTP server for the external data provider.
|
||||
type Server struct {
|
||||
verifier *Verifier
|
||||
port string
|
||||
httpServer *http.Server
|
||||
}
|
||||
|
||||
// ProviderRequest is the request format from Gatekeeper.
|
||||
type ProviderRequest struct {
|
||||
Keys []string `json:"keys"`
|
||||
Values []string `json:"values"`
|
||||
}
|
||||
|
||||
// ProviderResponse is the response format to Gatekeeper.
|
||||
type ProviderResponse struct {
|
||||
SystemError string `json:"system_error,omitempty"`
|
||||
Responses []map[string]interface{} `json:"responses"`
|
||||
}
|
||||
|
||||
// VerificationResult holds the result of verifying a single image.
|
||||
type VerificationResult struct {
|
||||
Image string `json:"image"`
|
||||
Verified bool `json:"verified"`
|
||||
DID string `json:"did,omitempty"`
|
||||
Handle string `json:"handle,omitempty"`
|
||||
SignedAt time.Time `json:"signedAt,omitempty"`
|
||||
CommitCID string `json:"commitCid,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// NewServer creates a new provider server.
|
||||
func NewServer(verifier *Verifier, port string) *Server {
|
||||
return &Server{
|
||||
verifier: verifier,
|
||||
port: port,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the HTTP server.
|
||||
func (s *Server) Start() error {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Provider endpoint (called by Gatekeeper)
|
||||
mux.HandleFunc("/provide", s.handleProvide)
|
||||
|
||||
// Health check endpoints
|
||||
mux.HandleFunc("/health", s.handleHealth)
|
||||
mux.HandleFunc("/ready", s.handleReady)
|
||||
|
||||
// Metrics endpoint (Prometheus)
|
||||
// TODO: Implement metrics
|
||||
// mux.HandleFunc("/metrics", s.handleMetrics)
|
||||
|
||||
s.httpServer = &http.Server{
|
||||
Addr: ":" + s.port,
|
||||
Handler: mux,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
log.Printf("Starting ATProto signature verification provider on port %s", s.port)
|
||||
return s.httpServer.ListenAndServe()
|
||||
}
|
||||
|
||||
// Stop gracefully stops the HTTP server.
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
if s.httpServer != nil {
|
||||
return s.httpServer.Shutdown(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleProvide handles the provider endpoint called by Gatekeeper.
|
||||
func (s *Server) handleProvide(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request
|
||||
var req ProviderRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
log.Printf("ERROR: failed to parse request: %v", err)
|
||||
http.Error(w, fmt.Sprintf("invalid request: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("INFO: received verification request for %d images", len(req.Values))
|
||||
|
||||
// Verify each image
|
||||
responses := make([]map[string]interface{}, 0, len(req.Values))
|
||||
for _, image := range req.Values {
|
||||
result := s.verifyImage(r.Context(), image)
|
||||
responses = append(responses, structToMap(result))
|
||||
}
|
||||
|
||||
// Send response
|
||||
resp := ProviderResponse{
|
||||
Responses: responses,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("ERROR: failed to encode response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// verifyImage verifies a single image.
|
||||
func (s *Server) verifyImage(ctx context.Context, image string) VerificationResult {
|
||||
start := time.Now()
|
||||
log.Printf("INFO: verifying image: %s", image)
|
||||
|
||||
// Call verifier
|
||||
verified, metadata, err := s.verifier.Verify(ctx, image)
|
||||
duration := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("ERROR: verification failed for %s: %v (duration: %v)", image, err, duration)
|
||||
return VerificationResult{
|
||||
Image: image,
|
||||
Verified: false,
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
if !verified {
|
||||
log.Printf("WARN: image %s failed verification (duration: %v)", image, duration)
|
||||
return VerificationResult{
|
||||
Image: image,
|
||||
Verified: false,
|
||||
Error: "signature verification failed",
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("INFO: image %s verified successfully (DID: %s, duration: %v)",
|
||||
image, metadata.DID, duration)
|
||||
|
||||
return VerificationResult{
|
||||
Image: image,
|
||||
Verified: true,
|
||||
DID: metadata.DID,
|
||||
Handle: metadata.Handle,
|
||||
SignedAt: metadata.SignedAt,
|
||||
CommitCID: metadata.CommitCID,
|
||||
}
|
||||
}
|
||||
|
||||
// handleHealth handles health check requests.
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ok",
|
||||
"version": "1.0.0",
|
||||
})
|
||||
}
|
||||
|
||||
// handleReady handles readiness check requests.
|
||||
func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Check dependencies (DID resolver, PDS connectivity)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ready",
|
||||
})
|
||||
}
|
||||
|
||||
// structToMap converts a struct to a map for JSON encoding.
|
||||
func structToMap(v interface{}) map[string]interface{} {
|
||||
data, _ := json.Marshal(v)
|
||||
var m map[string]interface{}
|
||||
json.Unmarshal(data, &m)
|
||||
return m
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Load configuration
|
||||
port := os.Getenv("HTTP_PORT")
|
||||
if port == "" {
|
||||
port = DefaultPort
|
||||
}
|
||||
|
||||
trustPolicyPath := os.Getenv("TRUST_POLICY_PATH")
|
||||
if trustPolicyPath == "" {
|
||||
trustPolicyPath = DefaultTrustPolicyPath
|
||||
}
|
||||
|
||||
// Create verifier
|
||||
verifier, err := NewVerifier(trustPolicyPath)
|
||||
if err != nil {
|
||||
log.Fatalf("FATAL: failed to create verifier: %v", err)
|
||||
}
|
||||
|
||||
// Create server
|
||||
server := NewServer(verifier, port)
|
||||
|
||||
// Start server
|
||||
if err := server.Start(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("FATAL: server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement verifier.go with ATProto signature verification logic
|
||||
// TODO: Implement resolver.go with DID resolution
|
||||
// TODO: Implement crypto.go with K-256 signature verification
|
||||
@@ -1,214 +0,0 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user