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

69 lines
2.7 KiB
Go

package signer
import (
"crypto"
"encoding/pem"
"fmt"
"crypto/x509"
)
// parsePrivateKey parses a PEM block into a crypto.Signer. Recognises the
// three PEM block types historically produced and consumed by certctl's
// local CA:
//
// - "RSA PRIVATE KEY" (PKCS#1 / RFC 3447, openssl genrsa default)
// - "EC PRIVATE KEY" (SEC 1 / RFC 5915, openssl ecparam default)
// - "PRIVATE KEY" (PKCS#8 / RFC 5208 — wraps RSA, ECDSA, others)
//
// This function is the single source of truth for PEM private-key parsing
// inside certctl. It was moved here from
// internal/connector/issuer/local/local.go as part of the Signer
// abstraction work; the local package now calls into here. Do not
// reintroduce a parallel implementation elsewhere.
//
// Behavior preserved exactly across the move:
// - Block type matching is case-sensitive (PEM convention).
// - PKCS#8 blocks that contain a non-Signer key (e.g., a Diffie-Hellman
// key, an Ed25519 key absent stdlib Signer support) return an error
// rather than a panic.
// - The error wrapping format is intentionally stable so existing test
// assertions in internal/connector/issuer/local/local_test.go and
// bundle9_coverage_test.go continue to match without modification.
func parsePrivateKey(block *pem.Block) (crypto.Signer, error) {
switch block.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
case "PRIVATE KEY":
// PKCS#8 — can contain RSA or ECDSA
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse PKCS#8 key: %w", err)
}
signer, ok := key.(crypto.Signer)
if !ok {
return nil, fmt.Errorf("PKCS#8 key is not a signing key")
}
return signer, nil
default:
return nil, fmt.Errorf("unsupported private key type: %s (expected RSA PRIVATE KEY, EC PRIVATE KEY, or PRIVATE KEY)", block.Type)
}
}
// ParsePrivateKey is the exported wrapper used by callers outside this
// package. It exists so that internal/connector/issuer/local/ (and any
// future caller that needs to load a PEM private key without going
// through a Driver — e.g., a one-off tool, a migration helper) can
// share the parser without re-implementing the block-type dispatch.
//
// Most callers should use a Driver instead — Driver.Load handles the
// file-read + PEM decode + key parse + Signer wrap in one call.
// ParsePrivateKey is exposed for the corner cases where a caller
// already holds the *pem.Block (e.g., the block was extracted from a
// multi-block PEM bundle).
func ParsePrivateKey(block *pem.Block) (crypto.Signer, error) {
return parsePrivateKey(block)
}