Files
certctl/internal/pkcs7/length_property_test.go
T
shankar0123 95d0d85391 Bundle Q (Coverage Audit Closure): property-based pilot + hygiene — L-001/L-002/L-003/L-004/I-001 closed
Five small closures wrapping the Low-tier and Info-tier audit findings.

Q.1 — cmd/cli round-out (L-001 closed)
======================================
cmd/cli/dispatch_test.go: ~30 dispatch tests across handleCerts /
handleAgents / handleJobs / handleImport / handleStatus. httptest.NewTLSServer
mocks the API; cli.NewClient(_, _, _, _, true) constructs an
insecure-skip-verify client. Each test pins the missing-args usage-print
path AND the happy-path delegation. Result: 7.1% -> 63.5% coverage
(gate: >=30%).

Q.2 — awssm round-out (L-002 closed)
======================================
internal/connector/discovery/awssm/awssm_edge_test.go: New() default
constructor, extractKeyInfo (ECDSA/Ed25519/unknown — was RSA-only),
processSecret filter arms (NamePrefix mismatch / TagFilter mismatch /
empty-value / GetSecretValue error), realSMClient stub-contract pin
(ListSecrets / GetSecretValue / NewRealSMClient), and EmailAddresses
SAN extraction. Result: 78.2% -> 96.0% coverage (gate: >=85%).

Q.3 — Property-based testing pilot (L-003 closed)
======================================
gopter@v0.2.11 added to go.mod (test-only).

internal/crypto/encryption_property_test.go:
- TestProperty_EncryptDecryptRoundTrip — 50 successful tests,
  DecryptIfKeySet(EncryptIfKeySet(x, k), k) == x
- TestProperty_WrongPassphraseRejected — 30 successful tests,
  AEAD never returns nil-error AND bytes-equal plaintext under
  wrong passphrase
Both skipped under -short to keep developer loop fast (PBKDF2 600k
rounds × 50 iters ≈ 15s on -race CI).

internal/pkcs7/length_property_test.go:
- TestProperty_ASN1LengthRoundTrip — three sub-properties:
  decodeLength(encode(x)) == x for x ∈ [0, 2³¹−1]; short-form
  invariant (length<128 → 1 byte == length); long-form invariant
  (length>=128 → high bit set + N bytes follow). 500 successful
  tests in <10ms.

Q.4 — Architecture diagram multi-agent update (L-004 closed)
======================================
docs/qa-test-guide.md::Architecture: ASCII diagram updated to show
'certctl-agent (×N)' + callout explaining seed_demo.sql provisions
12 agent rows (1 active, 2 retired, 9 reserved/sentinel) for Parts
04, 05, 55 + FSM coverage. Operators running parallel-agent topologies
guided to AGENT_COUNT=N + 'make qa-stats'.

Q.5 — Test-naming CI guard (I-001 closed)
======================================
.github/workflows/ci.yml: Test-naming convention guard added after
the QA-doc seed-count drift guard. Greps for func Test<X>( missing
the <X>_<Scenario> suffix. Prints first 20 non-conformant as
::warning:: annotations. continue-on-error: true (informational).
Excludes TestMain + TestProperty_*. Promotion to hard-fail tracked
as I-001-extended.

Verification
======================================
- python3 yaml.safe_load on ci.yml: OK
- go vet ./cmd/cli/... ./internal/connector/discovery/awssm/...
  ./internal/crypto/... ./internal/pkcs7/...: clean
- go test -short -count=1 across all four packages: PASS
- go test -count=1 (full property tests): PASS
  - crypto 15.4s (50 + 30 × 600k PBKDF2)
  - pkcs7 5ms

Audit deliverables
======================================
- gap-backlog.md: strikethroughs on L-001/L-002/L-003/L-004/I-001
  with per-finding closure note
- closure-plan.md: ticks Bundle Q [x] with per-item breakdown

Closes: L-001, L-002, L-003, L-004, I-001
Bundle: Q (Property-Based + Hygiene)
2026-04-27 18:36:47 +00:00

111 lines
2.9 KiB
Go

package pkcs7
import (
"testing"
"github.com/leanovate/gopter"
"github.com/leanovate/gopter/gen"
"github.com/leanovate/gopter/prop"
)
// Bundle Q (L-003 closure): property-based test for ASN.1 length encoding.
//
// The pkcs7 package implements DER-encoded length under [ASN1EncodeLength];
// the inverse parser is provided here as `decodeLength` (tracked under the
// EST/SCEP code path that consumes the DER framing). The property is the
// classic encode/decode round-trip:
//
// decodeLength(encodeLength(x)) == x for all 0 ≤ x ≤ math.MaxInt32
//
// In addition, structural invariants are pinned:
//
// - 0 ≤ x < 128 → output is 1 byte, equal to x
// - x ≥ 128 → output[0] has the high bit set; output[0]&0x7f == len(rest)
// and rest is big-endian
//
// These match X.690 §8.1.3.
// decodeLength is the inverse of ASN1EncodeLength, defined in this test file
// because the production code only needs the encoder. It returns the decoded
// length and the number of bytes consumed.
func decodeLength(b []byte) (int, int, bool) {
if len(b) == 0 {
return 0, 0, false
}
first := b[0]
if first < 0x80 {
return int(first), 1, true
}
n := int(first & 0x7f)
if n == 0 || n > 4 || len(b) < 1+n {
return 0, 0, false
}
v := 0
for i := 0; i < n; i++ {
v = (v << 8) | int(b[1+i])
}
return v, 1 + n, true
}
func TestProperty_ASN1LengthRoundTrip(t *testing.T) {
parameters := gopter.DefaultTestParameters()
parameters.MinSuccessfulTests = 500
properties := gopter.NewProperties(parameters)
properties.Property("decodeLength(ASN1EncodeLength(x)) == x", prop.ForAll(
func(x int32) bool {
if x < 0 {
return true // out of contract domain (lengths are non-negative)
}
encoded := ASN1EncodeLength(int(x))
got, n, ok := decodeLength(encoded)
if !ok {
t.Logf("decodeLength failed on encoded form of %d: %x", x, encoded)
return false
}
if n != len(encoded) {
t.Logf("consumed %d bytes but encoded form is %d bytes (%d → %x)", n, len(encoded), x, encoded)
return false
}
if got != int(x) {
t.Logf("round-trip mismatch: %d → %x → %d", x, encoded, got)
return false
}
return true
},
gen.Int32Range(0, 0x7fffffff),
))
properties.Property("short-form encoding for x < 128", prop.ForAll(
func(x int8) bool {
if x < 0 {
return true
}
encoded := ASN1EncodeLength(int(x))
return len(encoded) == 1 && encoded[0] == byte(x)
},
gen.Int8Range(0, 127),
))
properties.Property("long-form encoding sets high bit on first byte", prop.ForAll(
func(x int32) bool {
if x < 128 {
return true
}
encoded := ASN1EncodeLength(int(x))
if len(encoded) < 2 {
return false
}
if encoded[0]&0x80 == 0 {
t.Logf("long-form first byte %02x missing high bit for x=%d", encoded[0], x)
return false
}
n := int(encoded[0] & 0x7f)
return n == len(encoded)-1
},
gen.Int32Range(128, 0x7fffffff),
))
properties.TestingRun(t)
}