Files
certctl/internal/crypto/signer/driver.go
T
shankar0123 9039cef390 crypto/signer: introduce Signer interface; refactor local issuer to use it
This is a load-bearing internal refactor with no user-visible behavior
change. The new internal/crypto/signer package abstracts CA private-key
signing behind a Signer interface (embeds stdlib crypto.Signer + adds
Algorithm()). The local issuer now consumes this interface; the
historical c.caKey crypto.Signer field is renamed c.caSigner signer.Signer.

What landed:

  * internal/crypto/signer/ — new stdlib-only package
    - Signer interface: crypto.Signer + Algorithm()
    - Algorithm enum: RSA-2048, RSA-3072, RSA-4096, ECDSA-P256, ECDSA-P384
    - Driver interface: Load / Generate / Name
    - FileDriver: production driver, wraps file-on-disk PEM, hooks for
      DirHardener + Marshaler so the local package can inject Bundle 9
      keystore.ensureKeyDirSecure + keymem.marshalPrivateKeyAndZeroize
    - MemoryDriver: in-memory test driver; safe for concurrent use
    - parse.go: ParsePrivateKey moved here from local.go (PKCS#1, SEC 1, PKCS#8)
    - 91.6% coverage (gate ≥85)

  * internal/connector/issuer/local/local.go — refactor
    - Rename c.caKey crypto.Signer → c.caSigner signer.Signer
    - Rewire 4 signing call sites: leaf cert (line ~613), CRL (~849),
      OCSP response (~887), CA bootstrap (~482) — all access the
      interface; the bootstrap also switches to interface-level
      Public() + Signer
    - Wrap freshly-generated and freshly-loaded keys; reject Ed25519
      and other unsupported algorithms at load time (was silently
      accepted before, would have failed at first sign)
    - Delete the duplicated parsePrivateKey helper (single source of
      truth now lives in the signer package)
    - Update the L-014 threat-model comment block (lines 1-29) with a
      forward-reference paragraph: file-on-disk caveats apply only to
      FileDriver-backed signers; alternative drivers close that leg
    - Coverage 86.7 → 86.5 (above CI floor of 86); the 0.2pp drop is
      mechanical from deleting parsePrivateKey, partially recovered by
      a new test pinning the Wrap error path

  * internal/crypto/signer/equivalence_test.go — Phase 3 safety net
    - RSA byte-strict equality for leaf certs / CRLs / OCSP responses
      (PKCS#1 v1.5 is deterministic)
    - ECDSA TBS-strict equality (signature differs because of random k)
    - Both signatures independently validate against the CA
    - Negative sentinel proves the equivalence checker isn't trivially-
      passing

  * docs/architecture.md — new 'CA Signing Abstraction' section under
    Security Model, with ASCII diagram of FileDriver / MemoryDriver /
    future PKCS11Driver / future CloudKMSDriver

  * Test file mechanical edits (only):
    - bundle9_coverage_test.go: parsePrivateKey → signer.ParsePrivateKey
      (function moved, not behavior changed)
    - local_test.go: append one targeted test
      (TestSubCA_LoadCAFromDisk_RejectsUnsupportedKeyAlgorithm) that
      pins the new Wrap error path I introduced — recovers coverage
      cost of the deletion above

What did NOT change (verified empty diffs):
  * api/openapi.yaml
  * migrations/
  * internal/connector/issuer/interface.go
  * go.mod / go.sum (no new dependencies; stdlib only)

This refactor is the prerequisite for three downstream items:
  - PKCS#11/HSM driver (V3-Pro)
  - CRL/OCSP responder (V2)
  - SSH CA lifecycle (V2)

Each of those adds a new signing call site. Doing the abstraction now
costs once; deferring would cost three times.
2026-04-28 22:03:55 +00:00

55 lines
2.4 KiB
Go

package signer
import "context"
// Driver knows how to materialize a Signer from some external reference
// (a file path, a PKCS#11 URI, a cloud KMS key ID, etc.) and how to
// generate a fresh key with a given algorithm.
//
// Drivers are responsible for any side-effect storage: FileDriver writes
// generated keys to disk via the keystore.ensureKeyDirSecure +
// keymem.marshalPrivateKeyAndZeroize discipline (injected via the
// FileDriver's hooks); future PKCS11Driver delegates key generation to
// the token; cloud-KMS drivers call the provider API.
//
// All Driver methods take a context.Context for cancellation/deadline
// propagation. Drivers MUST honor ctx.Done() for any I/O they perform;
// purely-in-memory drivers (MemoryDriver) may return immediately
// regardless of ctx state.
//
// Adding a new driver does NOT require changing this interface or any
// existing driver. The driver lives in its own package
// (internal/crypto/signer/<name>) and is constructed by a typed
// factory (e.g., pkcs11.New(config)).
type Driver interface {
// Load resolves an existing key from ref and returns a Signer.
// ref interpretation is driver-specific:
//
// - FileDriver: filesystem path to a PEM-encoded private key
// - PKCS11Driver (future): pkcs11: URI per RFC 7512
// - CloudKMSDriver (future): provider-specific resource name
//
// Drivers MUST NOT log the contents of the loaded key (only the
// ref + Algorithm). Callers wrap the returned Signer's Sign method
// in their own logging if they need per-signature audit trail.
Load(ctx context.Context, ref string) (Signer, error)
// Generate creates a new key with the given algorithm and persists
// it to driver-specific storage (or in-memory for MemoryDriver).
// Returns a Signer wrapping the new key plus a ref string the
// caller passes to a subsequent Load call (e.g., the file path
// for FileDriver, the PKCS#11 URI for PKCS11Driver).
//
// If alg is not in the supported enum, Generate returns
// ErrUnsupportedAlgorithm without side effects (no file written,
// no token slot consumed).
Generate(ctx context.Context, alg Algorithm) (Signer, string, error)
// Name returns a stable identifier for the driver type. Used in
// structured logs and (eventually) in CRL distribution-point URLs
// when the URL embeds the signer kind. MUST be a single
// lowercase token without spaces ("file", "memory", "pkcs11",
// "aws-kms", "gcp-kms", "azure-kv").
Name() string
}