A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
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}