|
1 | 1 | package jwt |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "cmp" |
4 | 5 | "context" |
5 | 6 | "encoding/json" |
6 | 7 | "errors" |
@@ -64,28 +65,59 @@ func limitReader(reader io.Reader) io.Reader { |
64 | 65 |
|
65 | 66 | // FromData will find and optionally verify JWT secrets in a given set of bytes. |
66 | 67 | func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { |
67 | | - dataStr := string(data) |
| 68 | + client := cmp.Or(s.client, defaultClient) |
| 69 | + seenMatches := make(map[string]bool) |
68 | 70 |
|
69 | | - uniqueMatches := make(map[string]struct{}) |
70 | | - for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) { |
71 | | - uniqueMatches[match[1]] = struct{}{} |
72 | | - } |
| 71 | + for _, matchGroups := range keyPat.FindAllStringSubmatch(string(data), -1) { |
| 72 | + match := matchGroups[1] |
| 73 | + |
| 74 | + if seenMatches[match] { |
| 75 | + continue |
| 76 | + } |
| 77 | + seenMatches[match] = true |
| 78 | + |
| 79 | + parsedToken, _, err := jwt.NewParser(jwt.WithPaddingAllowed()).ParseUnverified(match, jwt.MapClaims{}) |
| 80 | + if err != nil { |
| 81 | + // we can skip a token that doesn't parse without any validation or verification |
| 82 | + continue |
| 83 | + } |
| 84 | + |
| 85 | + issString, _ := parsedToken.Claims.GetIssuer() |
| 86 | + |
| 87 | + iatString := "" |
| 88 | + iat, err := parsedToken.Claims.GetIssuedAt() |
| 89 | + if err == nil && iat != nil { |
| 90 | + iatString = iat.String() |
| 91 | + } |
| 92 | + |
| 93 | + expString := "" |
| 94 | + exp, err := parsedToken.Claims.GetExpirationTime() |
| 95 | + if err == nil && exp != nil { |
| 96 | + expString = exp.String() |
| 97 | + } |
| 98 | + |
| 99 | + extraData := map[string]string{ |
| 100 | + "iss": issString, |
| 101 | + "iat": iatString, |
| 102 | + "exp": expString, |
| 103 | + } |
73 | 104 |
|
74 | | - for match := range uniqueMatches { |
75 | 105 | s1 := detectors.Result{ |
76 | 106 | DetectorType: detectorspb.DetectorType_JWT, |
77 | 107 | Raw: []byte(match), |
| 108 | + ExtraData: extraData, |
78 | 109 | } |
79 | 110 |
|
80 | 111 | if verify { |
81 | | - client := s.client |
82 | | - if client == nil { |
83 | | - client = defaultClient |
| 112 | + var isVerified bool |
| 113 | + var verificationErr error |
| 114 | + switch parsedToken.Method.Alg() { |
| 115 | + case "HS256", "HS384", "HS512": |
| 116 | + isVerified, verificationErr = verifyHMAC(parsedToken) |
| 117 | + default: |
| 118 | + isVerified, verificationErr = verifyPublicKey(ctx, client, match) |
84 | 119 | } |
85 | | - |
86 | | - isVerified, extraData, verificationErr := verifyMatch(ctx, client, match) |
87 | 120 | s1.Verified = isVerified |
88 | | - s1.ExtraData = extraData |
89 | 121 | s1.SetVerificationError(verificationErr, match) |
90 | 122 | } |
91 | 123 |
|
@@ -138,11 +170,31 @@ func performHttpRequest(ctx context.Context, client *http.Client, method string, |
138 | 170 | return resp, nil |
139 | 171 | } |
140 | 172 |
|
141 | | -// Attempt to verify a JWT. |
| 173 | +// Attempt to verify a JWT that uses an HMAC algorithm. |
| 174 | +// |
| 175 | +// This implementation only attempts to verify JWTs whose issuers use the OIDC Discovery protocol to make public keys available via request. |
| 176 | +func verifyHMAC(parsedToken *jwt.Token) (bool, error) { |
| 177 | + v := jwt.NewValidator( |
| 178 | + jwt.WithValidMethods([]string{ |
| 179 | + jwt.SigningMethodHS256.Alg(), |
| 180 | + jwt.SigningMethodHS384.Alg(), |
| 181 | + jwt.SigningMethodHS512.Alg(), |
| 182 | + }), |
| 183 | + jwt.WithIssuedAt(), |
| 184 | + jwt.WithPaddingAllowed(), |
| 185 | + jwt.WithLeeway(time.Minute), |
| 186 | + ) |
| 187 | + if err := v.Validate(parsedToken.Claims); err != nil { |
| 188 | + // though we have not checked the signature, the token is definitely invalid |
| 189 | + return false, nil |
| 190 | + } |
| 191 | + return false, fmt.Errorf("no key available to verify an HMAC-based signature") |
| 192 | +} |
| 193 | + |
| 194 | +// Attempt to verify a JWT that uses a public-key signing algorithm. |
142 | 195 | // |
143 | | -// This implementation only attempts to verify JWTs that use an asymmetric encryption algorithm, |
144 | | -// and only those whose issuers use the OIDC Discovery protocol to make public keys available via request. |
145 | | -func verifyMatch(ctx context.Context, client *http.Client, tokenString string) (bool, map[string]string, error) { |
| 196 | +// This implementation only attempts to verify JWTs whose issuers use the OIDC Discovery protocol to make public keys available via request. |
| 197 | +func verifyPublicKey(ctx context.Context, client *http.Client, tokenString string) (bool, error) { |
146 | 198 |
|
147 | 199 | // A key retrieval function that uses the OIDC Discovery protocol, |
148 | 200 | // being careful to avoid possible DoS from a potentially malicious JWKS server. |
@@ -216,7 +268,8 @@ func verifyMatch(ctx context.Context, client *http.Client, tokenString string) ( |
216 | 268 |
|
217 | 269 | // Parse matching key to the "raw" key type needed for signature verification |
218 | 270 | var rawMatchingKey any |
219 | | - err = jwk.Export(matchingKey, &rawMatchingKey); if err != nil { |
| 271 | + err = jwk.Export(matchingKey, &rawMatchingKey) |
| 272 | + if err != nil { |
220 | 273 | return nil, fmt.Errorf("failed to export matching key: %w", err) |
221 | 274 | } |
222 | 275 |
|
@@ -244,13 +297,13 @@ func verifyMatch(ctx context.Context, client *http.Client, tokenString string) ( |
244 | 297 | ) |
245 | 298 | switch { |
246 | 299 | case token.Valid: |
247 | | - return true, nil, nil |
| 300 | + return true, nil |
248 | 301 | case errors.Is(err, jwt.ErrTokenUnverifiable): |
249 | | - return false, nil, err |
| 302 | + return false, err |
250 | 303 | case errors.Is(err, jwt.ErrHashUnavailable): |
251 | | - return false, nil, err |
| 304 | + return false, err |
252 | 305 | default: |
253 | | - return false, nil, nil |
| 306 | + return false, nil |
254 | 307 | } |
255 | 308 | } |
256 | 309 |
|
|
0 commit comments