Files
at-container-registry/pkg/auth/oauth/keys.go
2025-10-29 12:06:47 -05:00

194 lines
5.5 KiB
Go

package oauth
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"log/slog"
"os"
"path/filepath"
"github.com/bluesky-social/indigo/atproto/atcrypto"
)
// KeyType represents the elliptic curve algorithm for key generation
type KeyType int
const (
// KeyTypeP256 uses NIST P-256 (ES256) - standard for OAuth/OIDC
KeyTypeP256 KeyType = iota
// KeyTypeK256 uses secp256k1 (ES256K) - used for ATProto PDS signing
KeyTypeK256
)
// GenerateOrLoadKey generates a new key pair or loads an existing one
// Supports both P-256 (OAuth) and K-256 (ATProto PDS) key types
func GenerateOrLoadKey(keyPath string, keyType KeyType) (atcrypto.PrivateKey, error) {
// Ensure directory exists
dir := filepath.Dir(keyPath)
if err := os.MkdirAll(dir, 0700); err != nil {
return nil, fmt.Errorf("failed to create key directory: %w", err)
}
// Check if key already exists
if _, err := os.Stat(keyPath); err == nil {
// Key exists, load it
return loadKey(keyPath, keyType)
}
// Key doesn't exist, generate new one
return generateKey(keyPath, keyType)
}
// generateKey creates a new key pair of the specified type
func generateKey(keyPath string, keyType KeyType) (atcrypto.PrivateKey, error) {
var privateKey atcrypto.PrivateKey
var keyName string
var err error
switch keyType {
case KeyTypeP256:
privateKey, err = atcrypto.GeneratePrivateKeyP256()
keyName = "P-256"
case KeyTypeK256:
privateKey, err = atcrypto.GeneratePrivateKeyK256()
keyName = "K-256"
default:
return nil, fmt.Errorf("unsupported key type: %d", keyType)
}
if err != nil {
return nil, fmt.Errorf("failed to generate %s key: %w", keyName, err)
}
// Serialize key to bytes
exportableKey, ok := privateKey.(atcrypto.PrivateKeyExportable)
if !ok {
return nil, fmt.Errorf("key does not support export")
}
keyBytes := exportableKey.Bytes()
// Write to file with restrictive permissions
if err := os.WriteFile(keyPath, keyBytes, 0600); err != nil {
return nil, fmt.Errorf("failed to write key file: %w", err)
}
slog.Info("Generated new signing key", "type", keyName, "path", keyPath)
return privateKey, nil
}
// loadKey loads an existing private key from disk
func loadKey(keyPath string, keyType KeyType) (atcrypto.PrivateKey, error) {
// Read key bytes
keyBytes, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("failed to read key file: %w", err)
}
var privateKey atcrypto.PrivateKey
var keyName string
switch keyType {
case KeyTypeP256:
privateKey, err = atcrypto.ParsePrivateBytesP256(keyBytes)
keyName = "P-256"
case KeyTypeK256:
// Check for old PEM format (migration path)
if IsPEMFormat(keyBytes) {
slog.Warn("Detected old P-256 PEM key, replacing with K-256")
return generateKey(keyPath, keyType)
}
privateKey, err = atcrypto.ParsePrivateBytesK256(keyBytes)
keyName = "K-256"
default:
return nil, fmt.Errorf("unsupported key type: %d", keyType)
}
if err != nil {
return nil, fmt.Errorf("failed to parse %s private key: %w", keyName, err)
}
slog.Info("Loaded existing signing key", "type", keyName, "path", keyPath)
return privateKey, nil
}
// IsPEMFormat checks if bytes are in PEM format (for migration detection)
// Exported for testing and migration utilities
func IsPEMFormat(data []byte) bool {
return len(data) > 10 && string(data[:5]) == "-----"
}
// GenerateOrLoadClientKey generates a new P256 key pair or loads an existing one
// This is a convenience wrapper for OAuth client keys
func GenerateOrLoadClientKey(keyPath string) (*atcrypto.PrivateKeyP256, error) {
key, err := GenerateOrLoadKey(keyPath, KeyTypeP256)
if err != nil {
return nil, err
}
p256Key, ok := key.(*atcrypto.PrivateKeyP256)
if !ok {
return nil, fmt.Errorf("expected P-256 key, got different type")
}
return p256Key, nil
}
// GenerateOrLoadPDSKey generates a new K256 key pair or loads an existing one
// This is a convenience wrapper for ATProto PDS signing keys
func GenerateOrLoadPDSKey(keyPath string) (*atcrypto.PrivateKeyK256, error) {
key, err := GenerateOrLoadKey(keyPath, KeyTypeK256)
if err != nil {
return nil, err
}
k256Key, ok := key.(*atcrypto.PrivateKeyK256)
if !ok {
return nil, fmt.Errorf("expected K-256 key, got different type")
}
return k256Key, nil
}
// GenerateKeyID generates a stable key ID from a P256 public key
// Uses the first 8 characters of the hex-encoded SHA256 hash of the public key bytes
func GenerateKeyID(privateKey *atcrypto.PrivateKeyP256) (string, error) {
// Get public key
pubKey, err := privateKey.PublicKey()
if err != nil {
return "", fmt.Errorf("failed to get public key: %w", err)
}
// Get public key bytes
pubKeyBytes := pubKey.Bytes()
// Hash public key bytes
hash := sha256.Sum256(pubKeyBytes)
// Return first 8 characters of hex-encoded hash
return hex.EncodeToString(hash[:])[:8], nil
}
// PrivateKeyToMultibase converts a P256 private key to multibase format
// Required by indigo's SetClientSecret() API
func PrivateKeyToMultibase(key *atcrypto.PrivateKeyP256) string {
return key.Multibase()
}
// MultibaseToPrivateKey parses a multibase-encoded P256 private key
func MultibaseToPrivateKey(encoded string) (*atcrypto.PrivateKeyP256, error) {
// ParsePrivateMultibase returns PrivateKeyExportable interface
key, err := atcrypto.ParsePrivateMultibase(encoded)
if err != nil {
return nil, fmt.Errorf("failed to parse multibase key: %w", err)
}
// Type assert to P256 key
p256Key, ok := key.(*atcrypto.PrivateKeyP256)
if !ok {
return nil, fmt.Errorf("expected P-256 key, got different key type")
}
return p256Key, nil
}