mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-04-20 16:40:29 +00:00
226 lines
5.9 KiB
Plaintext
226 lines
5.9 KiB
Plaintext
// 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]any `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]any, 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 any) map[string]any {
|
|
data, _ := json.Marshal(v)
|
|
var m map[string]any
|
|
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
|