Files
certctl/internal/crypto/signer/memory_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

126 lines
3.9 KiB
Go

package signer
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"errors"
"fmt"
"sync"
)
// MemoryDriver holds keys in process memory. It is intended for tests
// that need a Signer-shaped object without touching the filesystem
// or any external infrastructure. It is NOT for production use:
// keys disappear when the process exits, no hardening of any kind is
// applied, and concurrent Generate calls have no rate limit.
//
// The driver is safe for concurrent use; an internal mutex guards the
// keys map.
type MemoryDriver struct {
mu sync.Mutex
keys map[string]crypto.Signer
// nextID is incremented on every successful Generate; the returned
// ref string is "mem-<nextID>" so multiple Generates produce
// distinct refs even when callers don't supply one.
nextID int
}
// NewMemoryDriver returns a freshly initialized MemoryDriver. Callers
// holding multiple drivers can rely on each one being independent —
// keys from driver A are not visible to driver B.
func NewMemoryDriver() *MemoryDriver {
return &MemoryDriver{keys: map[string]crypto.Signer{}}
}
// Name implements Driver.
func (d *MemoryDriver) Name() string { return "memory" }
// Load implements Driver. Returns the Signer for the given ref, or an
// error if the ref was never produced by Generate / Adopt.
func (d *MemoryDriver) Load(ctx context.Context, ref string) (Signer, error) {
if ref == "" {
return nil, errors.New("signer.MemoryDriver.Load: empty ref")
}
d.mu.Lock()
defer d.mu.Unlock()
key, ok := d.keys[ref]
if !ok {
return nil, fmt.Errorf("signer.MemoryDriver.Load: unknown ref %q", ref)
}
return Wrap(key)
}
// Generate implements Driver. Creates a fresh in-memory key with the
// requested algorithm and returns the wrapped Signer plus the ref
// string callers can pass to a subsequent Load.
func (d *MemoryDriver) Generate(ctx context.Context, alg Algorithm) (Signer, string, error) {
if err := ctx.Err(); err != nil {
return nil, "", fmt.Errorf("signer.MemoryDriver.Generate: %w", err)
}
var key crypto.Signer
switch alg {
case AlgorithmRSA2048, AlgorithmRSA3072, AlgorithmRSA4096:
bits := rsaBitsFor(alg)
k, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
return nil, "", fmt.Errorf("signer.MemoryDriver.Generate: rsa keygen %d: %w", bits, err)
}
key = k
case AlgorithmECDSAP256, AlgorithmECDSAP384:
curve := ecCurveFor(alg)
k, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, "", fmt.Errorf("signer.MemoryDriver.Generate: ecdsa keygen %s: %w", curve.Params().Name, err)
}
key = k
default:
return nil, "", fmt.Errorf("signer.MemoryDriver.Generate: %w: %s", ErrUnsupportedAlgorithm, alg)
}
d.mu.Lock()
d.nextID++
ref := fmt.Sprintf("mem-%d", d.nextID)
d.keys[ref] = key
d.mu.Unlock()
wrapped, err := Wrap(key)
if err != nil {
return nil, "", fmt.Errorf("signer.MemoryDriver.Generate: wrap: %w", err)
}
return wrapped, ref, nil
}
// Adopt registers an externally-generated crypto.Signer under ref so
// subsequent Load calls return it. Returns an error if ref is already
// taken — keep refs unique to avoid silent override surprises.
//
// Useful in tests that want a deterministic key (generated outside
// the driver, e.g. from a fixed PEM fixture) reachable through the
// driver.
func (d *MemoryDriver) Adopt(ref string, key crypto.Signer) error {
if ref == "" {
return errors.New("signer.MemoryDriver.Adopt: empty ref")
}
if key == nil {
return errors.New("signer.MemoryDriver.Adopt: nil key")
}
d.mu.Lock()
defer d.mu.Unlock()
if _, exists := d.keys[ref]; exists {
return fmt.Errorf("signer.MemoryDriver.Adopt: ref %q already exists", ref)
}
d.keys[ref] = key
return nil
}
// _ guards that MemoryDriver implements Driver (catch interface drift
// at build time, not test time).
var _ Driver = (*MemoryDriver)(nil)
// _ guards that FileDriver implements Driver.
var _ Driver = (*FileDriver)(nil)