11use crate :: tests:: util:: MockRequestExt ;
2+ use crate :: tests:: util:: insta:: api_token_redaction;
23use crate :: tests:: { RequestHelper , TestApp } ;
34use crate :: util:: token:: HashedToken ;
45use crate :: { models:: ApiToken , schema:: api_tokens} ;
56use 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;
610use crates_io_github:: { GitHubPublicKey , MockGitHubClient } ;
11+ use crates_io_trustpub:: access_token:: AccessToken ;
712use diesel:: prelude:: * ;
813use diesel_async:: RunQueryDsl ;
914use googletest:: prelude:: * ;
1015use insta:: { assert_json_snapshot, assert_snapshot} ;
1116use p256:: ecdsa:: { Signature , SigningKey , signature:: Signer } ;
1217use p256:: pkcs8:: DecodePrivateKey ;
18+ use secrecy:: ExposeSecret ;
1319use std:: sync:: LazyLock ;
1420
1521static URL : & str = "/api/github/secret-scanning/verify" ;
@@ -18,6 +24,14 @@ static URL: &str = "/api/github/secret-scanning/verify";
1824static GITHUB_ALERT : & [ u8 ] =
1925 br#"[{"token":"some_token","type":"some_type","url":"some_url","source":"some_source"}]"# ;
2026
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+
2135/// Private key for signing payloads (ECDSA P-256)
2236///
2337/// Generated specifically for testing - do not use in production.
@@ -48,6 +62,29 @@ fn sign_payload(payload: &[u8]) -> String {
4862 general_purpose:: STANDARD . encode ( signature. to_der ( ) )
4963}
5064
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+
5188fn github_mock ( ) -> MockGitHubClient {
5289 let mut mock = MockGitHubClient :: new ( ) ;
5390
@@ -279,3 +316,77 @@ async fn github_secret_alert_invalid_signature_fails() {
279316 let response = anon. run :: < ( ) > ( request) . await ;
280317 assert_snapshot ! ( response. status( ) , @"400 Bad Request" ) ;
281318}
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