A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
76
fork

Configure Feed

Select the types of activity you want to include in your feed.

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}