1
1
use crate :: tests:: util:: MockRequestExt ;
2
+ use crate :: tests:: util:: insta:: api_token_redaction;
2
3
use crate :: tests:: { RequestHelper , TestApp } ;
3
4
use crate :: util:: token:: HashedToken ;
4
5
use crate :: { models:: ApiToken , schema:: api_tokens} ;
5
6
use base64:: { Engine as _, engine:: general_purpose} ;
7
+ use chrono:: { TimeDelta , Utc } ;
8
+ use crates_io_database:: models:: trustpub:: NewToken ;
9
+ use crates_io_database:: schema:: trustpub_tokens;
6
10
use crates_io_github:: { GitHubPublicKey , MockGitHubClient } ;
11
+ use crates_io_trustpub:: access_token:: AccessToken ;
7
12
use diesel:: prelude:: * ;
8
13
use diesel_async:: RunQueryDsl ;
9
14
use googletest:: prelude:: * ;
10
15
use insta:: { assert_json_snapshot, assert_snapshot} ;
11
16
use p256:: ecdsa:: { Signature , SigningKey , signature:: Signer } ;
12
17
use p256:: pkcs8:: DecodePrivateKey ;
18
+ use secrecy:: ExposeSecret ;
13
19
use std:: sync:: LazyLock ;
14
20
15
21
static URL : & str = "/api/github/secret-scanning/verify" ;
@@ -18,6 +24,14 @@ static URL: &str = "/api/github/secret-scanning/verify";
18
24
static GITHUB_ALERT : & [ u8 ] =
19
25
br#"[{"token":"some_token","type":"some_type","url":"some_url","source":"some_source"}]"# ;
20
26
27
+ /// Generate a GitHub alert with a given token
28
+ fn github_alert_with_token ( token : & str ) -> Vec < u8 > {
29
+ format ! (
30
+ r#"[{{"token":"{token}","type":"some_type","url":"some_url","source":"some_source"}}]"# ,
31
+ )
32
+ . into_bytes ( )
33
+ }
34
+
21
35
/// Private key for signing payloads (ECDSA P-256)
22
36
///
23
37
/// Generated specifically for testing - do not use in production.
@@ -48,6 +62,29 @@ fn sign_payload(payload: &[u8]) -> String {
48
62
general_purpose:: STANDARD . encode ( signature. to_der ( ) )
49
63
}
50
64
65
+ /// Generate a new Trusted Publishing token and its SHA256 hash
66
+ fn generate_trustpub_token ( ) -> ( String , Vec < u8 > ) {
67
+ let token = AccessToken :: generate ( ) ;
68
+ let finalized_token = token. finalize ( ) . expose_secret ( ) . to_string ( ) ;
69
+ let hashed_token = token. sha256 ( ) . to_vec ( ) ;
70
+ ( finalized_token, hashed_token)
71
+ }
72
+
73
+ /// Create a new Trusted Publishing token in the database
74
+ async fn insert_trustpub_token ( conn : & mut diesel_async:: AsyncPgConnection ) -> QueryResult < String > {
75
+ let ( token, hashed_token) = generate_trustpub_token ( ) ;
76
+
77
+ let new_token = NewToken {
78
+ expires_at : Utc :: now ( ) + TimeDelta :: minutes ( 30 ) ,
79
+ hashed_token : & hashed_token,
80
+ crate_ids : & [ 1 ] , // Arbitrary crate ID for testing
81
+ } ;
82
+
83
+ new_token. insert ( conn) . await ?;
84
+
85
+ Ok ( token)
86
+ }
87
+
51
88
fn github_mock ( ) -> MockGitHubClient {
52
89
let mut mock = MockGitHubClient :: new ( ) ;
53
90
@@ -279,3 +316,77 @@ async fn github_secret_alert_invalid_signature_fails() {
279
316
let response = anon. run :: < ( ) > ( request) . await ;
280
317
assert_snapshot ! ( response. status( ) , @"400 Bad Request" ) ;
281
318
}
319
+
320
+ #[ tokio:: test( flavor = "multi_thread" ) ]
321
+ async fn github_secret_alert_revokes_trustpub_token ( ) {
322
+ let ( app, anon) = TestApp :: init ( ) . with_github ( github_mock ( ) ) . empty ( ) . await ;
323
+ let mut conn = app. db_conn ( ) . await ;
324
+
325
+ // Generate a valid Trusted Publishing token
326
+ let token = insert_trustpub_token ( & mut conn) . await . unwrap ( ) ;
327
+
328
+ // Verify the token exists in the database
329
+ let count = trustpub_tokens:: table
330
+ . count ( )
331
+ . get_result :: < i64 > ( & mut conn)
332
+ . await
333
+ . unwrap ( ) ;
334
+ assert_eq ! ( count, 1 ) ;
335
+
336
+ // Send the GitHub alert to the API endpoint
337
+ let mut request = anon. post_request ( URL ) ;
338
+ let vec = github_alert_with_token ( & token) ;
339
+ request. header ( "GITHUB-PUBLIC-KEY-IDENTIFIER" , KEY_IDENTIFIER ) ;
340
+ request. header ( "GITHUB-PUBLIC-KEY-SIGNATURE" , & sign_payload ( & vec) ) ;
341
+ * request. body_mut ( ) = vec. into ( ) ;
342
+ let response = anon. run :: < ( ) > ( request) . await ;
343
+ assert_snapshot ! ( response. status( ) , @"200 OK" ) ;
344
+ assert_json_snapshot ! ( response. json( ) , {
345
+ "[].token_raw" => api_token_redaction( )
346
+ } ) ;
347
+
348
+ // Verify the token was deleted from the database
349
+ let count = trustpub_tokens:: table
350
+ . count ( )
351
+ . get_result :: < i64 > ( & mut conn)
352
+ . await
353
+ . unwrap ( ) ;
354
+ assert_eq ! ( count, 0 ) ;
355
+ }
356
+
357
+ #[ tokio:: test( flavor = "multi_thread" ) ]
358
+ async fn github_secret_alert_for_unknown_trustpub_token ( ) {
359
+ let ( app, anon) = TestApp :: init ( ) . with_github ( github_mock ( ) ) . empty ( ) . await ;
360
+ let mut conn = app. db_conn ( ) . await ;
361
+
362
+ // Generate a valid Trusted Publishing token but don't insert it into the database
363
+ let ( token, _) = generate_trustpub_token ( ) ;
364
+
365
+ // Verify no tokens exist in the database
366
+ let count = trustpub_tokens:: table
367
+ . count ( )
368
+ . get_result :: < i64 > ( & mut conn)
369
+ . await
370
+ . unwrap ( ) ;
371
+ assert_eq ! ( count, 0 ) ;
372
+
373
+ // Send the GitHub alert to the API endpoint
374
+ let mut request = anon. post_request ( URL ) ;
375
+ let vec = github_alert_with_token ( & token) ;
376
+ request. header ( "GITHUB-PUBLIC-KEY-IDENTIFIER" , KEY_IDENTIFIER ) ;
377
+ request. header ( "GITHUB-PUBLIC-KEY-SIGNATURE" , & sign_payload ( & vec) ) ;
378
+ * request. body_mut ( ) = vec. into ( ) ;
379
+ let response = anon. run :: < ( ) > ( request) . await ;
380
+ assert_snapshot ! ( response. status( ) , @"200 OK" ) ;
381
+ assert_json_snapshot ! ( response. json( ) , {
382
+ "[].token_raw" => api_token_redaction( )
383
+ } ) ;
384
+
385
+ // Verify still no tokens exist in the database
386
+ let count = trustpub_tokens:: table
387
+ . count ( )
388
+ . get_result :: < i64 > ( & mut conn)
389
+ . await
390
+ . unwrap ( ) ;
391
+ assert_eq ! ( count, 0 ) ;
392
+ }
0 commit comments