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