A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

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

actually use indigo atcrypto and k256 keys

+59 -117
+4 -4
cmd/appview/config.go
··· 60 60 return configuration.Log{ 61 61 Level: configuration.Loglevel(level), 62 62 Formatter: formatter, 63 - Fields: map[string]interface{}{ 63 + Fields: map[string]any{ 64 64 "service": "atcr-appview", 65 65 }, 66 66 } ··· 92 92 storage["inmemory"] = configuration.Parameters{} 93 93 94 94 // Disable upload purging 95 - // NOTE: Must use map[interface{}]interface{} for uploadpurging (not configuration.Parameters) 96 - // because distribution's validation code does a type assertion to map[interface{}]interface{} 95 + // NOTE: Must use map[any]any for uploadpurging (not configuration.Parameters) 96 + // because distribution's validation code does a type assertion to map[any]any 97 97 storage["maintenance"] = configuration.Parameters{ 98 - "uploadpurging": map[interface{}]interface{}{ 98 + "uploadpurging": map[any]any{ 99 99 "enabled": false, 100 100 "age": 7 * 24 * time.Hour, // 168h 101 101 "interval": 24 * time.Hour, // 24h
+5 -44
pkg/hold/pds/did.go
··· 1 1 package pds 2 2 3 3 import ( 4 - "crypto/ecdsa" 5 - "crypto/elliptic" 6 - "encoding/base64" 7 4 "encoding/json" 8 5 "fmt" 9 6 "net/url" ··· 46 43 47 44 did := fmt.Sprintf("did:web:%s", hostname) 48 45 49 - // Convert public key to multibase format 50 - publicKeyMultibase, err := encodePublicKeyMultibase(p.signingKey.PublicKey) 46 + // Get public key in multibase format using indigo's crypto 47 + pubKey, err := p.signingKey.PublicKey() 51 48 if err != nil { 52 - return nil, fmt.Errorf("failed to encode public key: %w", err) 49 + return nil, fmt.Errorf("failed to get public key: %w", err) 53 50 } 51 + publicKeyMultibase := pubKey.Multibase() 54 52 55 53 doc := &DIDDocument{ 56 54 Context: []string{ 57 55 "https://www.w3.org/ns/did/v1", 58 56 "https://w3id.org/security/multikey/v1", 57 + "https://w3id.org/security/suites/secp256k1-2019/v1", 59 58 }, 60 59 ID: did, 61 60 AlsoKnownAs: []string{ ··· 84 83 return doc, nil 85 84 } 86 85 87 - // encodePublicKeyMultibase encodes an ECDSA public key in multibase format 88 - // For P-256 keys, we use the compressed format with multicodec prefix 89 - func encodePublicKeyMultibase(pubKey ecdsa.PublicKey) (string, error) { 90 - // Check if this is a P-256 key 91 - if pubKey.Curve != elliptic.P256() { 92 - return "", fmt.Errorf("unsupported curve: only P-256 is supported") 93 - } 94 - 95 - // Use compressed point format 96 - // 0x02 if Y is even, 0x03 if Y is odd 97 - var prefix byte 98 - if pubKey.Y.Bit(0) == 0 { 99 - prefix = 0x02 100 - } else { 101 - prefix = 0x03 102 - } 103 - 104 - // Compressed format: prefix (1 byte) + X coordinate (32 bytes for P-256) 105 - xBytes := pubKey.X.Bytes() 106 - 107 - // Pad X to 32 bytes if needed 108 - paddedX := make([]byte, 32) 109 - copy(paddedX[32-len(xBytes):], xBytes) 110 - 111 - compressed := append([]byte{prefix}, paddedX...) 112 - 113 - // Multicodec prefix for P-256 public key: 0x1200 114 - // See https://github.com/multiformats/multicodec/blob/master/table.csv 115 - multicodec := []byte{0x80, 0x24} // P-256 public key multicodec 116 - 117 - // Combine multicodec + compressed key 118 - multicodecKey := append(multicodec, compressed...) 119 - 120 - // Encode in multibase (base58btc, prefix 'z') 121 - encoded := base64.RawURLEncoding.EncodeToString(multicodecKey) 122 - 123 - return "z" + encoded, nil 124 - } 125 86 126 87 // MarshalDIDDocument converts a DID document to JSON using the stored public URL 127 88 func (p *HoldPDS) MarshalDIDDocument() ([]byte, error) {
+28 -53
pkg/hold/pds/keys.go
··· 1 1 package pds 2 2 3 3 import ( 4 - "crypto/ecdsa" 5 - "crypto/elliptic" 6 - "crypto/rand" 7 - "crypto/x509" 8 - "encoding/pem" 9 4 "fmt" 10 5 "os" 11 6 "path/filepath" 7 + 8 + "github.com/bluesky-social/indigo/atproto/crypto" 12 9 ) 13 10 14 11 // GenerateOrLoadKey generates a new K256 key pair or loads an existing one 15 - func GenerateOrLoadKey(keyPath string) (*ecdsa.PrivateKey, error) { 12 + func GenerateOrLoadKey(keyPath string) (*crypto.PrivateKeyK256, error) { 16 13 // Ensure directory exists 17 14 dir := filepath.Dir(keyPath) 18 15 if err := os.MkdirAll(dir, 0700); err != nil { ··· 29 26 return generateKey(keyPath) 30 27 } 31 28 32 - // generateKey creates a new K256 (secp256k1) key pair 33 - func generateKey(keyPath string) (*ecdsa.PrivateKey, error) { 34 - // Generate K256 key (secp256k1) 35 - privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 29 + // generateKey creates a new K256 (secp256k1) key pair using indigo's atcrypto 30 + func generateKey(keyPath string) (*crypto.PrivateKeyK256, error) { 31 + // Generate K256 key (secp256k1) using indigo 32 + privateKey, err := crypto.GeneratePrivateKeyK256() 36 33 if err != nil { 37 34 return nil, fmt.Errorf("failed to generate key: %w", err) 38 35 } 39 36 40 - // Marshal private key to DER format 41 - derBytes, err := x509.MarshalECPrivateKey(privateKey) 42 - if err != nil { 43 - return nil, fmt.Errorf("failed to marshal private key: %w", err) 44 - } 45 - 46 - // Create PEM block 47 - pemBlock := &pem.Block{ 48 - Type: "EC PRIVATE KEY", 49 - Bytes: derBytes, 50 - } 37 + // Serialize key to bytes 38 + keyBytes := privateKey.Bytes() 51 39 52 40 // Write to file with restrictive permissions 53 - file, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 54 - if err != nil { 55 - return nil, fmt.Errorf("failed to create key file: %w", err) 56 - } 57 - defer file.Close() 58 - 59 - if err := pem.Encode(file, pemBlock); err != nil { 60 - return nil, fmt.Errorf("failed to write PEM data: %w", err) 41 + if err := os.WriteFile(keyPath, keyBytes, 0600); err != nil { 42 + return nil, fmt.Errorf("failed to write key file: %w", err) 61 43 } 62 44 63 - fmt.Printf("Generated new signing key at %s\n", keyPath) 45 + fmt.Printf("Generated new K-256 signing key at %s\n", keyPath) 64 46 return privateKey, nil 65 47 } 66 48 67 49 // loadKey loads an existing private key from disk 68 - func loadKey(keyPath string) (*ecdsa.PrivateKey, error) { 69 - // Read PEM file 70 - pemData, err := os.ReadFile(keyPath) 50 + func loadKey(keyPath string) (*crypto.PrivateKeyK256, error) { 51 + // Read key bytes 52 + keyBytes, err := os.ReadFile(keyPath) 71 53 if err != nil { 72 54 return nil, fmt.Errorf("failed to read key file: %w", err) 73 55 } 74 56 75 - // Decode PEM block 76 - block, _ := pem.Decode(pemData) 77 - if block == nil { 78 - return nil, fmt.Errorf("failed to decode PEM block") 79 - } 80 - 81 - // Parse EC private key 82 - privateKey, err := x509.ParseECPrivateKey(block.Bytes) 57 + // Try to parse as K256 private key 58 + privateKey, err := crypto.ParsePrivateBytesK256(keyBytes) 83 59 if err != nil { 60 + // Check if this is an old P-256 PEM key (migration) 61 + if isPEMFormat(keyBytes) { 62 + fmt.Printf("⚠️ Detected old P-256 key, replacing with K-256...\n") 63 + // Generate new K-256 key (overwrites old P-256) 64 + return generateKey(keyPath) 65 + } 66 + 84 67 return nil, fmt.Errorf("failed to parse private key: %w", err) 85 68 } 86 69 87 - fmt.Printf("Loaded existing signing key from %s\n", keyPath) 70 + fmt.Printf("Loaded existing K-256 signing key from %s\n", keyPath) 88 71 return privateKey, nil 89 72 } 90 73 91 - // PublicKeyToBase58 converts an ECDSA public key to base58 format for DID documents 92 - func PublicKeyToBase58(pubKey *ecdsa.PublicKey) (string, error) { 93 - // Marshal public key to X.509 SPKI format 94 - derBytes, err := x509.MarshalPKIXPublicKey(pubKey) 95 - if err != nil { 96 - return "", fmt.Errorf("failed to marshal public key: %w", err) 97 - } 98 - 99 - // TODO: Convert to base58 (need to import base58 library) 100 - // For now, just return hex encoding as placeholder 101 - return fmt.Sprintf("%x", derBytes), nil 74 + // isPEMFormat checks if bytes are in PEM format (old P-256 keys) 75 + func isPEMFormat(data []byte) bool { 76 + return len(data) > 10 && string(data[:5]) == "-----" 102 77 }
+3 -3
pkg/hold/pds/server.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "crypto/ecdsa" 6 5 "fmt" 7 6 "os" 8 7 "path/filepath" 9 8 9 + "github.com/bluesky-social/indigo/atproto/crypto" 10 10 "github.com/bluesky-social/indigo/carstore" 11 11 "github.com/bluesky-social/indigo/models" 12 12 "github.com/bluesky-social/indigo/repo" ··· 21 21 repo *repo.Repo 22 22 dbPath string 23 23 uid models.Uid 24 - signingKey *ecdsa.PrivateKey 24 + signingKey *crypto.PrivateKeyK256 25 25 } 26 26 27 27 // NewHoldPDS creates or opens a hold PDS with SQLite carstore ··· 94 94 } 95 95 96 96 // SigningKey returns the hold's signing key 97 - func (p *HoldPDS) SigningKey() *ecdsa.PrivateKey { 97 + func (p *HoldPDS) SigningKey() *crypto.PrivateKeyK256 { 98 98 return p.signingKey 99 99 } 100 100
+19 -13
pkg/hold/pds/xrpc.go
··· 79 79 return 80 80 } 81 81 82 - response := map[string]interface{}{ 82 + response := map[string]any{ 83 83 "version": "0.4.999", 84 84 } 85 85 ··· 102 102 hostname = strings.Split(hostname, "/")[0] // Remove path 103 103 hostname = strings.Split(hostname, ":")[0] // Remove port 104 104 105 - response := map[string]interface{}{ 105 + response := map[string]any{ 106 106 "did": h.pds.DID(), 107 107 "availableUserDomains": []string{"." + hostname}, 108 108 "inviteCodeRequired": true, // Single-user PDS, no account creation ··· 135 135 136 136 // TODO: Get actual repo head from carstore 137 137 // Note: For did:web, the handle IS the DID (not just hostname) 138 - response := map[string]interface{}{ 138 + response := map[string]any{ 139 139 "did": h.pds.DID(), 140 140 "handle": h.pds.DID(), 141 141 "didDoc": didDoc, ··· 180 180 return 181 181 } 182 182 183 - response := map[string]interface{}{ 183 + response := map[string]any{ 184 184 "uri": fmt.Sprintf("at://%s/%s/%s", h.pds.DID(), collection, rkey), 185 185 "value": crewRecord, 186 186 } ··· 221 221 return 222 222 } 223 223 224 - records := make([]map[string]interface{}, len(crew)) 224 + records := make([]map[string]any, len(crew)) 225 225 for i, member := range crew { 226 226 // TODO: Get actual rkey from somewhere 227 - records[i] = map[string]interface{}{ 227 + records[i] = map[string]any{ 228 228 "uri": fmt.Sprintf("at://%s/%s/%s", h.pds.DID(), collection, member.Member), 229 229 "value": member, 230 230 } 231 231 } 232 232 233 - response := map[string]interface{}{ 233 + response := map[string]any{ 234 234 "records": records, 235 235 } 236 236 ··· 311 311 head, err := h.pds.carstore.GetUserRepoHead(r.Context(), h.pds.uid) 312 312 if err != nil { 313 313 // If no repo exists yet, return empty list 314 - response := map[string]interface{}{ 315 - "repos": []interface{}{}, 314 + response := map[string]any{ 315 + "repos": []any{}, 316 316 } 317 317 w.Header().Set("Content-Type", "application/json") 318 318 json.NewEncoder(w).Encode(response) ··· 320 320 } 321 321 322 322 rev, err := h.pds.carstore.GetUserRepoRev(r.Context(), h.pds.uid) 323 - if err != nil { 324 - http.Error(w, fmt.Sprintf("failed to get repo rev: %v", err), http.StatusInternalServerError) 323 + if err != nil || rev == "" { 324 + // No commits yet, return empty list 325 + // Don't expose repos with no revision (empty/uninitialized) 326 + response := map[string]any{ 327 + "repos": []any{}, 328 + } 329 + w.Header().Set("Content-Type", "application/json") 330 + json.NewEncoder(w).Encode(response) 325 331 return 326 332 } 327 333 328 - repos := []map[string]interface{}{ 334 + repos := []map[string]any{ 329 335 { 330 336 "did": did, 331 337 "head": head.String(), ··· 334 340 }, 335 341 } 336 342 337 - response := map[string]interface{}{ 343 + response := map[string]any{ 338 344 "repos": repos, 339 345 } 340 346