diff --git a/.claude/docs/architecture.md b/.claude/docs/architecture.md index dfa5839..665b32e 100644 --- a/.claude/docs/architecture.md +++ b/.claude/docs/architecture.md @@ -72,7 +72,7 @@ Thin CLI layer. Each file is one Cobra command. Flag variables are package-level - `probe_ssh.go` — `probe ssh` subcommand. Connects without authenticating, prints banner/algorithm details, and supports `--fips-140-2` / `--fips-140-3` policy heuristics for SSH transport algorithms. - `policy.go` — Shared CLI flag-to-policy selection helper used by `connect` and `probe ssh`. - `sign.go` — Sign certificates. Parent command with `self-signed` and `csr` subcommands for creating self-signed certs and signing CSRs with a CA. -- `tree.go` — `tree` subcommand. Renders the full Cobra command graph, including built-in commands plus local and inherited flags accepted by each command; honors the global `--json` flag with structured command/flag output. +- `tree.go` — `tree` subcommand. Renders the full Cobra command graph with a command-focused default text view; `--flags` and `--inherited` opt into text-mode flag detail, and the global `--json` flag still returns the structured command/flag surface. - `ocsp.go` — Check certificate revocation status via OCSP; `--format` flag. - `crl.go` — Parse and inspect Certificate Revocation Lists; `--check` to verify a cert against the CRL; `--format` flag. - `convert.go` — Convert certificates and keys between PEM, DER, PKCS#12, JKS, and PKCS#7 formats; `--to`, `-o` flags. diff --git a/CHANGELOG.md b/CHANGELOG.md index c3e0460..0ead276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Parse extensionless or renamed DER/PKCS#7/PKCS#12/JKS inputs during certstore ingestion instead of skipping binary crypto content based on filename extension alone ([#172]) - Harden browser/WASM AIA and upload handling by blocking obvious internal hostnames without DNS, streaming AIA response-size enforcement before buffering, and rejecting oversized uploads before `arrayBuffer()` reads them into JS memory ([#172]) - Stop draining oversized `tar.gz` members after the size violation is already known, treat `gh api graphql` string variables literally in `pr-comments.py`, and add JSON output support for `certkit tree --json` so the global flag matches documented behavior ([#172]) +- Make `certkit tree` default to a command-focused text view, with `--flags` and `--inherited` available when you want flag detail in text mode ([#172]) - Encrypt private key in YAML bundle output (`.yaml`) when an export password is supplied; previously leaked plaintext key ([#167]) - Reject malformed `ENCRYPTED PRIVATE KEY` blocks with invalid AES IV length instead of panicking ([#167]) - Trim whitespace from web UI export password so whitespace-only input is treated as blank ([#167]) diff --git a/EXAMPLES.md b/EXAMPLES.md index 5d5713e..ef93644 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -74,8 +74,7 @@ A practical guide to common certificate tasks. No prior TLS/SSL knowledge requir ### See the full command surface -Print the actual CLI command tree, including built-in Cobra commands and the -flags each command accepts: +Print the actual CLI command tree, including built-in Cobra commands: ```sh certkit tree @@ -84,6 +83,12 @@ certkit tree This is useful when you want a quick map of the CLI without hopping through `--help` output command by command. +If you also want the flag surface, opt in explicitly: + +```sh +certkit tree --flags --inherited +``` + --- ## Inspecting diff --git a/RALPH.md b/RALPH.md index ecdef55..61eb184 100644 --- a/RALPH.md +++ b/RALPH.md @@ -231,3 +231,38 @@ Area: [internal/certstore/process_test.go](internal/certstore/process_test.go) Summary: direct unit tests of unexported algorithm-conversion helpers were too implementation-coupled for the compat parser behavior they were intended to protect. Source: Follow-up PR review Fix: Removed the helper-specific tests and asserted public-key/signature algorithm mapping through the compatibility ingestion path instead + +33. `tree-default-flags-noise` +Status: fixed +Area: [cmd/certkit/tree.go](cmd/certkit/tree.go), [cmd/certkit/cli_semantics_test.go](cmd/certkit/cli_semantics_test.go), [README.md](README.md), [EXAMPLES.md](EXAMPLES.md) +Summary: the default `tree` output included every local and inherited flag, which made the command map harder to scan than a command-focused tree. +Source: User feedback +Fix: `tree` now defaults to commands-only text output, with `--flags` and `--inherited` opt-ins for text-mode flag detail; JSON output remains the detailed machine-readable surface + +34. `validate-duplicate-ski-fallback` +Status: fixed +Area: [internal/certstore/validate.go](internal/certstore/validate.go), [internal/certstore/validate_test.go](internal/certstore/validate_test.go) +Summary: validation errored on duplicate-SKI renewal sets even though the rest of the store/UI already presents one latest-expiring certificate per SKI. +Source: Follow-up PR review +Fix: `RunValidation()` now uses the store's latest-cert selection for a given SKI, and the regression test proves the later renewal is the record that gets validated + +35. `readme-tail-noise-and-command-overclaim` +Status: fixed +Area: [README.md](README.md) +Summary: the README command index overclaimed the full CLI surface and the end-of-file mermaid diagram added noise without carrying much durable documentation value. +Source: User feedback +Fix: narrowed the section to “Common Commands”, clarified the `tree` entry, removed the low-value diagram, and kept the useful scan behavior notes as prose + +36. `validate-latest-fallback-assertion-gap` +Status: fixed +Area: [internal/certstore/validate_test.go](internal/certstore/validate_test.go) +Summary: the duplicate-SKI regression only asserted the selected subject, so it could still pass if validation picked the wrong renewal by insertion order instead of latest `NotAfter`. +Source: Follow-up PR review +Fix: pinned the expected later renewal `NotAfter` and asserted the exact RFC3339 result emitted by `RunValidation()` + +37. `readme-followup-runtime-drift` +Status: fixed +Area: [README.md](README.md) +Summary: several README details had drifted from current behavior, including `tree` flag wording, CRL URL schemes, duplicate bundle export paths, library `SignCSR` SAN copying, and scan issuer-linkage notes. +Source: Follow-up docs audit finding +Fix: aligned the README text and library example with the current command/runtime behavior diff --git a/README.md b/README.md index 47f0eaf..9217985 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ certkit bundle cert.pem -o chain.pem See [EXAMPLES.md](EXAMPLES.md) for a walkthrough of the main certificate workflows and real-world scenarios. -## Commands +## Common Commands | Command | What it does | | --------------------------- | ------------------------------------------------------- | @@ -110,7 +110,7 @@ See [EXAMPLES.md](EXAMPLES.md) for a walkthrough of the main certificate workflo | `certkit sign self-signed` | Create a self-signed certificate | | `certkit sign csr ` | Sign a CSR with a CA certificate and key | | `certkit scan ` | Scan a directory and catalog everything found | -| `certkit tree` | Print the full CLI command and flag surface as a tree | +| `certkit tree` | Print the full CLI command tree (`--flags`/`--inherited` for details) | | `certkit keygen` | Generate a new key pair (and optionally a CSR) | | `certkit csr` | Generate a CSR from a template, cert, or existing CSR | | `certkit ocsp ` | Check certificate revocation status via OCSP | @@ -319,7 +319,7 @@ The OCSP responder URL is read from the certificate's AIA extension. | `--format` | `text` | Output format: text, json | -Accepts local files (PEM or DER) or HTTP URLs. +Accepts local files (PEM or DER) or HTTP/HTTPS URLs. ### Exit Codes @@ -363,7 +363,7 @@ Bundles without an explicit `subject` block inherit from `defaultSubject`. Certi ### Bundle Output Files -When running `certkit scan --bundle-path`, each bundle produces the following files under `//`: +When running `certkit scan --bundle-path`, each bundle produces the following files under `//`. If `--duplicates` keeps older matching certificates, those extra exports are written under suffixed directories like `__/`: | File | Contents | | ------------------------ | ------------------------------------------------------------------------------------- | @@ -417,7 +417,7 @@ csrPEM, keyPEM, _ := certkit.GenerateCSR(leaf, nil) // auto-generates EC P-256 k // Sign certificates selfSigned, _ := certkit.CreateSelfSigned(certkit.SelfSignedInput{Signer: caKey, Subject: pkix.Name{CommonName: "My CA"}, IsCA: true}) -issued, _ := certkit.SignCSR(certkit.SignCSRInput{CSR: csr, CACert: caCert, CAKey: caKey, Days: 365}) +issued, _ := certkit.SignCSR(certkit.SignCSRInput{CSR: csr, CACert: caCert, CAKey: caKey, Days: 365, CopySANs: true}) // TLS connection probing result, _ := certkit.ConnectTLS(ctx, certkit.ConnectTLSInput{Host: "example.com"}) @@ -434,17 +434,6 @@ jks, _ := certkit.EncodeJKS(key, leaf, intermediates, "changeit") encPEM, _ := certkit.MarshalEncryptedPrivateKeyToPEM(key, "secret") ``` -### How It Works - -```mermaid -flowchart TD - A[Input files / stdin] --> B[Format detection - PEM vs DER] - B --> C[Parse certs, keys, CSRs
PKCS#12, PKCS#7, JKS, encrypted PEM, PKCS#8, SEC1, Ed25519] - C --> D[Catalog in MemStore
certificates + keys indexed by SKI] - D --> E[Resolve AKIs
match legacy SHA-1 AKIs to computed RFC 7093 M1 SKIs] - E --> F{--bundle-path?} - F -- yes --> G[Match keys to certs, build chains,
write all output formats per bundle] - F -- no --> H[Print scan summary] -``` +### Scan Notes -Expired certificates are always ingested; expiry filtering is output-only (`--allow-expired` overrides). SKI computation uses RFC 7093 Method 1 (SHA-256 truncated to 160 bits). Non-root certificate AKIs are resolved post-ingestion by matching against a multi-hash lookup (RFC 7093 M1 + legacy SHA-1) of all CA certificates. +Expired certificates are always ingested; expiry filtering is output-only (`--allow-expired` overrides). SKI computation uses RFC 7093 Method 1 (SHA-256 truncated to 160 bits). Non-root issuer linkage is resolved after ingestion by checking for raw ASN.1 subject/issuer matches among CA certificates and falling back to AIA fetching when the issuer is still missing. diff --git a/cmd/certkit/cli_semantics_test.go b/cmd/certkit/cli_semantics_test.go index 227aa03..b54c2e2 100644 --- a/cmd/certkit/cli_semantics_test.go +++ b/cmd/certkit/cli_semantics_test.go @@ -144,15 +144,23 @@ func TestTreeCommand(t *testing.T) { } }) - t.Run("includes help and version flags", func(t *testing.T) { + t.Run("default text output omits flags", func(t *testing.T) { for _, flag := range []string{"--help", "--version"} { - if !strings.Contains(rootOutput, flag) { - t.Errorf("tree output missing flag %q", flag) + if strings.Contains(rootOutput, flag) { + t.Errorf("default tree output should omit flag %q", flag) } } }) - t.Run("includes subcommand help and inherited global flags", func(t *testing.T) { + t.Run("includes subcommand help and inherited global flags when requested", func(t *testing.T) { + prevTreeIncludeFlags := treeIncludeFlags + prevTreeIncludeInherited := treeIncludeInherited + t.Cleanup(func() { + treeIncludeFlags = prevTreeIncludeFlags + treeIncludeInherited = prevTreeIncludeInherited + }) + treeIncludeFlags = true + treeIncludeInherited = true var bundleTree strings.Builder fmt.Fprintf(&bundleTree, "%s — %s\n", bundleCmd.Name(), bundleCmd.Short) printCommandTree(&bundleTree, printCommandTreeInput{cmd: bundleCmd}) @@ -173,22 +181,66 @@ func TestTreeCommand(t *testing.T) { }) t.Run("prefers long flag names over shorthand pairs", func(t *testing.T) { + prevTreeIncludeFlags := treeIncludeFlags + prevTreeIncludeInherited := treeIncludeInherited + t.Cleanup(func() { + treeIncludeFlags = prevTreeIncludeFlags + treeIncludeInherited = prevTreeIncludeInherited + }) + treeIncludeFlags = true + treeIncludeInherited = true + var flagsTree strings.Builder + printCommandTree(&flagsTree, printCommandTreeInput{cmd: rootCmd}) + flagsOutput := flagsTree.String() for _, flag := range []string{"-h, --help", "-l, --log-level", "-p, --passwords", "-v, --verbose"} { - if strings.Contains(rootOutput, flag) { + if strings.Contains(flagsOutput, flag) { t.Errorf("tree output should omit shorthand pair %q", flag) } } }) - t.Run("collapses inherited flags to one summary line", func(t *testing.T) { - if strings.Count(rootOutput, "inherits: ") == 0 { + t.Run("collapses inherited flags to one summary line when requested", func(t *testing.T) { + prevTreeIncludeFlags := treeIncludeFlags + prevTreeIncludeInherited := treeIncludeInherited + t.Cleanup(func() { + treeIncludeFlags = prevTreeIncludeFlags + treeIncludeInherited = prevTreeIncludeInherited + }) + treeIncludeFlags = true + treeIncludeInherited = true + var flagsTree strings.Builder + printCommandTree(&flagsTree, printCommandTreeInput{cmd: rootCmd}) + flagsOutput := flagsTree.String() + if strings.Count(flagsOutput, "inherits: ") == 0 { t.Fatal("tree output missing inherited flag summaries") } - if strings.Count(rootOutput, "\n│ ├── --json\n")+strings.Count(rootOutput, "\n│ └── --json\n") != 0 { + if strings.Count(flagsOutput, "\n│ ├── --json\n")+strings.Count(flagsOutput, "\n│ └── --json\n") != 0 { t.Fatal("tree output should not repeat inherited flags as standalone subcommand entries") } }) + t.Run("includes local flags only when requested", func(t *testing.T) { + prevTreeIncludeFlags := treeIncludeFlags + prevTreeIncludeInherited := treeIncludeInherited + t.Cleanup(func() { + treeIncludeFlags = prevTreeIncludeFlags + treeIncludeInherited = prevTreeIncludeInherited + }) + treeIncludeFlags = true + treeIncludeInherited = false + var flagsTree strings.Builder + printCommandTree(&flagsTree, printCommandTreeInput{cmd: rootCmd}) + flagsOutput := flagsTree.String() + for _, flag := range []string{"--help", "--version"} { + if !strings.Contains(flagsOutput, flag) { + t.Errorf("tree output missing requested local flag %q", flag) + } + } + if strings.Contains(flagsOutput, "inherits: ") { + t.Fatal("tree output should omit inherited summaries unless requested") + } + }) + t.Run("rejects arguments", func(t *testing.T) { if treeCmd.Args == nil { t.Fatal("treeCmd.Args is nil; expected cobra.NoArgs") diff --git a/cmd/certkit/readonly_commands_test.go b/cmd/certkit/readonly_commands_test.go index 6a11b89..13dfe2a 100644 --- a/cmd/certkit/readonly_commands_test.go +++ b/cmd/certkit/readonly_commands_test.go @@ -72,6 +72,10 @@ type readonlyGlobals struct { // inspect flags inspectFormat string inspectAllowPrivateNetwork bool + + // tree flags + treeIncludeFlags bool + treeIncludeInherited bool } func snapshotReadonlyGlobals() readonlyGlobals { @@ -120,6 +124,9 @@ func snapshotReadonlyGlobals() readonlyGlobals { inspectFormat: inspectFormat, inspectAllowPrivateNetwork: inspectAllowPrivateNetwork, + + treeIncludeFlags: treeIncludeFlags, + treeIncludeInherited: treeIncludeInherited, } } @@ -166,6 +173,9 @@ func restoreReadonlyGlobals(g readonlyGlobals) { inspectFormat = g.inspectFormat inspectAllowPrivateNetwork = g.inspectAllowPrivateNetwork + + treeIncludeFlags = g.treeIncludeFlags + treeIncludeInherited = g.treeIncludeInherited readonlyGlobalsMu.Unlock() } diff --git a/cmd/certkit/tree.go b/cmd/certkit/tree.go index d6bfe79..42d4249 100644 --- a/cmd/certkit/tree.go +++ b/cmd/certkit/tree.go @@ -13,11 +13,16 @@ import ( var treeCmd = &cobra.Command{ Use: "tree", Short: "Display the full command tree", - Long: "Display every command, subcommand, and flag in a tree layout.", + Long: "Display the command and subcommand tree. Use --flags to include local flag details and --inherited to include inherited flag details.", Args: cobra.NoArgs, RunE: runTree, } +var ( + treeIncludeFlags bool + treeIncludeInherited bool +) + type printCommandTreeInput struct { cmd *cobra.Command prefix string @@ -32,6 +37,8 @@ type commandTreeJSON struct { } func init() { + treeCmd.Flags().BoolVar(&treeIncludeFlags, "flags", false, "Include local flags in text tree output") + treeCmd.Flags().BoolVar(&treeIncludeInherited, "inherited", false, "Include inherited flags in text tree output") rootCmd.AddCommand(treeCmd) } @@ -66,7 +73,8 @@ func initTreeSurface(cmd *cobra.Command) { } // printCommandTree recursively prints a command and its children with -// box-drawing connectors. Each command shows its flags indented beneath it. +// box-drawing connectors. Flag details are opt-in so the default tree stays +// focused on the command surface. func printCommandTree(b *strings.Builder, in printCommandTreeInput) { cmd := in.cmd prefix := in.prefix @@ -87,23 +95,28 @@ func printCommandTree(b *strings.Builder, in printCommandTreeInput) { } } - total := len(localFlags) + len(visible) - if len(inheritedFlags) > 0 { + total := len(visible) + if treeIncludeFlags { + total += len(localFlags) + } + if treeIncludeInherited && len(inheritedFlags) > 0 { total++ } idx := 0 // Print local flags. - for _, flag := range localFlags { - idx++ - connector := "├── " - if idx == total { - connector = "└── " + if treeIncludeFlags { + for _, flag := range localFlags { + idx++ + connector := "├── " + if idx == total { + connector = "└── " + } + fmt.Fprintf(b, "%s%s%s\n", prefix, connector, flag) } - fmt.Fprintf(b, "%s%s%s\n", prefix, connector, flag) } - if len(inheritedFlags) > 0 { + if treeIncludeInherited && len(inheritedFlags) > 0 { idx++ connector := "├── " if idx == total { diff --git a/go.mod b/go.mod index 80d6532..34fc4bc 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/smallstep/pkcs7 v0.2.1 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 - golang.org/x/crypto v0.48.0 + golang.org/x/crypto v0.49.0 golang.org/x/sys v0.42.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.46.1 diff --git a/go.sum b/go.sum index 08a528a..ecfdff9 100644 --- a/go.sum +++ b/go.sum @@ -67,8 +67,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -116,8 +116,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/internal/certstore/validate.go b/internal/certstore/validate.go index a42d6fa..9df155a 100644 --- a/internal/certstore/validate.go +++ b/internal/certstore/validate.go @@ -16,7 +16,6 @@ import ( ) var errValidationCertNotFound = errors.New("certificate with SKI not found") -var errValidationCertAmbiguous = errors.New("multiple certificates share SKI") // ValidationResult holds the outcome of validating a single certificate. type ValidationResult struct { @@ -46,14 +45,10 @@ func RunValidation(ctx context.Context, input RunValidationInput) (*ValidationRe skiHex := strings.ReplaceAll(input.SKIColon, ":", "") skiHex = strings.ToLower(skiHex) - certs := input.Store.CertsForSKI(skiHex) - if len(certs) == 0 { + rec := input.Store.GetCert(skiHex) + if rec == nil { return nil, fmt.Errorf("%w: %s", errValidationCertNotFound, input.SKIColon) } - if len(certs) > 1 { - return nil, fmt.Errorf("%w: %s", errValidationCertAmbiguous, input.SKIColon) - } - rec := certs[0] leaf := rec.Cert now := time.Now() diff --git a/internal/certstore/validate_test.go b/internal/certstore/validate_test.go index d1e29d7..6f08dd3 100644 --- a/internal/certstore/validate_test.go +++ b/internal/certstore/validate_test.go @@ -334,14 +334,14 @@ func TestRunValidation(t *testing.T) { tests := []struct { name string - setup func(t *testing.T) (store *MemStore, skiColon string) + setup func(t *testing.T) (store *MemStore, skiColon string, expectedNotAfter string) wantErr bool errContains string - validate func(t *testing.T, result *ValidationResult) + validate func(t *testing.T, result *ValidationResult, expectedNotAfter string) }{ { name: "valid leaf with matching key", - setup: func(t *testing.T) (*MemStore, string) { + setup: func(t *testing.T) (*MemStore, string, string) { t.Helper() ca := newRSACA(t) leaf := newRSALeaf(t, ca, "test.example.com", []string{"test.example.com", "www.example.com"}) @@ -361,9 +361,9 @@ func TestRunValidation(t *testing.T) { hexSKI = ski break } - return store, skiToColonHex(t, hexSKI) + return store, skiToColonHex(t, hexSKI), "" }, - validate: func(t *testing.T, result *ValidationResult) { + validate: func(t *testing.T, result *ValidationResult, _ string) { t.Helper() // Subject should be the leaf CN. @@ -416,7 +416,7 @@ func TestRunValidation(t *testing.T) { }, { name: "expired cert", - setup: func(t *testing.T) (*MemStore, string) { + setup: func(t *testing.T) (*MemStore, string, string) { t.Helper() ca := newRSACA(t) leaf := newExpiredLeaf(t, ca) @@ -432,9 +432,9 @@ func TestRunValidation(t *testing.T) { hexSKI = ski break } - return store, skiToColonHex(t, hexSKI) + return store, skiToColonHex(t, hexSKI), "" }, - validate: func(t *testing.T, result *ValidationResult) { + validate: func(t *testing.T, result *ValidationResult, _ string) { t.Helper() if result.Valid { @@ -464,17 +464,17 @@ func TestRunValidation(t *testing.T) { }, { name: "nonexistent SKI", - setup: func(t *testing.T) (*MemStore, string) { + setup: func(t *testing.T) (*MemStore, string, string) { t.Helper() store := NewMemStore() - return store, "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd" + return store, "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd", "" }, wantErr: true, errContains: "not found", }, { name: "ambiguous reused SKI", - setup: func(t *testing.T) (*MemStore, string) { + setup: func(t *testing.T) (*MemStore, string, string) { t.Helper() ca := newRSACA(t) store := NewMemStore() @@ -483,12 +483,13 @@ func TestRunValidation(t *testing.T) { if !ok { t.Fatal("expected RSA key from newRSALeaf") } + secondNotAfter := time.Now().UTC().Truncate(time.Second).Add(366 * 24 * time.Hour) secondTemplate := &x509.Certificate{ SerialNumber: randomSerial(t), Subject: pkix.Name{CommonName: "renewal-two.example.com"}, DNSNames: []string{"renewal-two.example.com"}, NotBefore: time.Now().Add(-time.Hour), - NotAfter: time.Now().Add(365 * 24 * time.Hour), + NotAfter: secondNotAfter, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, } @@ -514,17 +515,24 @@ func TestRunValidation(t *testing.T) { hexSKI = ski break } - return store, skiToColonHex(t, hexSKI) + return store, skiToColonHex(t, hexSKI), secondNotAfter.Format(time.RFC3339) + }, + validate: func(t *testing.T, result *ValidationResult, expectedNotAfter string) { + t.Helper() + if result.Subject != "renewal-two.example.com" { + t.Fatalf("Subject = %q, want renewal-two.example.com", result.Subject) + } + if result.NotAfter != expectedNotAfter { + t.Fatalf("NotAfter = %q, want %q", result.NotAfter, expectedNotAfter) + } }, - wantErr: true, - errContains: "multiple certificates share SKI", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - store, skiColon := tt.setup(t) + store, skiColon, expectedNotAfter := tt.setup(t) result, err := RunValidation(context.Background(), RunValidationInput{ Store: store, @@ -544,7 +552,7 @@ func TestRunValidation(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - tt.validate(t, result) + tt.validate(t, result, expectedNotAfter) }) } }