package security import ( "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "errors" "fmt" "net" ) // GenerateCSR mints a new RSA private key, builds a CSR with the given // commonName and SANs (DNS or IP entries), and returns the key + CSR in // PEM form. The private key is RSA 3072 (matches CAKeySize). // // REQ-036: server certs MUST have at least one DNS or IP SAN. This function // enforces that constraint — calling with empty sans returns an error. // // Validation: dns entries must be syntactically valid hostnames; ip entries // must be parseable by net.ParseIP. Bad inputs are rejected up-front so // the operator gets a clear error before signing. // // Deprecated: v0.9 re-architecture replaces the internal CA with step-ca // (D-101/REQ-076). GenerateCSR is retained for the dual-write window and // scheduled for deletion in v0.10-P14. See .ciagent/PRD_v0.9.md. func GenerateCSR(commonName string, sans []string) (keyPEM, csrPEM []byte, err error) { if commonName == "" { return nil, nil, errors.New("GenerateCSR: commonName is required") } if len(sans) == 0 { return nil, nil, errors.New("GenerateCSR: at least one DNS or IP SAN is required (REQ-036)") } dnsNames := make([]string, 0, len(sans)) ipAddrs := make([]net.IP, 0, len(sans)) for _, s := range sans { if s == "" { return nil, nil, errors.New("GenerateCSR: empty SAN entry") } if ip := net.ParseIP(s); ip != nil { ipAddrs = append(ipAddrs, ip) continue } // Treat as a DNS name. Validate it parses and is not a host:port form. if _, _, err := net.SplitHostPort(s); err == nil { return nil, nil, fmt.Errorf("GenerateCSR: SAN %q looks like host:port; use a bare hostname or IP", s) } dnsNames = append(dnsNames, s) } if len(dnsNames) == 0 && len(ipAddrs) == 0 { return nil, nil, errors.New("GenerateCSR: at least one valid DNS or IP SAN is required (REQ-036)") } key, err := rsa.GenerateKey(rand.Reader, CAKeySize) if err != nil { return nil, nil, fmt.Errorf("GenerateCSR: generate key: %w", err) } csr := &x509.CertificateRequest{ Subject: pkix.Name{ CommonName: commonName, Organization: []string{"orca"}, }, DNSNames: dnsNames, IPAddresses: ipAddrs, } csrDER, err := x509.CreateCertificateRequest(rand.Reader, csr, key) if err != nil { return nil, nil, fmt.Errorf("GenerateCSR: create CSR: %w", err) } keyDER, err := x509.MarshalPKCS8PrivateKey(key) if err != nil { return nil, nil, fmt.Errorf("GenerateCSR: marshal key: %w", err) } keyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}) csrPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}) return keyPEM, csrPEM, nil }