// Package cert managed die Supervision-Identity (Cert + Private-Key). // Cert wird einmal generiert via go-ios und persistent unter // ~/.rebreak-supervise/ abgelegt. Default-Path matched die existing // Bootstrap-Tool-Konvention. package cert import ( "crypto/rsa" "crypto/x509" "encoding/pem" "errors" "fmt" "os" "path/filepath" ios "github.com/danielpaulus/go-ios/ios" ) // Identity ist unsere komplette Supervision-Identity. // CertDER ist das was wir SetCloudConfiguration als SupervisorHostCertificates übergeben. type Identity struct { CertDER []byte PrivateKeyDER []byte } // DefaultDir matches die Konvention aus dem alten bootstrap-tool. func DefaultDir() string { home, _ := os.UserHomeDir() return filepath.Join(home, ".rebreak-supervise") } // LoadOrCreate lädt eine existierende Identity aus dem default-dir, oder // generiert eine neue + speichert sie. Idempotent: zweiter Call returnt // die gleiche Identity wie der erste. func LoadOrCreate() (*Identity, error) { dir := DefaultDir() certPath := filepath.Join(dir, "supervision-cert.pem") keyPath := filepath.Join(dir, "supervision-key.pem") id, err := load(certPath, keyPath) if err == nil { return id, nil } if !os.IsNotExist(err) && !errors.Is(err, errCorruptIdentity) { return nil, fmt.Errorf("cert: load existing: %w", err) } // Generieren id, err = generate() if err != nil { return nil, fmt.Errorf("cert: generate: %w", err) } if err := id.save(certPath, keyPath); err != nil { return nil, fmt.Errorf("cert: save: %w", err) } return id, nil } // load aus PEM-Files. func load(certPath, keyPath string) (*Identity, error) { certPEM, err := os.ReadFile(certPath) if err != nil { return nil, err } keyPEM, err := os.ReadFile(keyPath) if err != nil { return nil, err } certBlock, _ := pem.Decode(certPEM) keyBlock, _ := pem.Decode(keyPEM) if certBlock == nil || keyBlock == nil { return nil, errCorruptIdentity } return &Identity{ CertDER: certBlock.Bytes, PrivateKeyDER: keyBlock.Bytes, }, nil } // generate via go-ios's CreateDERFormattedSupervisionCert. func generate() (*Identity, error) { ca, err := ios.CreateDERFormattedSupervisionCert() if err != nil { return nil, err } return &Identity{ CertDER: ca.CertDER, PrivateKeyDER: ca.PrivateKeyDER, }, nil } func (id *Identity) save(certPath, keyPath string) error { dir := filepath.Dir(certPath) if err := os.MkdirAll(dir, 0o700); err != nil { return err } certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: id.CertDER}) keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: id.PrivateKeyDER}) if err := os.WriteFile(certPath, certPEM, 0o600); err != nil { return err } if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil { return err } return nil } var errCorruptIdentity = errors.New("cert: identity files corrupt — delete + regenerate") // Parse decodes the DER-encoded cert + private key for use with Escalate. func (id *Identity) Parse() (*x509.Certificate, *rsa.PrivateKey, error) { cert, err := x509.ParseCertificate(id.CertDER) if err != nil { return nil, nil, fmt.Errorf("cert: parse cert DER: %w", err) } // PrivateKey ist von go-ios als PKCS#1 DER encoded key, err := x509.ParsePKCS1PrivateKey(id.PrivateKeyDER) if err != nil { // Fallback: PKCS#8 k8, err2 := x509.ParsePKCS8PrivateKey(id.PrivateKeyDER) if err2 != nil { return nil, nil, fmt.Errorf("cert: parse key DER (PKCS1 + PKCS8 failed): %w / %v", err, err2) } rsaKey, ok := k8.(*rsa.PrivateKey) if !ok { return nil, nil, fmt.Errorf("cert: key is not RSA") } key = rsaKey } return cert, key, nil }