Files
shankar0123 21aeed4f4e legal: addlicense headers + normalize legacy variants (Phase 0 RED-4)
Phase 0 closure (Path B2, post-rewrite):

addlicense sweep — adds the canonical certctl LLC copyright + BUSL-1.1
SPDX header to every production Go file. Template:

  // Copyright 2026 certctl LLC. All rights reserved.
  // SPDX-License-Identifier: BUSL-1.1

Coverage: 338 / 338 production Go files (cmd/ + internal/, excluding
*_test.go and **/testdata/**). Pre-sweep coverage was 22 / 338 (6.5%);
post-sweep is 338 / 338 (100%).

Normalized 22 pre-existing legacy headers (`// Copyright (c) certctl`
+ `// SPDX-License-Identifier: BSL-1.1`) and 1 file using a
`Certctl Contributors` attribution. The legacy SPDX ID `BSL-1.1`
is non-standard; the official SPDX identifier for Business Source
License 1.1 is `BUSL-1.1` (capital U). All 338 files now share the
canonical form.

Generated via:
  addlicense -c "certctl LLC" -y 2026 \
    -f cowork/legal/copyright-header.tpl \
    -ignore '**/testdata/**' -ignore '**/*_test.go' \
    cmd/ internal/

Verification:
  find cmd internal -name '*.go' -not -name '*_test.go' \
    -not -path '*/testdata/*' \
    -exec grep -L '^// Copyright 2026 certctl LLC' {} \; | wc -l

  Returns: 0

gofmt clean. Header additions are comments only, no compile impact.

Closes: cowork/certctl-architecture-diligence-audit.html#fix-RED-4
2026-05-13 21:23:35 +00:00

162 lines
5.8 KiB
Go

// Copyright 2026 certctl LLC. All rights reserved.
// SPDX-License-Identifier: BUSL-1.1
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)
}
}