mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 17:12:04 +00:00
21aeed4f4e
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
162 lines
5.8 KiB
Go
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)
|
|
}
|
|
}
|