VC-59682: Align the go directive with the vendored Go 1.27.1 toolchain - #841
Conversation
Problem go.mod declares go 1.26.4 while VENDORED_GO_VERSION is 1.27.1, the toolchain CI builds with. Richard's 0c29720 (#831) moved the toolchain and left the directive behind. This fixes no bug — the agent was never broken for ML-DSA, since it ships raw PEM unparsed and its one customer-certificate parse point reads only IsCA and ExtKeyUsage. But the mismatch has two costs: * The compiler gates standard library symbols on the go directive, so nothing in this repo can import crypto/mldsa and the agent's ML-DSA behaviour cannot be pinned by a test at all. * Nothing stops a Go 1.26 build. Once anyone reads cert.PublicKeyAlgorithm to report a key type, which is where PQC discovery is heading, that build silently reports 0 instead of failing to compile. Solution Move the directive to 1.27.1 and add ML-DSA-65 cases to TestIsExcludableSecret pinning that a post-quantum chain is classified exactly like the P-256 one. The helper imports crypto/mldsa, so the suite now also fails to build on an older toolchain, which gives the directive teeth. The bump activates modernize/embedlit, which would rewrite ~250 lines of unrelated Kubernetes object literals and whose --fix leaves a tree that does not compile; disabled in .golangci.yaml. Remaining go.mod churn is go mod tidy canonicalising three require blocks into two. Test plan * TestIsExcludableSecret — ML-DSA-65 client-auth leaf kept, server-auth leaf excluded, matching the existing P-256 cases. All 11 subtests pass. * The agent's parse path run against a real ML-DSA-65 chain on go1.26.5 and go1.27.1 gives identical classification, confirming this is a guard, not a fix. * make -j verify green — golangci-lint 0 issues at GOVERSION=1.27.1, tree clean. * make -j test-unit green — 465 tests, 4 skipped, 0 failures. * Rebased onto 2b69e98; go.mod require set identical to master. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wallrj-cyberark
left a comment
There was a problem hiding this comment.
Four points, none blocking the direction. I checked out the branch and ran it: make verify-golangci-lint is 0 issues, make verify-govulncheck is clean, go mod tidy and make generate-golangci-lint-config are both idempotent, and TestIsExcludableSecret passes all 11 subtests. Your --fix claim reproduces too: enabling embedlit and running --fix rewrites five files and leaves an unused metav1 import in pkg/agent/run.go, so the tree does not compile.
The four comments are about how durable the change is, not whether it works today.
| # The flattened form hides which embedded struct a field came from, and | ||
| # mixes TypeMeta and ObjectMeta fields into one list. Keep it off. | ||
| disable: | ||
| - embedlit |
There was a problem hiding this comment.
An unknown analyzer name here is silently ignored, so this suppression can rot without telling you.
I changed embedlit to embedlitXXX on this branch and ran GOVERSION=1.27.1 golangci-lint run -c .golangci.yaml. No error, no warning, and the embedlit findings came straight back. golangci-lint v2.13.2 does not validate the names in settings.modernize.disable.
So if x/tools renames or splits embedlit in a later bump, this entry becomes a no-op and CI goes red with the ~200 lines you describe, with nothing pointing at the dead config line.
Please use an exclusions.rules entry instead. This file already sets exclusions.warn-unused: true, and golangci-lint reports how many issues each exclusion rule actually skipped. In the same run it prints Skipped 0 issues by rules: [Path: "third_party", ...] for the paths that match nothing. That gives you a signal when the suppression stops doing anything; settings.modernize.disable gives you none.
There was a problem hiding this comment.
Done in 5453b8e — moved to exclusions.rules.
Reproduced your embedlitXXX result, and confirmed the replacement behaves the way you expect:
level=warning msg="[runner/exclusion_rules] Skipped 0 issues by rules: [Text: \"^embedlitXXX:\", Linters: \"modernize\"]"
46 issues:
So a typo'd rule announces itself and turns CI red, where the disable list did neither. (46, not the ~200 I claimed — that number was the line count --fix rewrites, and the 3 findings a plain run reports is max-same-issues: 3 truncating.)
The one thing I wanted to prove before making the swap: exclusions.rules only filters the report, so I checked it does not reopen the --fix hole that the disable list was closing. golangci-lint run --fix -c with the new rule leaves the tree untouched — exclusions are applied before fixes. make fix-golangci-lint is a documented target here, so that mattered.
make generate-golangci-lint-config is still idempotent over two runs with the comment block in its new position.
[Generated with claude code]
| exclude: true, | ||
| }, | ||
| { | ||
| name: "TLS secret with ML-DSA client cert in tls.crt", |
There was a problem hiding this comment.
These two cases do not pin what the helper comment says they pin.
isExcludableTLSSecret branches only on isClientCertificate(cert), which reads ExtKeyUsage. The subtests assert only the exclude boolean. Nothing in the path touches the key.
So take the scenario from your own description: someone adds cert.PublicKeyAlgorithm key-type reporting and gets it wrong for ML-DSA. Both of these still pass. The regression they are meant to catch goes through untouched.
The crypto/mldsa import is the part that earns its place, and it does so at compile time, not at assert time. Please either assert something algorithm-specific — parse the leaf and check PublicKeyAlgorithm == x509.MLDSA65, which is UnknownPublicKeyAlgorithm on 1.26 — or drop the claim from the comment and say plainly that the import exists to hold the toolchain floor.
There was a problem hiding this comment.
Agreed, and fixed in 5453b8e. The helper now parses the leaf and asserts on it.
One correction to the suggested check: x509.MLDSA65 is a SignatureAlgorithm (enum 18), not a PublicKeyAlgorithm. The PublicKeyAlgorithm constant added in 1.27 is x509.MLDSA (enum 5). Comparing PublicKeyAlgorithm == x509.MLDSA65 would compile — both are int underneath — and never hold. So the helper asserts both, each against the right type:
require.Equal(t, x509.MLDSA, leaf.PublicKeyAlgorithm)
require.Equal(t, x509.MLDSA65, leaf.SignatureAlgorithm)and sampleCertificateChain asserts x509.ECDSA for symmetry.
Checked the assertion is live rather than vacuous: swapping the expectation to x509.ECDSA fails with expected: 3, actual: 5.
[Generated with claude code]
There was a problem hiding this comment.
Confirmed against the 1.27.1 stdlib: x509.MLDSA is a PublicKeyAlgorithm (5), x509.MLDSA65 a SignatureAlgorithm (18). My version would have compared across the two types through require.Equal's any parameters and never held. Asserting both is better than what I asked for.
Mutation-checked all three assertions and each fails as it should, including x509.ECDSA → x509.MLDSA on the P-256 helper.
| // key. crypto/mldsa exists only in Go 1.27+, so this also fails to build under | ||
| // an older toolchain, which is the requirement the go directive in go.mod | ||
| // states. | ||
| func sampleMLDSACertificateChain(t testing.TB, usages ...x509.ExtKeyUsage) string { |
There was a problem hiding this comment.
This is a 60-line copy of sampleCertificateChain directly above it. The only differences are the two key generations, two subject strings, and caPrivKey.Public() in place of &caPrivKey.PublicKey.
It will drift. Someone adds a SAN or changes NotAfter in the ECDSA helper to fix an unrelated test, this copy keeps the old shape, and "classified exactly like the elliptic curve one" quietly stops comparing like with like. Nothing fails when that happens.
Please take the two keys as parameters and keep one body. crypto.Signer plus Public() covers *ecdsa.PrivateKey and *mldsa.PrivateKey alike, so the ECDSA caller just passes ecdsa.GenerateKey(...) and the new one passes mldsa.GenerateKey(mldsa.MLDSA65()).
There was a problem hiding this comment.
Done in 5453b8e. Both samples are now wrappers over one certificateChain(t, newKey func() (crypto.Signer, error), usages ...x509.ExtKeyUsage) (string, *x509.Certificate).
*mldsa.PrivateKey documents itself as implementing crypto.Signer, so caPrivKey.Public() works for both and the &caPrivKey.PublicKey special case disappears. The two subject strings are gone too — nothing asserts on them, so both chains now use Test CA / example.com, which removes the other axis you could drift along.
It returns the parsed leaf so each wrapper can assert its own algorithm, which is what your other comment asked for.
[Generated with claude code]
| return base64.StdEncoding.EncodeToString(append(clientCertPEM, caCertPEM...)) | ||
| } | ||
|
|
||
| // sampleMLDSACertificateChain is sampleCertificateChain with ML-DSA-65 |
There was a problem hiding this comment.
Please cut this down to a line or two. "crypto/mldsa exists only in Go 1.27+, so this also fails to build under an older toolchain, which is the requirement the go directive in go.mod states" is the PR description, not a fact about the helper. It goes stale the moment the directive moves again.
Something like // sampleCertificateChain with ML-DSA-65 keys. The crypto/mldsa import holds the Go 1.27 floor: this package will not build on an older toolchain. carries everything the next reader needs.
There was a problem hiding this comment.
Done in 5453b8e, using your wording with the name corrected to match the function:
// sampleMLDSACertificateChain is certificateChain with ML-DSA-65 keys. The
// crypto/mldsa import holds the Go 1.27 floor: this package will not build on
// an older toolchain.The reasoning about what isExcludableTLSSecret reads has gone; it belongs in the PR, as you say.
[Generated with claude code]
…ed helper Richard's review on #841. Four points, all taken. * .golangci.yaml: move the embedlit suppression from settings.modernize.disable to exclusions.rules. golangci-lint v2.13.2 does not validate names under settings.modernize.disable — renaming embedlit to embedlitXXX there is silently accepted and the findings come back. Under exclusions.rules the same typo makes warn-unused print Skipped 0 issues by rules: [Text: "^embedlitXXX:", Linters: "modernize"] and the 46 embedlit findings resurface, so CI goes red. Verified that `golangci-lint run --fix` with the exclusion in place still leaves the tree untouched: exclusions are applied before fixes, so this keeps the property the disable list was there for. * sampleMLDSACertificateChain asserted nothing about the key, so it did not pin the claim in its own name. It now checks the parsed leaf is x509.MLDSA / x509.MLDSA65, and sampleCertificateChain checks x509.ECDSA. Note x509.MLDSA65 is a SignatureAlgorithm, not a PublicKeyAlgorithm — the PublicKeyAlgorithm constant is x509.MLDSA. Swapping the expectation to x509.ECDSA fails with `expected: 3, actual: 5`, so the assertion is live. * Both samples are now thin wrappers over certificateChain(t, newKey, usages...), parameterised on a crypto.Signer factory. *mldsa.PrivateKey and *ecdsa.PrivateKey both implement crypto.Signer, so one body serves both and the 60-line copy is gone. * Trimmed the sampleMLDSACertificateChain doc comment to Richard's wording. Test plan * make -j verify green, including verify-golangci-lint at 0 issues and generate-golangci-lint-config idempotent over two runs. * make test-unit green — 465 tests, 4 skipped. * TestIsExcludableSecret passes all 11 subtests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks — all four taken, pushed as 5453b8e.
Separately, I have corrected the PR description: I wrote that the compiler gates stdlib symbols on the
[Generated with claude code] |
wallrj-cyberark
left a comment
There was a problem hiding this comment.
Approving. All four are addressed at 5453b8e, and you are right on both points where you corrected me.
The exclusion-rule swap was the change worth proving, because exclusions.rules filters reported issues where settings.modernize.disable switched the analyser off. Those are not the same mechanism, so I checked it does not reopen the --fix hole. It does not, and a typo now fails loudly, which was the whole point of the original comment.
You are right about the constants. x509.MLDSA is a PublicKeyAlgorithm (5) and x509.MLDSA65 a SignatureAlgorithm (18), confirmed against the 1.27.1 stdlib. My suggested check would have compared across the two types through require.Equal's any parameters, compiled, and never held. Asserting both, each against the right type, is better than what I asked for.
Your correction to the description is also right. With the directive lowered to go 1.26.4, go build ./pkg/client/ exits 0, while go vet and go test both report mldsa.GenerateKey requires go1.27 or later (file is go1.26). The test is what cannot exist below the directive, not the build.
Nothing further from me.
What I ran at 5453b8e, including the --fix negative control
Detached worktree at 5453b8e, vendored Go 1.27.1.
Exclusion rule behaviour:
| Check | Result |
|---|---|
| Rule present | 0 issues |
| Rule removed | 3 issues, all modernize (max-same-issues: 3 is capping, not the true count) |
Rule typo'd to ^embedlitXXX: |
Skipped 0 issues by rules: [Text: "^embedlitXXX:", Linters: "modernize"] and 3 issues, so CI goes red |
--fix with the rule |
no files modified |
--fix with the rule removed |
5 files, 214 lines, unused metav1 import in pkg/agent/run.go, tree no longer builds |
The last two rows are the pair that matters: the protection survives the swap, and the negative control shows the test is not passing vacuously.
The rule is also live rather than merely unreported. warn-unused only warns about rules that match nothing, and the embedlit rule draws no warning on a clean run while the three formatter path rules do.
Mutation tests on the new assertions, each failing as it should:
| Mutation | Failure |
|---|---|
x509.MLDSA → x509.ECDSA |
expected: 3, actual: 5 |
x509.MLDSA65 → x509.ECDSAWithSHA256 |
expected: 10, actual: 18 |
x509.ECDSA → x509.MLDSA on the P-256 helper |
expected: 5, actual: 3 |
Everything else:
make verify-golangci-lint— 0 issuesTestIsExcludableSecret— 11/11 subtests passgo mod tidy— no drift ingo.modorgo.summake generate-golangci-lint-config— idempotent, the comment block survives in its new position insideexclusions.rulescertificateChaindoes not collide with any existing identifier, and nothing in the package asserts on the subject strings you dropped
Problem
go.mod declares go 1.26.4 while VENDORED_GO_VERSION is 1.27.1, the toolchain CI builds with. Richard's 0c29720 (#831) moved the toolchain and left the directive behind. This fixes no bug — the agent was never broken for ML-DSA, since it ships raw PEM unparsed and its one customer-certificate parse point reads only IsCA and ExtKeyUsage. But the mismatch has two costs:
go vetgates standard library symbols on the go directive, so nothing in this repo can importcrypto/mldsaand the agent's ML-DSA behaviour cannot be pinned by a test at all. (Correction to the original wording, which said "the compiler":go buildsucceeds at any directive level with a 1.27 toolchain. It isgo vetthat reportsmldsa.GenerateKey requires go1.27 or later (file is go1.26), andgo testruns vet, so the test is what cannot exist. Same practical conclusion, different mechanism.)Solution
Move the directive to 1.27.1 and add ML-DSA-65 cases to TestIsExcludableSecret pinning that a post-quantum chain is classified exactly like the P-256 one. The helper imports crypto/mldsa, so the suite now also fails to build on an older toolchain, which gives the directive teeth. The bump activates modernize/embedlit, which would rewrite ~250 lines of unrelated Kubernetes object literals and whose --fix leaves a tree that does not compile; disabled in .golangci.yaml. Remaining go.mod churn is go mod tidy canonicalising three require blocks into two.
Test plan