package signer import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rsa" "crypto/x509" "errors" "fmt" "io" ) // Signer extends crypto.Signer with an Algorithm method that lets callers // pick the matching x509.SignatureAlgorithm without reflecting on the key. // // Implementations MUST satisfy the crypto.Signer contract: Public() returns // the matching public key, and Sign(rand, digest, opts) produces a // signature in the algorithm's standard wire format (PKCS#1 v1.5 / PSS for // RSA, ASN.1 DER-encoded ECDSA-Sig-Value for ECDSA). The Algorithm method // is purely a metadata accessor — it MUST NOT cause I/O. type Signer interface { crypto.Signer Algorithm() Algorithm } // Algorithm enumerates the certctl-supported signing algorithms. // // The set is deliberately small. Adding an algorithm requires updating // signer.go's enum, parse.go's algorithmFromKey, the SignatureAlgorithm // helper below, and the corresponding profile validators in // internal/service that gate operator-facing key-policy choices. Do not // add Ed25519 (or any new algorithm) without that full sweep — the // half-implemented case is worse than the absent case. type Algorithm string // Algorithm constants enumerate the certctl-supported signing algorithms. // Wire-format strings match the operator-facing values used in // CertificateProfile validators so the values are stable across the // audit/policy/connector boundary. const ( // AlgorithmRSA2048 is RSA with a 2048-bit modulus. AlgorithmRSA2048 Algorithm = "RSA-2048" // AlgorithmRSA3072 is RSA with a 3072-bit modulus. AlgorithmRSA3072 Algorithm = "RSA-3072" // AlgorithmRSA4096 is RSA with a 4096-bit modulus. AlgorithmRSA4096 Algorithm = "RSA-4096" // AlgorithmECDSAP256 is ECDSA over the NIST P-256 (secp256r1) curve. AlgorithmECDSAP256 Algorithm = "ECDSA-P256" // AlgorithmECDSAP384 is ECDSA over the NIST P-384 (secp384r1) curve. AlgorithmECDSAP384 Algorithm = "ECDSA-P384" ) // ErrUnsupportedAlgorithm is returned when a key uses a curve, modulus, // or type the signer package does not recognize. Callers can use // errors.Is to distinguish this from other failure modes. var ErrUnsupportedAlgorithm = errors.New("signer: unsupported key algorithm") // SignatureAlgorithm maps a Signer's Algorithm to the matching // x509.SignatureAlgorithm. Used by call sites that build cert / CRL / // OCSP templates so they don't have to do their own type-switch. // // Returns x509.UnknownSignatureAlgorithm for unrecognized inputs; // callers SHOULD treat that as a bug (the only supported values are the // constants above). func SignatureAlgorithm(a Algorithm) x509.SignatureAlgorithm { switch a { case AlgorithmRSA2048, AlgorithmRSA3072, AlgorithmRSA4096: return x509.SHA256WithRSA case AlgorithmECDSAP256: return x509.ECDSAWithSHA256 case AlgorithmECDSAP384: return x509.ECDSAWithSHA384 default: return x509.UnknownSignatureAlgorithm } } // Wrap adapts a stdlib crypto.Signer into a signer.Signer by inferring // the Algorithm from the key's public half. Returns ErrUnsupportedAlgorithm // (wrapped with key-shape detail) for keys outside the supported enum. // // This is the canonical adapter used by every Driver in this package // and by callers that already hold a crypto.Signer (e.g., a key parsed // elsewhere). Drivers SHOULD NOT implement Signer from scratch; wrapping // keeps the Algorithm-detection logic in one place. func Wrap(s crypto.Signer) (Signer, error) { if s == nil { return nil, fmt.Errorf("signer.Wrap: nil signer") } alg, err := algorithmFromKey(s.Public()) if err != nil { return nil, err } return &wrappedSigner{inner: s, alg: alg}, nil } // wrappedSigner is the concrete type returned by Wrap. It is unexported // so the only path to a Signer is through Wrap (or a Driver that calls // Wrap internally) — that keeps Algorithm()'s value-semantics consistent. type wrappedSigner struct { inner crypto.Signer alg Algorithm } func (w *wrappedSigner) Public() crypto.PublicKey { return w.inner.Public() } func (w *wrappedSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { return w.inner.Sign(rand, digest, opts) } func (w *wrappedSigner) Algorithm() Algorithm { return w.alg } // algorithmFromKey infers the Algorithm enum value from a public key. // Used by Wrap; exported via the Signer contract through Algorithm(). // // Bounds-checked against the enum exactly: an RSA-1024 key returns // ErrUnsupportedAlgorithm even though it would otherwise satisfy // crypto.Signer — the local CA never produces RSA-1024 and operators // importing such a key into a sub-CA path should fail loudly at load // time, not at first-sign time. func algorithmFromKey(pub crypto.PublicKey) (Algorithm, error) { switch k := pub.(type) { case *rsa.PublicKey: switch k.N.BitLen() { case 2048: return AlgorithmRSA2048, nil case 3072: return AlgorithmRSA3072, nil case 4096: return AlgorithmRSA4096, nil default: return "", fmt.Errorf("%w: RSA modulus %d bits (supported: 2048, 3072, 4096)", ErrUnsupportedAlgorithm, k.N.BitLen()) } case *ecdsa.PublicKey: switch k.Curve { case elliptic.P256(): return AlgorithmECDSAP256, nil case elliptic.P384(): return AlgorithmECDSAP384, nil default: // ecdsa.PublicKey embeds elliptic.Curve, so Params() resolves // through the embedded field. Spelled this way to satisfy // staticcheck QF1008 (could remove embedded field "Curve" from // selector); functionally identical to k.Curve.Params(). name := "unknown" if p := k.Params(); p != nil { name = p.Name } return "", fmt.Errorf("%w: ECDSA curve %s (supported: P-256, P-384)", ErrUnsupportedAlgorithm, name) } default: return "", fmt.Errorf("%w: %T (supported: *rsa.PublicKey, *ecdsa.PublicKey)", ErrUnsupportedAlgorithm, pub) } }