@@ -2,14 +2,17 @@ package access_keys
2
2
3
3
import (
4
4
"context"
5
- "encoding/hex"
6
- "encoding/json"
7
5
"fmt"
8
- "io "
6
+ "net "
9
7
"net/http"
10
8
"strings"
11
9
"time"
12
10
11
+ "github.com/aws/aws-sdk-go-v2/aws/middleware"
12
+ awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
13
+ "github.com/aws/aws-sdk-go-v2/config"
14
+ "github.com/aws/aws-sdk-go-v2/credentials"
15
+ "github.com/aws/aws-sdk-go-v2/service/sts"
13
16
regexp "github.com/wasilibs/go-re2"
14
17
15
18
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
@@ -20,7 +23,7 @@ import (
20
23
)
21
24
22
25
type scanner struct {
23
- verificationClient * http. Client
26
+ verificationClient config. HTTPClient
24
27
skipIDs map [string ]struct {}
25
28
detectors.DefaultMultiPartCredentialProvider
26
29
}
@@ -55,7 +58,6 @@ var _ interface {
55
58
} = (* scanner )(nil )
56
59
57
60
var (
58
- defaultVerificationClient = common .SaneHttpClient ()
59
61
60
62
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
61
63
// Key types are from this list https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-unique-ids
@@ -72,6 +74,32 @@ func (s scanner) Keywords() []string {
72
74
}
73
75
}
74
76
77
+ // The recommended way by AWS is to use the SDK's http client.
78
+ // https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/configure-http.html
79
+ // Note: Using default http.Client causes SignatureInvalid error in response. therefore, based on http default client implementation, we are using the same configuration.
80
+ func getDefaultBuildableClient () * awshttp.BuildableClient {
81
+ return awshttp .NewBuildableClient ().
82
+ WithTimeout (common .DefaultResponseTimeout ).
83
+ WithDialerOptions (func (dialer * net.Dialer ) {
84
+ dialer .Timeout = 2 * time .Second
85
+ dialer .KeepAlive = 5 * time .Second
86
+ }).
87
+ WithTransportOptions (func (tr * http.Transport ) {
88
+ tr .Proxy = http .ProxyFromEnvironment
89
+ tr .MaxIdleConns = 5
90
+ tr .IdleConnTimeout = 5 * time .Second
91
+ tr .TLSHandshakeTimeout = 3 * time .Second
92
+ tr .ExpectContinueTimeout = 1 * time .Second
93
+ })
94
+ }
95
+
96
+ func (s scanner ) getAWSBuilableClient () config.HTTPClient {
97
+ if s .verificationClient == nil {
98
+ s .verificationClient = getDefaultBuildableClient ()
99
+ }
100
+ return s .verificationClient
101
+ }
102
+
75
103
// FromData will find and optionally verify AWS secrets in a given set of bytes.
76
104
func (s scanner ) FromData (ctx context.Context , verify bool , data []byte ) (results []detectors.Result , err error ) {
77
105
logger := logContext .AddLogger (ctx ).Logger ().WithName ("aws" )
@@ -127,7 +155,7 @@ func (s scanner) FromData(ctx context.Context, verify bool, data []byte) (result
127
155
isCanary = true
128
156
s1 .ExtraData ["message" ] = thinkstMessage
129
157
if verify {
130
- verified , arn , err := s .verifyCanary (idMatch , secretMatch )
158
+ verified , arn , err := s .verifyCanary (ctx , idMatch , secretMatch )
131
159
s1 .Verified = verified
132
160
if arn != "" {
133
161
s1 .ExtraData ["arn" ] = arn
@@ -139,7 +167,7 @@ func (s scanner) FromData(ctx context.Context, verify bool, data []byte) (result
139
167
isCanary = true
140
168
s1 .ExtraData ["message" ] = thinkstKnockoffsMessage
141
169
if verify {
142
- verified , arn , err := s .verifyCanary (idMatch , secretMatch )
170
+ verified , arn , err := s .verifyCanary (ctx , idMatch , secretMatch )
143
171
s1 .Verified = verified
144
172
if arn != "" {
145
173
s1 .ExtraData ["arn" ] = arn
@@ -154,7 +182,7 @@ func (s scanner) FromData(ctx context.Context, verify bool, data []byte) (result
154
182
}
155
183
156
184
if verify && ! isCanary {
157
- isVerified , extraData , verificationErr := s .verifyMatch (ctx , idMatch , secretMatch , true )
185
+ isVerified , extraData , verificationErr := s .verifyMatch (ctx , idMatch , secretMatch , len ( secretMatches ) > 1 )
158
186
s1 .Verified = isVerified
159
187
160
188
// Log if the calculated ID does not match the ID value from verification.
@@ -199,117 +227,53 @@ const (
199
227
)
200
228
201
229
func (s scanner ) verifyMatch (ctx context.Context , resIDMatch , resSecretMatch string , retryOn403 bool ) (bool , map [string ]string , error ) {
202
- // REQUEST VALUES.
203
- now := time .Now ().UTC ()
204
- datestamp := now .Format ("20060102" )
205
- amzDate := now .Format ("20060102T150405Z0700" )
206
-
207
- req , err := http .NewRequestWithContext (ctx , method , endpoint , nil )
230
+ // Prep AWS Creds for STS
231
+ cfg , err := config .LoadDefaultConfig (ctx ,
232
+ config .WithRegion (region ),
233
+ config .WithHTTPClient (s .getAWSBuilableClient ()),
234
+ config .WithCredentialsProvider (
235
+ credentials .NewStaticCredentialsProvider (resIDMatch , resSecretMatch , "" ),
236
+ ),
237
+ )
208
238
if err != nil {
209
239
return false , nil , err
210
240
}
211
- req .Header .Set ("Accept" , "application/json" )
212
-
213
- // TASK 1: CREATE A CANONICAL REQUEST.
214
- // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
215
- canonicalURI := "/"
216
- canonicalHeaders := "host:" + host + "\n "
217
- signedHeaders := "host"
218
- algorithm := "AWS4-HMAC-SHA256"
219
- credentialScope := fmt .Sprintf ("%s/%s/%s/aws4_request" , datestamp , region , service )
220
-
221
- params := req .URL .Query ()
222
- params .Add ("Action" , "GetCallerIdentity" )
223
- params .Add ("Version" , "2011-06-15" )
224
- params .Add ("X-Amz-Algorithm" , algorithm )
225
- params .Add ("X-Amz-Credential" , resIDMatch + "/" + credentialScope )
226
- params .Add ("X-Amz-Date" , amzDate )
227
- params .Add ("X-Amz-Expires" , "30" )
228
- params .Add ("X-Amz-SignedHeaders" , signedHeaders )
229
-
230
- canonicalQuerystring := params .Encode ()
231
- payloadHash := aws .GetHash ("" ) // empty payload
232
- canonicalRequest := method + "\n " + canonicalURI + "\n " + canonicalQuerystring + "\n " + canonicalHeaders + "\n " + signedHeaders + "\n " + payloadHash
233
-
234
- // TASK 2: CREATE THE STRING TO SIGN.
235
- stringToSign := algorithm + "\n " + amzDate + "\n " + credentialScope + "\n " + aws .GetHash (canonicalRequest )
236
-
237
- // TASK 3: CALCULATE THE SIGNATURE.
238
- // https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
239
- hash := aws .GetHMAC ([]byte (fmt .Sprintf ("AWS4%s" , resSecretMatch )), []byte (datestamp ))
240
- hash = aws .GetHMAC (hash , []byte (region ))
241
- hash = aws .GetHMAC (hash , []byte (service ))
242
- hash = aws .GetHMAC (hash , []byte ("aws4_request" ))
243
-
244
- signature2 := aws .GetHMAC (hash , []byte (stringToSign )) // Get Signature HMAC SHA256
245
- signature := hex .EncodeToString (signature2 )
246
-
247
- // TASK 4: ADD SIGNING INFORMATION TO THE REQUEST.
248
- params .Add ("X-Amz-Signature" , signature )
249
- req .Header .Add ("Content-type" , "application/x-www-form-urlencoded; charset=utf-8" )
250
- req .URL .RawQuery = params .Encode ()
251
-
252
- client := s .verificationClient
253
- if client == nil {
254
- client = defaultVerificationClient
255
- }
241
+ // Create STS client
242
+ stsClient := sts .NewFromConfig (cfg , func (o * sts.Options ) {
243
+ o .APIOptions = append (o .APIOptions , middleware .AddUserAgentKeyValue ("User-Agent" , common .UserAgent ()))
244
+ })
256
245
257
- res , err := client .Do (req )
246
+ // Make the GetCallerIdentity API call
247
+ resp , err := stsClient .GetCallerIdentity (ctx , & sts.GetCallerIdentityInput {})
258
248
if err != nil {
259
- return false , nil , err
260
- }
261
- defer func () {
262
- _ , _ = io .Copy (io .Discard , res .Body )
263
- _ = res .Body .Close ()
264
- }()
265
-
266
- // TODO: tighten range of acceptable status codes
267
- if res .StatusCode >= 200 && res .StatusCode < 300 {
268
- identityInfo := aws.IdentityResponse {}
269
- if err := json .NewDecoder (res .Body ).Decode (& identityInfo ); err != nil {
270
- return false , nil , err
271
- }
272
-
273
- extraData := map [string ]string {
274
- "rotation_guide" : "https://howtorotate.com/docs/tutorials/aws/" ,
275
- "account" : identityInfo .GetCallerIdentityResponse .GetCallerIdentityResult .Account ,
276
- "user_id" : identityInfo .GetCallerIdentityResponse .GetCallerIdentityResult .UserID ,
277
- "arn" : identityInfo .GetCallerIdentityResponse .GetCallerIdentityResult .Arn ,
278
- }
279
- return true , extraData , nil
280
- } else if res .StatusCode == 403 {
281
- // Experimentation has indicated that if you make two GetCallerIdentity requests within five seconds that
249
+ // Experimentation has indicated that if you make multiple GetCallerIdentity requests within five seconds that
282
250
// share a key ID but are signed with different secrets the second one will be rejected with a 403 that
283
251
// carries a SignatureDoesNotMatch code in its body. This happens even if the second ID-secret pair is
284
252
// valid. Since this is exactly our access pattern, we need to work around it.
285
253
//
286
254
// Fortunately, experimentation has also revealed a workaround: simply resubmit the second request. The
287
- // response to the resubmission will be as expected. But there's a caveat: You can't have closed the body of
288
- // the response to the original second request, or read to its end, or the resubmission will also yield a
289
- // SignatureDoesNotMatch. For this reason, we have to re-request all 403s. We can't re-request only
290
- // SignatureDoesNotMatch responses, because we can only tell whether a given 403 is a SignatureDoesNotMatch
291
- // after decoding its response body, which requires reading the entire response body, which disables the
292
- // workaround.
255
+ // response to the resubmission will be as expected.
293
256
//
294
257
// We are clearly deep in the guts of AWS implementation details here, so this all might change with no
295
258
// notice. If you're here because something in this detector broke, you have my condolences.
296
- if retryOn403 {
297
- return s .verifyMatch (ctx , resIDMatch , resSecretMatch , false )
298
- }
299
-
300
- var body aws.ErrorResponseBody
301
- if err = json .NewDecoder (res .Body ).Decode (& body ); err != nil {
302
- return false , nil , fmt .Errorf ("couldn't parse the sts response body (%v)" , err )
303
- }
304
- // All instances of the code I've seen in the wild are PascalCased but this check is
305
- // case-insensitive out of an abundance of caution
306
- if strings .EqualFold (body .Error .Code , "InvalidClientTokenId" ) {
259
+ if strings .Contains (err .Error (), "StatusCode: 403" ) {
260
+ if retryOn403 {
261
+ return s .verifyMatch (ctx , resIDMatch , resSecretMatch , false )
262
+ }
263
+ return false , nil , nil
264
+ } else if strings .Contains (err .Error (), "InvalidClientTokenId" ) {
307
265
return false , nil , nil
308
266
}
309
- return false , nil , fmt .Errorf ("request returned status %d with an unexpected reason (%s: %s)" , res .StatusCode , body .Error .Code , body .Error .Message )
310
- } else {
311
- return false , nil , fmt .Errorf ("request to %v returned unexpected status %d" , res .Request .URL , res .StatusCode )
267
+ return false , nil , fmt .Errorf ("request returned unexpected error: %w" , err )
268
+ }
269
+
270
+ extraData := map [string ]string {
271
+ "rotation_guide" : "https://howtorotate.com/docs/tutorials/aws/" ,
272
+ "account" : * resp .Account ,
273
+ "user_id" : * resp .UserId ,
274
+ "arn" : * resp .Arn ,
312
275
}
276
+ return true , extraData , nil
313
277
}
314
278
315
279
func (s scanner ) CleanResults (results []detectors.Result ) []detectors.Result {
0 commit comments