A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
at main 182 lines 5.8 kB view raw
1package appview 2 3import ( 4 "crypto/rand" 5 "crypto/rsa" 6 "crypto/x509" 7 "crypto/x509/pkix" 8 "database/sql" 9 "encoding/pem" 10 "fmt" 11 "log/slog" 12 "math/big" 13 "os" 14 "path/filepath" 15 "time" 16 17 "atcr.io/pkg/appview/db" 18 "github.com/bluesky-social/indigo/atproto/atcrypto" 19) 20 21// loadOAuthKey loads the OAuth P-256 key with priority: DB → file → generate. 22// Keys loaded from file or newly generated are stored in the DB. 23func loadOAuthKey(database *sql.DB, keyPath string) (*atcrypto.PrivateKeyP256, error) { 24 // Try database first 25 data, err := db.GetCryptoKey(database, "oauth_p256") 26 if err != nil { 27 return nil, fmt.Errorf("failed to query crypto_keys: %w", err) 28 } 29 if data != nil { 30 key, err := atcrypto.ParsePrivateBytesP256(data) 31 if err != nil { 32 return nil, fmt.Errorf("failed to parse OAuth key from database: %w", err) 33 } 34 slog.Info("Loaded OAuth P-256 key from database") 35 return key, nil 36 } 37 38 // Try file fallback 39 if keyPath != "" { 40 if fileData, err := os.ReadFile(keyPath); err == nil { 41 key, err := atcrypto.ParsePrivateBytesP256(fileData) 42 if err != nil { 43 return nil, fmt.Errorf("failed to parse OAuth key from file %s: %w", keyPath, err) 44 } 45 // Migrate to database 46 if err := db.PutCryptoKey(database, "oauth_p256", fileData); err != nil { 47 return nil, fmt.Errorf("failed to store OAuth key in database: %w", err) 48 } 49 slog.Info("Migrated OAuth P-256 key from file to database", "path", keyPath) 50 return key, nil 51 } 52 } 53 54 // Generate new key 55 p256Key, err := atcrypto.GeneratePrivateKeyP256() 56 if err != nil { 57 return nil, fmt.Errorf("failed to generate OAuth P-256 key: %w", err) 58 } 59 60 keyBytes := p256Key.Bytes() 61 if err := db.PutCryptoKey(database, "oauth_p256", keyBytes); err != nil { 62 return nil, fmt.Errorf("failed to store generated OAuth key in database: %w", err) 63 } 64 slog.Info("Generated new OAuth P-256 key and stored in database") 65 66 return p256Key, nil 67} 68 69// loadJWTKeyAndCert loads the JWT RSA key from DB (with file fallback) and generates 70// a self-signed certificate. The cert is always regenerated and written to certPath 71// on disk because the distribution library reads it via os.Open(). 72func loadJWTKeyAndCert(database *sql.DB, keyPath, certPath string) (*rsa.PrivateKey, []byte, error) { 73 rsaKey, err := loadRSAKey(database, keyPath) 74 if err != nil { 75 return nil, nil, err 76 } 77 78 // Generate cert and write to disk for distribution library 79 certDER, err := generateAndWriteCert(rsaKey, certPath) 80 if err != nil { 81 return nil, nil, err 82 } 83 84 return rsaKey, certDER, nil 85} 86 87// loadRSAKey loads the RSA private key with priority: DB → file → generate. 88func loadRSAKey(database *sql.DB, keyPath string) (*rsa.PrivateKey, error) { 89 // Try database first 90 data, err := db.GetCryptoKey(database, "jwt_rsa") 91 if err != nil { 92 return nil, fmt.Errorf("failed to query crypto_keys: %w", err) 93 } 94 if data != nil { 95 key, err := parseRSAKeyPEM(data) 96 if err != nil { 97 return nil, fmt.Errorf("failed to parse RSA key from database: %w", err) 98 } 99 slog.Info("Loaded JWT RSA key from database") 100 return key, nil 101 } 102 103 // Try file fallback 104 if keyPath != "" { 105 if fileData, err := os.ReadFile(keyPath); err == nil { 106 key, err := parseRSAKeyPEM(fileData) 107 if err != nil { 108 return nil, fmt.Errorf("failed to parse RSA key from file %s: %w", keyPath, err) 109 } 110 // Migrate to database 111 if err := db.PutCryptoKey(database, "jwt_rsa", fileData); err != nil { 112 return nil, fmt.Errorf("failed to store RSA key in database: %w", err) 113 } 114 slog.Info("Migrated JWT RSA key from file to database", "path", keyPath) 115 return key, nil 116 } 117 } 118 119 // Generate new key 120 rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) 121 if err != nil { 122 return nil, fmt.Errorf("failed to generate RSA key: %w", err) 123 } 124 125 keyPEM := pem.EncodeToMemory(&pem.Block{ 126 Type: "RSA PRIVATE KEY", 127 Bytes: x509.MarshalPKCS1PrivateKey(rsaKey), 128 }) 129 if err := db.PutCryptoKey(database, "jwt_rsa", keyPEM); err != nil { 130 return nil, fmt.Errorf("failed to store generated RSA key in database: %w", err) 131 } 132 slog.Info("Generated new JWT RSA key and stored in database") 133 134 return rsaKey, nil 135} 136 137func parseRSAKeyPEM(data []byte) (*rsa.PrivateKey, error) { 138 block, _ := pem.Decode(data) 139 if block == nil || block.Type != "RSA PRIVATE KEY" { 140 return nil, fmt.Errorf("failed to decode PEM block containing RSA private key") 141 } 142 return x509.ParsePKCS1PrivateKey(block.Bytes) 143} 144 145// generateAndWriteCert creates a self-signed certificate from the RSA key and writes 146// it to certPath. Returns the DER-encoded certificate bytes for the JWT x5c header. 147func generateAndWriteCert(rsaKey *rsa.PrivateKey, certPath string) ([]byte, error) { 148 template := x509.Certificate{ 149 SerialNumber: big.NewInt(1), 150 Subject: pkix.Name{ 151 Organization: []string{"ATCR"}, 152 CommonName: "ATCR Token Signing Certificate", 153 }, 154 NotBefore: time.Now(), 155 NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), 156 KeyUsage: x509.KeyUsageDigitalSignature, 157 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 158 BasicConstraintsValid: true, 159 } 160 161 certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &rsaKey.PublicKey, rsaKey) 162 if err != nil { 163 return nil, fmt.Errorf("failed to create certificate: %w", err) 164 } 165 166 // Write cert to disk for distribution library 167 certPEM := pem.EncodeToMemory(&pem.Block{ 168 Type: "CERTIFICATE", 169 Bytes: certDER, 170 }) 171 172 dir := filepath.Dir(certPath) 173 if err := os.MkdirAll(dir, 0700); err != nil { 174 return nil, fmt.Errorf("failed to create cert directory: %w", err) 175 } 176 if err := os.WriteFile(certPath, certPEM, 0644); err != nil { 177 return nil, fmt.Errorf("failed to write certificate: %w", err) 178 } 179 180 slog.Info("Generated JWT signing certificate", "path", certPath) 181 return certDER, nil 182}