From d00c621016ec74106dc14dbb017e17b2af3da0ed Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Sat, 24 Oct 2026 15:23:33 +0200 Subject: [PATCH 1/2] fix(trace): keep SSH key fingerprints in redacted sessions The generic-api-key rule matched the SHA256 fingerprint that git prints when it checks an SSH commit signature, for example the output of git log --show-signature. A fingerprint is a hash of a public key, not a credential, so the redaction replaced it and the no-secrets policy reported a false positive. Add an allowlist for an OpenSSH SHA256 fingerprint: SHA256: and exactly 43 base64 characters at the end of the match. A longer value after the same prefix is still redacted. Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino Chainloop-Trace-Sessions: 34d67f3d-296c-4dce-b668-3a73637e6a43 --- internal/redaction/betterleaks.toml | 11 ++++++++++ internal/redaction/precision_test.go | 33 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/internal/redaction/betterleaks.toml b/internal/redaction/betterleaks.toml index a02a0fdec..f46afb3b0 100644 --- a/internal/redaction/betterleaks.toml +++ b/internal/redaction/betterleaks.toml @@ -21,6 +21,17 @@ targetRules = ["generic-api-key"] regexTarget = "match" regexes = ['''(?i)\bsha(?:1|224|256|384|512):[0-9a-f]{40,128}\b'''] +[[allowlists]] +description = "SSH public key fingerprints, such as the output of git log --show-signature" +targetRules = ["generic-api-key"] +regexTarget = "match" +# OpenSSH prints a fingerprint as SHA256: and the unpadded base64 of the hash, +# which is always 43 characters. The fingerprint must end the match, so a +# longer value after SHA256: is still redacted. RE2 has no lookahead, and the +# last character can be + or /, so the end is a non-base64 character or the +# end of the match instead of \b. +regexes = ['''\bSHA256:[A-Za-z0-9+/]{43}(?:[^A-Za-z0-9+/=]|$)'''] + [[allowlists]] description = "Code expressions, test durations and the bare key word, which are not password values" targetRules = ["generic-password"] diff --git a/internal/redaction/precision_test.go b/internal/redaction/precision_test.go index 06a99169a..6c14845a9 100644 --- a/internal/redaction/precision_test.go +++ b/internal/redaction/precision_test.go @@ -36,6 +36,9 @@ const ( // sampleAPIKey is a random-looking key the generic-api-key rule matches. sampleAPIKey = "q8Zr2LmX7v" + "Pk4TnW9sYb" sampleEmail = "sarah@chainloop.local" + // sampleSSHFingerprint is the unpadded base64 of a SHA-256 hash, as + // OpenSSH prints the fingerprint of a public key after SHA256: + sampleSSHFingerprint = "q3Rk8vLm2+Zx7Pw4" + "Tn9sYbHc1/Fd6Gj0" + "Ue5Ai8Ko2Ws" ruleGenericAPIKey = "generic-api-key" ruleGitHubPAT = "github-pat" @@ -148,6 +151,17 @@ func TestRulesConfigKeepsNonSecrets(t *testing.T) { text: "API_URL=http://localhost:48002\nCAS_ENDPOINT=localhost:49001\nTEST_EMAIL=" + sampleEmail + "\n", wantDefaultRule: ruleCredentialURI, }, + { + // git log --show-signature on a commit signed with an SSH key + name: "ssh key fingerprint of a commit signature", + text: "Good \"git\" signature for " + sampleEmail + " with ED25519 key SHA256:" + sampleSSHFingerprint + "\n", + wantDefaultRule: ruleGenericAPIKey, + }, + { + name: "rsa key fingerprint of a commit signature", + text: "Good \"git\" signature for " + sampleEmail + " with RSA key SHA256:" + sampleSSHFingerprint + "\n", + wantDefaultRule: ruleGenericAPIKey, + }, } for _, tc := range testCases { @@ -191,6 +205,25 @@ func TestRulesConfigStillRedactsSecrets(t *testing.T) { secret: sampleDigest, wantRule: ruleGenericAPIKey, }, + { + name: "base64 key without a fingerprint prefix", + text: "api_key: " + sampleSSHFingerprint + "\n", + secret: sampleSSHFingerprint, + wantRule: ruleGenericAPIKey, + }, + { + name: "key longer than a fingerprint after a SHA256 prefix", + text: "signing key SHA256:" + strings.Repeat(sampleAPIKey, 3) + "\n", + secret: strings.Repeat(sampleAPIKey, 3), + wantRule: ruleGenericAPIKey, + }, + { + name: "api key on the line after an ssh key fingerprint", + text: "Good \"git\" signature with ED25519 key SHA256:" + sampleSSHFingerprint + "\napi_key: " + sampleAPIKey + "\n", + secret: sampleAPIKey, + wantRule: ruleGenericAPIKey, + keep: "SHA256:" + sampleSSHFingerprint, + }, { name: "password next to an email username", text: "host: db.internal\nuser: " + sampleEmail + "\npassword: " + samplePassword + "\n", From 55b4f9ab75e9a60ae7f7c9604571a9a17e23bc48 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Sat, 24 Oct 2026 15:28:06 +0200 Subject: [PATCH 2/2] fix(trace): anchor the SSH fingerprint allowlist to the end of the match A key that went on after the 43 fingerprint characters with a - . or _ was kept, because any non-base64 character counted as the end. Only the terminator the generic-api-key rule adds to its match can now follow. Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino Chainloop-Trace-Sessions: 34d67f3d-296c-4dce-b668-3a73637e6a43 --- internal/redaction/betterleaks.toml | 8 ++++---- internal/redaction/precision_test.go | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/redaction/betterleaks.toml b/internal/redaction/betterleaks.toml index f46afb3b0..047af011a 100644 --- a/internal/redaction/betterleaks.toml +++ b/internal/redaction/betterleaks.toml @@ -27,10 +27,10 @@ targetRules = ["generic-api-key"] regexTarget = "match" # OpenSSH prints a fingerprint as SHA256: and the unpadded base64 of the hash, # which is always 43 characters. The fingerprint must end the match, so a -# longer value after SHA256: is still redacted. RE2 has no lookahead, and the -# last character can be + or /, so the end is a non-base64 character or the -# end of the match instead of \b. -regexes = ['''\bSHA256:[A-Za-z0-9+/]{43}(?:[^A-Za-z0-9+/=]|$)'''] +# longer value after SHA256:, also one that goes on after a - . or _, is still +# redacted. Only the terminator the rule itself adds to its match can follow: +# a quote, a backtick, whitespace, a semicolon, or an escaped line break. +regexes = ['''\bSHA256:[A-Za-z0-9+/]{43}(?:[\x60'"\s;]|\\[nr])?$'''] [[allowlists]] description = "Code expressions, test durations and the bare key word, which are not password values" diff --git a/internal/redaction/precision_test.go b/internal/redaction/precision_test.go index 6c14845a9..4fd340ca7 100644 --- a/internal/redaction/precision_test.go +++ b/internal/redaction/precision_test.go @@ -217,6 +217,12 @@ func TestRulesConfigStillRedactsSecrets(t *testing.T) { secret: strings.Repeat(sampleAPIKey, 3), wantRule: ruleGenericAPIKey, }, + { + name: "key that continues a fingerprint with a separator", + text: "signing key SHA256:" + strings.Repeat(sampleAPIKey, 3)[:43] + "-" + sampleAPIKey + "\n", + secret: sampleAPIKey, + wantRule: ruleGenericAPIKey, + }, { name: "api key on the line after an ssh key fingerprint", text: "Good \"git\" signature with ED25519 key SHA256:" + sampleSSHFingerprint + "\napi_key: " + sampleAPIKey + "\n",