Skip to content

Commit e8bfa2c

Browse files
fix(attestation): avoid logging the raw federated OIDC token (#3732)
Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
1 parent a00dd1f commit e8bfa2c

2 files changed

Lines changed: 112 additions & 4 deletions

File tree

‎pkg/attestation/crafter/runners/oidc/github.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ func (c *GitHubOIDCClient) requestToken(ctx context.Context, audience []string)
219219
}
220220

221221
func (c *GitHubOIDCClient) decodePayload(b []byte) (string, error) {
222-
c.logger.Debug().Msgf("decoding payload: %s", b)
222+
c.logger.Debug().Msg("decoding token request payload")
223223
var payload struct {
224224
Value string `json:"value"`
225225
}
@@ -233,7 +233,7 @@ func (c *GitHubOIDCClient) decodePayload(b []byte) (string, error) {
233233
// verifyToken verifies the token contents and signature.
234234
func (c *GitHubOIDCClient) verifyToken(ctx context.Context, audience []string, actor []string, rawIDToken string) (*oidc.IDToken, error) {
235235
// Verify the token.
236-
c.logger.Debug().Msgf("verifying token: %s with audience: %s and actor: %s", rawIDToken, audience, actor)
236+
c.logger.Debug().Msgf("verifying token with audience: %s and actor: %s", audience, actor)
237237
verifier, err := c.verifierFunc(ctx)
238238
if err != nil {
239239
return nil, fmt.Errorf("%w: creating verifier: %w", errVerify, err)
@@ -264,7 +264,7 @@ func (c *GitHubOIDCClient) verifyToken(ctx context.Context, audience []string, a
264264
}
265265

266266
func (c *GitHubOIDCClient) decodeToken(token *oidc.IDToken) (*Token, error) {
267-
c.logger.Debug().Msgf("decoding token: %s", token)
267+
c.logger.Debug().Msg("decoding verified token claims")
268268
var t Token
269269
if err := token.Claims(&t); err != nil {
270270
return nil, fmt.Errorf("%w: getting claims: %w", errToken, err)
@@ -274,7 +274,7 @@ func (c *GitHubOIDCClient) decodeToken(token *oidc.IDToken) (*Token, error) {
274274
}
275275

276276
func (c *GitHubOIDCClient) verifyClaims(token *Token) error {
277-
c.logger.Debug().Msgf("verifying claims: %s", token)
277+
c.logger.Debug().Msg("verifying token claims")
278278
if token.JobWorkflowRef == "" {
279279
return fmt.Errorf("%w: job workflow ref is empty", errClaims)
280280
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//
2+
// Copyright 2026 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package oidc
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"net/http"
22+
"net/http/httptest"
23+
"strings"
24+
"testing"
25+
26+
"github.com/coreos/go-oidc/v3/oidc"
27+
"github.com/rs/zerolog"
28+
"github.com/stretchr/testify/assert"
29+
"github.com/stretchr/testify/require"
30+
)
31+
32+
// A distinctive, JWT-shaped placeholder standing in for the federated identity
33+
// token. It is not a real credential; the tests only check it never shows up in logs.
34+
const fakeFederatedToken = "eyJhbGciOiJSUzI1NiJ9." +
35+
"eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJqb2Jfd29ya2Zsb3dfcmVmIjoicmVmIn0." +
36+
"ZmFrZS1zaWduYXR1cmUtbm90LWEtcmVhbC10b2tlbg"
37+
38+
// TestTokenDoesNotLeakRawFederatedToken makes sure the GitHub OIDC runner never
39+
// writes the raw federated token to its debug logs. The token flows through
40+
// Token() -> decodePayload (the raw token-endpoint response body) and verifyToken
41+
// (rawIDToken); logging either at debug level would expose a usable, short-lived
42+
// identity token in CI job logs.
43+
func TestTokenDoesNotLeakRawFederatedToken(t *testing.T) {
44+
// GitHub's token endpoint returns the JWT wrapped as {"value": "<jwt>"}.
45+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
46+
w.Header().Set("Content-Type", "application/json")
47+
_, _ = fmt.Fprintf(w, `{"value": %q}`, fakeFederatedToken)
48+
}))
49+
defer server.Close()
50+
51+
t.Setenv(RequestURLEnvKey, server.URL)
52+
t.Setenv(RequestTokenEnvKey, "request-bearer-token")
53+
54+
// Capture everything written at debug level into a buffer.
55+
var logs strings.Builder
56+
prevGlobal := zerolog.GlobalLevel()
57+
zerolog.SetGlobalLevel(zerolog.TraceLevel)
58+
defer zerolog.SetGlobalLevel(prevGlobal)
59+
logger := zerolog.New(&logs).Level(zerolog.TraceLevel)
60+
61+
client, err := NewGitHubClient(&logger)
62+
require.NoError(t, err)
63+
64+
// Stub out signature verification so the test stays offline and deterministic.
65+
// The token is already logged before the verifier is ever invoked, so this does
66+
// not hide the behaviour under test; it only removes the network dependency.
67+
client.verifierFunc = func(_ context.Context) (*oidc.IDTokenVerifier, error) {
68+
return nil, fmt.Errorf("verification stubbed out for test")
69+
}
70+
71+
// Verification is expected to fail; we only care about what got logged on the way.
72+
_, _ = client.Token(context.Background())
73+
74+
// Collect every debug line that leaked the token, for actionable failure output.
75+
var leaking []string
76+
for _, line := range strings.Split(strings.TrimSpace(logs.String()), "\n") {
77+
if line != "" && strings.Contains(line, fakeFederatedToken) {
78+
leaking = append(leaking, line)
79+
}
80+
}
81+
82+
assert.Emptyf(t, leaking,
83+
"the raw federated OIDC token was written to debug logs in %d message(s):\n%s",
84+
len(leaking), strings.Join(leaking, "\n"))
85+
}
86+
87+
// TestVerifyClaimsDoesNotLeakRawToken covers the other raw-token path: verifyClaims
88+
// stringifies the whole *Token, whose exported RawToken field holds the raw JWT
89+
// (set in Token() before verifyClaims runs). Logging the struct would expose it.
90+
func TestVerifyClaimsDoesNotLeakRawToken(t *testing.T) {
91+
var logs strings.Builder
92+
prevGlobal := zerolog.GlobalLevel()
93+
zerolog.SetGlobalLevel(zerolog.TraceLevel)
94+
defer zerolog.SetGlobalLevel(prevGlobal)
95+
logger := zerolog.New(&logs).Level(zerolog.TraceLevel)
96+
97+
c := &GitHubOIDCClient{logger: &logger}
98+
token := &Token{
99+
JobWorkflowRef: "octo-org/octo-repo/.github/workflows/ci.yml@refs/heads/main",
100+
RunnerEnvironment: "github-hosted",
101+
RawToken: fakeFederatedToken,
102+
}
103+
104+
require.NoError(t, c.verifyClaims(token))
105+
106+
assert.NotContainsf(t, logs.String(), fakeFederatedToken,
107+
"verifyClaims wrote the raw federated token to debug logs via Token.RawToken")
108+
}

0 commit comments

Comments
 (0)