// Package sshca implements the SSH CA engine for SSH certificate issuance. package sshca import ( "context" "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/x509" "encoding/binary" "encoding/json" "encoding/pem" "errors" "fmt" "sort" "strconv" "strings" "sync" "time" "golang.org/x/crypto/ssh" "git.wntrmute.dev/mc/metacrypt/internal/barrier" mcrypto "git.wntrmute.dev/mc/metacrypt/internal/crypto" "git.wntrmute.dev/mc/metacrypt/internal/engine" ) var ( ErrSealed = errors.New("sshca: engine is sealed") ErrCertNotFound = errors.New("sshca: certificate not found") ErrProfileExists = errors.New("sshca: profile already exists") ErrProfileNotFound = errors.New("sshca: profile not found") ErrForbidden = errors.New("sshca: forbidden") ErrUnauthorized = errors.New("sshca: authentication required") ) // SSHCAEngine implements the SSH CA engine. type SSHCAEngine struct { barrier barrier.Barrier config *SSHCAConfig caKey crypto.PrivateKey caSigner ssh.Signer mountPath string krlVersion uint64 krlData []byte mu sync.RWMutex } // NewSSHCAEngine creates a new SSH CA engine instance. func NewSSHCAEngine() engine.Engine { return &SSHCAEngine{} } func (e *SSHCAEngine) Type() engine.EngineType { return engine.EngineTypeSSHCA } func (e *SSHCAEngine) Initialize(ctx context.Context, b barrier.Barrier, mountPath string, config map[string]interface{}) error { e.mu.Lock() defer e.mu.Unlock() e.barrier = b e.mountPath = mountPath cfg := &SSHCAConfig{ KeyAlgorithm: "ed25519", MaxTTL: "87600h", DefaultTTL: "24h", } if v, ok := config["key_algorithm"].(string); ok && v != "" { cfg.KeyAlgorithm = v } if v, ok := config["max_ttl"].(string); ok && v != "" { cfg.MaxTTL = v } if v, ok := config["default_ttl"].(string); ok && v != "" { cfg.DefaultTTL = v } // Validate config. if _, err := time.ParseDuration(cfg.MaxTTL); err != nil { return fmt.Errorf("sshca: invalid max_ttl: %w", err) } if _, err := time.ParseDuration(cfg.DefaultTTL); err != nil { return fmt.Errorf("sshca: invalid default_ttl: %w", err) } // Generate CA key. privKey, err := generateKey(cfg.KeyAlgorithm) if err != nil { return fmt.Errorf("sshca: generate CA key: %w", err) } // Store CA key as PKCS8 PEM. keyBytes, err := x509.MarshalPKCS8PrivateKey(privKey) if err != nil { return fmt.Errorf("sshca: marshal CA key: %w", err) } keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes}) if err := b.Put(ctx, mountPath+"ca/key.pem", keyPEM); err != nil { return fmt.Errorf("sshca: store CA key: %w", err) } // Store CA public key in SSH authorized_keys format. sshPub, err := ssh.NewPublicKey(publicKey(privKey)) if err != nil { return fmt.Errorf("sshca: create SSH public key: %w", err) } pubKeyBytes := ssh.MarshalAuthorizedKey(sshPub) if err := b.Put(ctx, mountPath+"ca/pubkey.pub", pubKeyBytes); err != nil { return fmt.Errorf("sshca: store CA public key: %w", err) } // Store config. cfgData, err := json.Marshal(cfg) if err != nil { return fmt.Errorf("sshca: marshal config: %w", err) } if err := b.Put(ctx, mountPath+"config.json", cfgData); err != nil { return fmt.Errorf("sshca: store config: %w", err) } // Initialize KRL version. if err := e.storeKRLVersion(ctx, 0); err != nil { return fmt.Errorf("sshca: store KRL version: %w", err) } // Set in-memory state. e.config = cfg e.caKey = privKey signer, err := ssh.NewSignerFromKey(privKey) if err != nil { return fmt.Errorf("sshca: create signer: %w", err) } e.caSigner = signer e.krlVersion = 0 e.krlData = e.buildKRL(nil) return nil } func (e *SSHCAEngine) Unseal(ctx context.Context, b barrier.Barrier, mountPath string) error { e.mu.Lock() defer e.mu.Unlock() e.barrier = b e.mountPath = mountPath // Load config. cfgData, err := b.Get(ctx, mountPath+"config.json") if err != nil { return fmt.Errorf("sshca: load config: %w", err) } var cfg SSHCAConfig if err := json.Unmarshal(cfgData, &cfg); err != nil { return fmt.Errorf("sshca: parse config: %w", err) } e.config = &cfg // Load CA key. keyPEM, err := b.Get(ctx, mountPath+"ca/key.pem") if err != nil { return fmt.Errorf("sshca: load CA key: %w", err) } block, _ := pem.Decode(keyPEM) if block == nil { return fmt.Errorf("sshca: decode CA key PEM") } privKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return fmt.Errorf("sshca: parse CA key: %w", err) } e.caKey = privKey signer, err := ssh.NewSignerFromKey(privKey) if err != nil { return fmt.Errorf("sshca: create signer: %w", err) } e.caSigner = signer // Load KRL version. e.krlVersion, err = e.loadKRLVersion(ctx) if err != nil { // Default to 0 if not found. e.krlVersion = 0 } // Rebuild KRL from revoked certs. revokedSerials, err := e.collectRevokedSerials(ctx) if err != nil { return fmt.Errorf("sshca: rebuild KRL: %w", err) } e.krlData = e.buildKRL(revokedSerials) return nil } func (e *SSHCAEngine) Seal() error { e.mu.Lock() defer e.mu.Unlock() if e.caKey != nil { engine.ZeroizeKey(e.caKey) } e.caKey = nil e.caSigner = nil e.config = nil e.krlData = nil return nil } func (e *SSHCAEngine) HandleRequest(ctx context.Context, req *engine.Request) (*engine.Response, error) { e.mu.Lock() defer e.mu.Unlock() if e.config == nil { return nil, ErrSealed } switch req.Operation { case "get-ca-pubkey": return e.handleGetCAPubkey(ctx) case "sign-host": return e.handleSignHost(ctx, req) case "sign-user": return e.handleSignUser(ctx, req) case "create-profile": return e.handleCreateProfile(ctx, req) case "update-profile": return e.handleUpdateProfile(ctx, req) case "get-profile": return e.handleGetProfile(ctx, req) case "list-profiles": return e.handleListProfiles(ctx, req) case "delete-profile": return e.handleDeleteProfile(ctx, req) case "get-cert": return e.handleGetCert(ctx, req) case "list-certs": return e.handleListCerts(ctx, req) case "revoke-cert": return e.handleRevokeCert(ctx, req) case "delete-cert": return e.handleDeleteCert(ctx, req) case "get-krl": return e.handleGetKRL(ctx) default: return nil, fmt.Errorf("sshca: unknown operation %q", req.Operation) } } // GetCAPubkey returns the CA public key. Thread-safe for use by route handlers. func (e *SSHCAEngine) GetCAPubkey(ctx context.Context) ([]byte, error) { e.mu.RLock() defer e.mu.RUnlock() if e.config == nil { return nil, ErrSealed } return e.barrier.Get(ctx, e.mountPath+"ca/pubkey.pub") } // GetKRL returns the current KRL data. Thread-safe for use by route handlers. func (e *SSHCAEngine) GetKRL() ([]byte, error) { e.mu.RLock() defer e.mu.RUnlock() if e.config == nil { return nil, ErrSealed } if e.krlData == nil { return e.buildKRL(nil), nil } cp := make([]byte, len(e.krlData)) copy(cp, e.krlData) return cp, nil } func (e *SSHCAEngine) handleGetCAPubkey(ctx context.Context) (*engine.Response, error) { pubKeyBytes, err := e.barrier.Get(ctx, e.mountPath+"ca/pubkey.pub") if err != nil { return nil, fmt.Errorf("sshca: load CA public key: %w", err) } return &engine.Response{ Data: map[string]interface{}{ "public_key": string(pubKeyBytes), }, }, nil } func (e *SSHCAEngine) handleSignHost(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } if !req.CallerInfo.IsUser() { return nil, ErrForbidden } pubKeyStr, _ := req.Data["public_key"].(string) if pubKeyStr == "" { return nil, fmt.Errorf("sshca: public_key is required") } hostname, _ := req.Data["hostname"].(string) if hostname == "" { return nil, fmt.Errorf("sshca: hostname is required") } ttlStr, _ := req.Data["ttl"].(string) // Policy check: sshca/{mount}/id/{hostname} action sign. mountName := e.mountName() resource := fmt.Sprintf("sshca/%s/id/%s", mountName, hostname) if req.CheckPolicy != nil { effect, matched := req.CheckPolicy(resource, "sign") if matched && effect == "deny" { return nil, ErrForbidden } } // Parse public key. sshPubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKeyStr)) if err != nil { return nil, fmt.Errorf("sshca: parse public key: %w", err) } // Determine TTL. ttl, err := e.resolveTTL(ttlStr, "") if err != nil { return nil, err } // Generate serial. serial, err := generateSerial() if err != nil { return nil, fmt.Errorf("sshca: generate serial: %w", err) } now := time.Now() cert := &ssh.Certificate{ CertType: ssh.HostCert, Key: sshPubKey, Serial: serial, KeyId: fmt.Sprintf("host:%s", hostname), ValidAfter: uint64(now.Add(-5 * time.Minute).Unix()), ValidBefore: uint64(now.Add(ttl).Unix()), ValidPrincipals: []string{hostname}, } if err := cert.SignCert(rand.Reader, e.caSigner); err != nil { return nil, fmt.Errorf("sshca: sign certificate: %w", err) } certData := ssh.MarshalAuthorizedKey(cert) // Store cert record. record := CertRecord{ Serial: serial, CertType: "host", Principals: []string{hostname}, CertData: string(certData), KeyID: cert.KeyId, IssuedBy: req.CallerInfo.Username, IssuedAt: now, ExpiresAt: now.Add(ttl), } if err := e.storeCertRecord(ctx, &record); err != nil { return nil, err } return &engine.Response{ Data: map[string]interface{}{ "serial": strconv.FormatUint(serial, 10), "cert_type": "host", "principals": []interface{}{hostname}, "cert_data": string(certData), "key_id": cert.KeyId, "issued_by": req.CallerInfo.Username, "issued_at": now.Format(time.RFC3339), "expires_at": now.Add(ttl).Format(time.RFC3339), }, }, nil } func (e *SSHCAEngine) handleSignUser(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } if !req.CallerInfo.IsUser() { return nil, ErrForbidden } pubKeyStr, _ := req.Data["public_key"].(string) if pubKeyStr == "" { return nil, fmt.Errorf("sshca: public_key is required") } ttlStr, _ := req.Data["ttl"].(string) profileName, _ := req.Data["profile"].(string) // Parse principals. principals := extractStringSlice(req.Data, "principals") // Default: user can only sign for own username as principal. if len(principals) == 0 { principals = []string{req.CallerInfo.Username} } // Load profile if specified. var profile *SigningProfile if profileName != "" { p, err := e.loadProfile(ctx, profileName) if err != nil { return nil, fmt.Errorf("sshca: profile %q: %w", profileName, err) } profile = p } // Check principals. if profile != nil && len(profile.AllowedPrincipals) > 0 { for _, p := range principals { if !contains(profile.AllowedPrincipals, p) { return nil, fmt.Errorf("sshca: principal %q not allowed by profile %q", p, profileName) } } } else if !req.CallerInfo.IsAdmin { // Non-admin without profile can only sign for own username. for _, p := range principals { if p != req.CallerInfo.Username { // Check policy. if req.CheckPolicy != nil { mountName := e.mountName() resource := fmt.Sprintf("sshca/%s/id/%s", mountName, p) effect, matched := req.CheckPolicy(resource, "sign") if matched && effect == "allow" { continue } } return nil, fmt.Errorf("sshca: forbidden: non-admin cannot sign for principal %q", p) } } } // Parse public key. sshPubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKeyStr)) if err != nil { return nil, fmt.Errorf("sshca: parse public key: %w", err) } // Determine TTL. profileMaxTTL := "" if profile != nil { profileMaxTTL = profile.MaxTTL } ttl, err := e.resolveTTL(ttlStr, profileMaxTTL) if err != nil { return nil, err } // Build extensions. extensions := map[string]string{"permit-pty": ""} if profile != nil && len(profile.Extensions) > 0 { // Start with defaults, profile wins on conflict. for k, v := range profile.Extensions { extensions[k] = v } } // Build critical options. var criticalOptions map[string]string if profile != nil && len(profile.CriticalOptions) > 0 { criticalOptions = make(map[string]string) for k, v := range profile.CriticalOptions { criticalOptions[k] = v } } // Generate serial. serial, err := generateSerial() if err != nil { return nil, fmt.Errorf("sshca: generate serial: %w", err) } now := time.Now() cert := &ssh.Certificate{ CertType: ssh.UserCert, Key: sshPubKey, Serial: serial, KeyId: fmt.Sprintf("user:%s", principals[0]), ValidAfter: uint64(now.Add(-5 * time.Minute).Unix()), ValidBefore: uint64(now.Add(ttl).Unix()), ValidPrincipals: principals, Permissions: ssh.Permissions{ CriticalOptions: criticalOptions, Extensions: extensions, }, } if err := cert.SignCert(rand.Reader, e.caSigner); err != nil { return nil, fmt.Errorf("sshca: sign certificate: %w", err) } certData := ssh.MarshalAuthorizedKey(cert) // Store cert record. record := CertRecord{ Serial: serial, CertType: "user", Principals: principals, CertData: string(certData), KeyID: cert.KeyId, Profile: profileName, IssuedBy: req.CallerInfo.Username, IssuedAt: now, ExpiresAt: now.Add(ttl), } if err := e.storeCertRecord(ctx, &record); err != nil { return nil, err } principalsIface := make([]interface{}, len(principals)) for i, p := range principals { principalsIface[i] = p } return &engine.Response{ Data: map[string]interface{}{ "serial": strconv.FormatUint(serial, 10), "cert_type": "user", "principals": principalsIface, "cert_data": string(certData), "key_id": cert.KeyId, "profile": profileName, "issued_by": req.CallerInfo.Username, "issued_at": now.Format(time.RFC3339), "expires_at": now.Add(ttl).Format(time.RFC3339), }, }, nil } func (e *SSHCAEngine) handleCreateProfile(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } if !req.CallerInfo.IsAdmin { return nil, ErrForbidden } name, _ := req.Data["name"].(string) if name == "" { return nil, fmt.Errorf("sshca: name is required") } if err := engine.ValidateName(name); err != nil { return nil, fmt.Errorf("sshca: %w", err) } // Check if profile already exists. _, err := e.barrier.Get(ctx, e.mountPath+"profiles/"+name+".json") if err == nil { return nil, ErrProfileExists } profile := SigningProfile{ Name: name, CriticalOptions: extractStringMap(req.Data, "critical_options"), Extensions: extractStringMap(req.Data, "extensions"), MaxTTL: stringFromData(req.Data, "max_ttl"), AllowedPrincipals: extractStringSlice(req.Data, "allowed_principals"), } if err := e.storeProfile(ctx, &profile); err != nil { return nil, err } return &engine.Response{ Data: map[string]interface{}{ "name": name, }, }, nil } func (e *SSHCAEngine) handleUpdateProfile(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } if !req.CallerInfo.IsAdmin { return nil, ErrForbidden } name, _ := req.Data["name"].(string) if name == "" { return nil, fmt.Errorf("sshca: name is required") } if err := engine.ValidateName(name); err != nil { return nil, err } // Load existing profile. profile, err := e.loadProfile(ctx, name) if err != nil { return nil, ErrProfileNotFound } if v := extractStringMap(req.Data, "critical_options"); v != nil { profile.CriticalOptions = v } if v := extractStringMap(req.Data, "extensions"); v != nil { profile.Extensions = v } if v := stringFromData(req.Data, "max_ttl"); v != "" { profile.MaxTTL = v } if v := extractStringSlice(req.Data, "allowed_principals"); v != nil { profile.AllowedPrincipals = v } if err := e.storeProfile(ctx, profile); err != nil { return nil, err } return &engine.Response{ Data: map[string]interface{}{ "name": name, }, }, nil } func (e *SSHCAEngine) handleGetProfile(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } if !req.CallerInfo.IsUser() { return nil, ErrForbidden } name, _ := req.Data["name"].(string) if name == "" { return nil, fmt.Errorf("sshca: name is required") } if err := engine.ValidateName(name); err != nil { return nil, err } profile, err := e.loadProfile(ctx, name) if err != nil { return nil, ErrProfileNotFound } principalsIface := make([]interface{}, len(profile.AllowedPrincipals)) for i, p := range profile.AllowedPrincipals { principalsIface[i] = p } return &engine.Response{ Data: map[string]interface{}{ "name": profile.Name, "critical_options": profile.CriticalOptions, "extensions": profile.Extensions, "max_ttl": profile.MaxTTL, "allowed_principals": principalsIface, }, }, nil } func (e *SSHCAEngine) handleListProfiles(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } if !req.CallerInfo.IsUser() { return nil, ErrForbidden } paths, err := e.barrier.List(ctx, e.mountPath+"profiles/") if err != nil { return &engine.Response{ Data: map[string]interface{}{ "profiles": []interface{}{}, }, }, nil } profiles := make([]interface{}, 0, len(paths)) for _, p := range paths { if strings.HasSuffix(p, ".json") { name := strings.TrimSuffix(p, ".json") profiles = append(profiles, name) } } return &engine.Response{ Data: map[string]interface{}{ "profiles": profiles, }, }, nil } func (e *SSHCAEngine) handleDeleteProfile(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } if !req.CallerInfo.IsAdmin { return nil, ErrForbidden } name, _ := req.Data["name"].(string) if name == "" { return nil, fmt.Errorf("sshca: name is required") } if err := engine.ValidateName(name); err != nil { return nil, err } // Check existence. if _, err := e.barrier.Get(ctx, e.mountPath+"profiles/"+name+".json"); err != nil { return nil, ErrProfileNotFound } if err := e.barrier.Delete(ctx, e.mountPath+"profiles/"+name+".json"); err != nil { return nil, fmt.Errorf("sshca: delete profile: %w", err) } return &engine.Response{ Data: map[string]interface{}{"ok": true}, }, nil } func (e *SSHCAEngine) handleGetCert(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } if !req.CallerInfo.IsUser() { return nil, ErrForbidden } serialStr, _ := req.Data["serial"].(string) if serialStr == "" { return nil, fmt.Errorf("sshca: serial is required") } record, err := e.loadCertRecord(ctx, serialStr) if err != nil { return nil, ErrCertNotFound } return &engine.Response{ Data: certRecordToData(record), }, nil } func (e *SSHCAEngine) handleListCerts(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } if !req.CallerInfo.IsUser() { return nil, ErrForbidden } paths, err := e.barrier.List(ctx, e.mountPath+"certs/") if err != nil { return &engine.Response{ Data: map[string]interface{}{ "certs": []interface{}{}, }, }, nil } certs := make([]interface{}, 0, len(paths)) for _, p := range paths { if !strings.HasSuffix(p, ".json") { continue } serialStr := strings.TrimSuffix(p, ".json") record, err := e.loadCertRecord(ctx, serialStr) if err != nil { continue } certs = append(certs, certRecordToData(record)) } return &engine.Response{ Data: map[string]interface{}{ "certs": certs, }, }, nil } func (e *SSHCAEngine) handleRevokeCert(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } if !req.CallerInfo.IsAdmin { return nil, ErrForbidden } serialStr, _ := req.Data["serial"].(string) if serialStr == "" { return nil, fmt.Errorf("sshca: serial is required") } record, err := e.loadCertRecord(ctx, serialStr) if err != nil { return nil, ErrCertNotFound } now := time.Now() record.Revoked = true record.RevokedAt = now record.RevokedBy = req.CallerInfo.Username if err := e.storeCertRecord(ctx, record); err != nil { return nil, err } // Rebuild KRL. e.krlVersion++ if err := e.storeKRLVersion(ctx, e.krlVersion); err != nil { return nil, err } revokedSerials, _ := e.collectRevokedSerials(ctx) e.krlData = e.buildKRL(revokedSerials) return &engine.Response{ Data: map[string]interface{}{ "serial": serialStr, "revoked_at": now.Format(time.RFC3339), }, }, nil } func (e *SSHCAEngine) handleDeleteCert(ctx context.Context, req *engine.Request) (*engine.Response, error) { if req.CallerInfo == nil { return nil, ErrUnauthorized } if !req.CallerInfo.IsAdmin { return nil, ErrForbidden } serialStr, _ := req.Data["serial"].(string) if serialStr == "" { return nil, fmt.Errorf("sshca: serial is required") } // Check existence. if _, err := e.barrier.Get(ctx, e.mountPath+"certs/"+serialStr+".json"); err != nil { return nil, ErrCertNotFound } if err := e.barrier.Delete(ctx, e.mountPath+"certs/"+serialStr+".json"); err != nil { return nil, fmt.Errorf("sshca: delete cert: %w", err) } // Rebuild KRL (the deleted cert may have been revoked). e.krlVersion++ if err := e.storeKRLVersion(ctx, e.krlVersion); err != nil { return nil, err } revokedSerials, _ := e.collectRevokedSerials(ctx) e.krlData = e.buildKRL(revokedSerials) return &engine.Response{ Data: map[string]interface{}{"ok": true}, }, nil } func (e *SSHCAEngine) handleGetKRL(_ context.Context) (*engine.Response, error) { if e.krlData == nil { return &engine.Response{ Data: map[string]interface{}{ "krl": string(e.buildKRL(nil)), }, }, nil } cp := make([]byte, len(e.krlData)) copy(cp, e.krlData) return &engine.Response{ Data: map[string]interface{}{ "krl": string(cp), }, }, nil } // --- Helpers --- func generateKey(algorithm string) (crypto.PrivateKey, error) { switch algorithm { case "ed25519": _, priv, err := ed25519.GenerateKey(rand.Reader) return priv, err case "ecdsa-p256": return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) case "ecdsa-p384": return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) default: return nil, fmt.Errorf("unsupported key algorithm: %s", algorithm) } } func publicKey(priv crypto.PrivateKey) crypto.PublicKey { switch k := priv.(type) { case ed25519.PrivateKey: return k.Public() case *ecdsa.PrivateKey: return &k.PublicKey default: return nil } } func generateSerial() (uint64, error) { var b [8]byte if _, err := rand.Read(b[:]); err != nil { return 0, err } return binary.BigEndian.Uint64(b[:]), nil } func (e *SSHCAEngine) resolveTTL(requestedTTL, profileMaxTTL string) (time.Duration, error) { maxTTL, err := time.ParseDuration(e.config.MaxTTL) if err != nil { return 0, fmt.Errorf("sshca: invalid engine max_ttl: %w", err) } // Profile max_ttl overrides engine max_ttl if more restrictive. if profileMaxTTL != "" { profileMax, err := time.ParseDuration(profileMaxTTL) if err == nil && profileMax < maxTTL { maxTTL = profileMax } } if requestedTTL != "" { ttl, err := time.ParseDuration(requestedTTL) if err != nil { return 0, fmt.Errorf("sshca: invalid ttl: %w", err) } if ttl > maxTTL { return 0, fmt.Errorf("sshca: requested TTL %s exceeds maximum %s", ttl, maxTTL) } return ttl, nil } defaultTTL, err := time.ParseDuration(e.config.DefaultTTL) if err != nil { return 0, fmt.Errorf("sshca: invalid default_ttl: %w", err) } if defaultTTL > maxTTL { return maxTTL, nil } return defaultTTL, nil } func (e *SSHCAEngine) mountName() string { // mountPath is "engine/sshca/{name}/" — extract name. parts := strings.Split(strings.TrimSuffix(e.mountPath, "/"), "/") if len(parts) >= 3 { return parts[2] } return "" } func (e *SSHCAEngine) storeProfile(ctx context.Context, profile *SigningProfile) error { data, err := json.Marshal(profile) if err != nil { return fmt.Errorf("sshca: marshal profile: %w", err) } return e.barrier.Put(ctx, e.mountPath+"profiles/"+profile.Name+".json", data) } func (e *SSHCAEngine) loadProfile(ctx context.Context, name string) (*SigningProfile, error) { data, err := e.barrier.Get(ctx, e.mountPath+"profiles/"+name+".json") if err != nil { return nil, err } var profile SigningProfile if err := json.Unmarshal(data, &profile); err != nil { return nil, err } return &profile, nil } func (e *SSHCAEngine) storeCertRecord(ctx context.Context, record *CertRecord) error { data, err := json.Marshal(record) if err != nil { return fmt.Errorf("sshca: marshal cert record: %w", err) } serialStr := strconv.FormatUint(record.Serial, 10) return e.barrier.Put(ctx, e.mountPath+"certs/"+serialStr+".json", data) } func (e *SSHCAEngine) loadCertRecord(ctx context.Context, serialStr string) (*CertRecord, error) { data, err := e.barrier.Get(ctx, e.mountPath+"certs/"+serialStr+".json") if err != nil { return nil, err } var record CertRecord if err := json.Unmarshal(data, &record); err != nil { return nil, err } return &record, nil } func (e *SSHCAEngine) storeKRLVersion(ctx context.Context, version uint64) error { data, err := json.Marshal(map[string]uint64{"version": version}) if err != nil { return err } return e.barrier.Put(ctx, e.mountPath+"krl_version.json", data) } func (e *SSHCAEngine) loadKRLVersion(ctx context.Context) (uint64, error) { data, err := e.barrier.Get(ctx, e.mountPath+"krl_version.json") if err != nil { return 0, err } var v map[string]uint64 if err := json.Unmarshal(data, &v); err != nil { return 0, err } return v["version"], nil } func (e *SSHCAEngine) collectRevokedSerials(ctx context.Context) ([]uint64, error) { paths, err := e.barrier.List(ctx, e.mountPath+"certs/") if err != nil { return nil, nil } var serials []uint64 for _, p := range paths { if !strings.HasSuffix(p, ".json") { continue } serialStr := strings.TrimSuffix(p, ".json") record, err := e.loadCertRecord(ctx, serialStr) if err != nil { continue } if record.Revoked { serials = append(serials, record.Serial) } } return serials, nil } // buildKRL builds an OpenSSH KRL binary blob. // // Format: // // MAGIC = "OPENSSH_KRL\x00" (12 bytes) // VERSION = uint32(1) // KRL_VERSION = uint64 // GENERATED_DATE = uint64(unix timestamp) // FLAGS = uint64(0) // RESERVED = string (empty, length-prefixed) // COMMENT = string (empty, length-prefixed) // [Section: type=0x01 (KRL_SECTION_CERTIFICATES)] // CA key blob (length-prefixed) // [Subsection: type=0x20 (KRL_SECTION_CERT_SERIAL_LIST)] // Sorted uint64 serials func (e *SSHCAEngine) buildKRL(revokedSerials []uint64) []byte { var buf []byte // Magic. buf = append(buf, []byte("OPENSSH_KRL\x00")...) // Format version. buf = binary.BigEndian.AppendUint32(buf, 1) // KRL version. buf = binary.BigEndian.AppendUint64(buf, e.krlVersion) // Generated date. buf = binary.BigEndian.AppendUint64(buf, uint64(time.Now().Unix())) // Flags. buf = binary.BigEndian.AppendUint64(buf, 0) // Reserved (empty string). buf = binary.BigEndian.AppendUint32(buf, 0) // Comment (empty string). buf = binary.BigEndian.AppendUint32(buf, 0) if len(revokedSerials) > 0 && e.caSigner != nil { // Sort serials. sort.Slice(revokedSerials, func(i, j int) bool { return revokedSerials[i] < revokedSerials[j] }) // Build serial list subsection. var subsection []byte // Subsection type: 0x20 = KRL_SECTION_CERT_SERIAL_LIST. subsection = append(subsection, 0x20) // Subsection data: list of uint64 serials. var serialData []byte for _, s := range revokedSerials { serialData = binary.BigEndian.AppendUint64(serialData, s) } // Length-prefixed subsection data. subsection = binary.BigEndian.AppendUint32(subsection, uint32(len(serialData))) subsection = append(subsection, serialData...) // Build section. // Section type: 0x01 = KRL_SECTION_CERTIFICATES. buf = append(buf, 0x01) // Section data: CA key blob + subsections. var sectionData []byte // CA key blob (length-prefixed). caKeyBlob := e.caSigner.PublicKey().Marshal() sectionData = binary.BigEndian.AppendUint32(sectionData, uint32(len(caKeyBlob))) sectionData = append(sectionData, caKeyBlob...) sectionData = append(sectionData, subsection...) // Length-prefixed section data. buf = binary.BigEndian.AppendUint32(buf, uint32(len(sectionData))) buf = append(buf, sectionData...) } return buf } func certRecordToData(record *CertRecord) map[string]interface{} { principalsIface := make([]interface{}, len(record.Principals)) for i, p := range record.Principals { principalsIface[i] = p } data := map[string]interface{}{ "serial": strconv.FormatUint(record.Serial, 10), "cert_type": record.CertType, "principals": principalsIface, "cert_data": record.CertData, "key_id": record.KeyID, "issued_by": record.IssuedBy, "issued_at": record.IssuedAt.Format(time.RFC3339), "expires_at": record.ExpiresAt.Format(time.RFC3339), } if record.Profile != "" { data["profile"] = record.Profile } if record.Revoked { data["revoked"] = true data["revoked_at"] = record.RevokedAt.Format(time.RFC3339) data["revoked_by"] = record.RevokedBy } return data } func extractStringSlice(data map[string]interface{}, key string) []string { raw, ok := data[key] if !ok { return nil } switch v := raw.(type) { case []interface{}: result := make([]string, 0, len(v)) for _, item := range v { if s, ok := item.(string); ok { result = append(result, s) } } return result case []string: return v default: return nil } } func extractStringMap(data map[string]interface{}, key string) map[string]string { raw, ok := data[key] if !ok { return nil } switch v := raw.(type) { case map[string]interface{}: result := make(map[string]string, len(v)) for k, val := range v { if s, ok := val.(string); ok { result[k] = s } } return result case map[string]string: return v default: return nil } } func stringFromData(data map[string]interface{}, key string) string { v, _ := data[key].(string) return v } func contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false } // Ensure mcrypto import is used (for zeroize if needed in future). var _ = mcrypto.Zeroize