From 176e2be91e8fd9cd0a1f6c3ce1299f9e8c8960be Mon Sep 17 00:00:00 2001 From: pd Date: Fri, 13 Mar 2026 13:36:30 -0600 Subject: [PATCH 1/4] fix: backup keys are k256 --- Cargo.lock | 14 ++++++++++++++ Cargo.toml | 5 +++-- src/routes/reset.rs | 17 +++++++++-------- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dbe6069..1a13c09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -786,6 +786,7 @@ dependencies = [ "http 1.3.1", "http-body-util", "josekit", + "k256", "mockito", "openidconnect", "p256 0.13.2", @@ -2272,6 +2273,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "once_cell", + "sha2", +] + [[package]] name = "lazy_static" version = "1.5.0" diff --git a/Cargo.toml b/Cargo.toml index 7be8e3c..516f8e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,3 @@ - [workspace] members = [".", "test-utils"] @@ -15,7 +14,8 @@ rand = "0.8.0" serde_json = "1.0.149" sha2 = "0.10.8" uuid = { version = "1.16.0", features = ["v4", "serde"] } -p256 = { version = "0.13.2", features = ["ecdsa"] } +p256 = { version = "0.13.2", default-features = false, features = ["ecdsa", "std"] } +k256 = { version = "0.13.4", default-features = false, features = ["ecdsa", "std"] } # dev workspace dependencies mockito = "1.7.1" @@ -55,6 +55,7 @@ http = "1.3.1" josekit = { version = "0.10.1", features = ["vendored"] } openidconnect = { workspace = true } p256 = { workspace = true } +k256 = { workspace = true } rand = { workspace = true } redis = { version = "0.32.5", features = ["connection-manager", "tokio-comp", "tokio-rustls-comp"] } reqwest = { version = "0.12.22", features = ["json", "rustls-tls"] } diff --git a/src/routes/reset.rs b/src/routes/reset.rs index 9f2e851..05bc0ea 100644 --- a/src/routes/reset.rs +++ b/src/routes/reset.rs @@ -10,9 +10,9 @@ use axum::http::StatusCode; use axum::{Extension, Json}; use base64::engine::general_purpose::STANDARD; use base64::Engine; -use p256::ecdsa::signature::Verifier; -use p256::ecdsa::{Signature, VerifyingKey}; -use p256::EncodedPoint; +use k256::ecdsa::signature::Verifier; +use k256::ecdsa::{Signature, VerifyingKey}; +use k256::EncodedPoint; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tracing::Instrument; @@ -178,17 +178,18 @@ fn verify_backup_account_signature( #[cfg(test)] mod tests { use super::*; - use p256::ecdsa::signature::Signer; - use p256::ecdsa::SigningKey; - use p256::elliptic_curve::rand_core::OsRng; - use p256::SecretKey; + use k256::ecdsa::signature::Signer; + use k256::ecdsa::SigningKey; + use k256::elliptic_curve::rand_core::OsRng; + use k256::SecretKey; #[test] fn test_verify_backup_account_signature_success() { // Generate a test keypair let secret_key = SecretKey::random(&mut OsRng); let signing_key = SigningKey::from(&secret_key); - let verifying_key = VerifyingKey::from(&signing_key); + // Explicit use of `secp256k1` in the tests + let verifying_key = k256::ecdsa::VerifyingKey::from(&signing_key); // Create backup_account_id from compressed public key let compressed_bytes = verifying_key.to_encoded_point(true).as_bytes().to_vec(); From 6a9355b86e2cadc7e7db8ead09bf869730975fe4 Mon Sep 17 00:00:00 2001 From: pd Date: Fri, 13 Mar 2026 13:38:15 -0600 Subject: [PATCH 2/4] extra doc --- src/routes/reset.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/routes/reset.rs b/src/routes/reset.rs index 05bc0ea..1e0cdf0 100644 --- a/src/routes/reset.rs +++ b/src/routes/reset.rs @@ -106,6 +106,9 @@ pub async fn handler( } /// Extracts the public key from a `backup_account_id` and verifies a signature on a given message. +/// +/// # Important +/// The `backup_account_id` is a public key on the `secp256k1` curve. fn verify_backup_account_signature( backup_account_id: &str, signature_base64: &str, From ed2bf6e6a86a7d8b269eac8168504ecf3ec5e988 Mon Sep 17 00:00:00 2001 From: pd Date: Fri, 13 Mar 2026 13:50:18 -0600 Subject: [PATCH 3/4] fix tests --- tests/reset_backup_integration.rs | 45 +++++++++++++++++++------------ 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/tests/reset_backup_integration.rs b/tests/reset_backup_integration.rs index 2d22654..c2ca413 100644 --- a/tests/reset_backup_integration.rs +++ b/tests/reset_backup_integration.rs @@ -20,14 +20,25 @@ use serde_json::json; use std::sync::Arc; /// Helper function to derive backup_account_id from a secret key -fn derive_backup_account_id_from_keypair(secret_key: &SecretKey) -> String { - let signing_key = SigningKey::from(secret_key); - let verifying_key = p256::ecdsa::VerifyingKey::from(&signing_key); +fn derive_backup_account_id_from_keypair(secret_key: &k256::SecretKey) -> String { + let signing_key = k256::ecdsa::SigningKey::from(secret_key); + let verifying_key = k256::ecdsa::VerifyingKey::from(&signing_key); let compressed_point = verifying_key.to_encoded_point(true); let compressed_bytes = compressed_point.as_bytes(); format!("backup_account_{}", hex::encode(compressed_bytes)) } +/// Signs a challenge using a backup key keypair. This differs from sync factors as it uses +/// the `secp256k1` curve. +fn sign_challenge_with_backup_key(secret_key: &k256::SecretKey, challenge: &str) -> String { + use k256::ecdsa::signature::Signer; + let signing_key = k256::ecdsa::SigningKey::from(secret_key.clone()); + let challenge = STANDARD.decode(challenge).unwrap(); + let signature: k256::ecdsa::Signature = signing_key.sign(&challenge); + let signature_der = signature.to_der(); + STANDARD.encode(signature_der) +} + /// Helper function to get a reset challenge async fn get_reset_challenge(backup_account_id: &str) -> serde_json::Value { let challenge_response = send_post_request( @@ -87,7 +98,7 @@ async fn verify_backup_deleted(backup_id: &str) { /// Helper function to create a backup with a specific backup_account_id derived from a keypair async fn create_test_backup_with_backup_account_id( - backup_account_secret_key: &SecretKey, + backup_account_secret_key: &k256::SecretKey, backup_data: &[u8], ) -> (String, SecretKey, SecretKey) { // Derive backup_account_id from the provided secret key @@ -183,7 +194,7 @@ async fn test_reset_backup_happy_path() { let factor_lookup = FactorLookup::new(environment, dynamodb_client.clone()); // Generate a keypair for the backup_account_id - let backup_account_secret_key = SecretKey::random(&mut OsRng); + let backup_account_secret_key = k256::SecretKey::random(&mut OsRng); // Create a backup with this backup_account_id let (backup_account_id, main_secret_key, sync_secret_key) = @@ -218,7 +229,7 @@ async fn test_reset_backup_happy_path() { let challenge_response = get_reset_challenge(&backup_account_id).await; // Sign the challenge with the backup_account_id secret key - let signature = sign_keypair_challenge( + let signature = sign_challenge_with_backup_key( &backup_account_secret_key, challenge_response["challenge"].as_str().unwrap(), ); @@ -257,7 +268,7 @@ async fn test_reset_backup_happy_path() { #[tokio::test] async fn test_reset_backup_with_incorrect_signature() { // Generate a keypair for the backup_account_id - let backup_account_secret_key = SecretKey::random(&mut OsRng); + let backup_account_secret_key = k256::SecretKey::random(&mut OsRng); // Create a backup with this backup_account_id let (backup_account_id, _, _) = @@ -306,7 +317,7 @@ async fn test_reset_backup_with_incorrect_signature() { #[tokio::test] async fn test_reset_backup_with_mismatched_public_key() { // Generate a keypair for the backup_account_id - let backup_account_secret_key = SecretKey::random(&mut OsRng); + let backup_account_secret_key = k256::SecretKey::random(&mut OsRng); // Create a backup with this backup_account_id let (backup_account_id, _, _) = @@ -356,7 +367,7 @@ async fn test_reset_backup_with_mismatched_public_key() { #[tokio::test] async fn test_reset_backup_challenge_token_reuse() { // Generate a keypair for the backup_account_id - let backup_account_secret_key = SecretKey::random(&mut OsRng); + let backup_account_secret_key = k256::SecretKey::random(&mut OsRng); // Create first backup let (backup_account_id_1, _, _) = @@ -367,7 +378,7 @@ async fn test_reset_backup_challenge_token_reuse() { let challenge_response = get_reset_challenge(&backup_account_id_1).await; // Sign the challenge - let signature = sign_keypair_challenge( + let signature = sign_challenge_with_backup_key( &backup_account_secret_key, challenge_response["challenge"].as_str().unwrap(), ); @@ -385,13 +396,13 @@ async fn test_reset_backup_challenge_token_reuse() { assert_eq!(reset_response.status(), StatusCode::NO_CONTENT); // Create a second backup with a different backup_account_id - let backup_account_secret_key_2 = SecretKey::random(&mut OsRng); + let backup_account_secret_key_2 = k256::SecretKey::random(&mut OsRng); let (backup_account_id_2, _, _) = create_test_backup_with_backup_account_id(&backup_account_secret_key_2, b"TEST BACKUP 2") .await; // Try to reuse the same challenge token on the second backup (should fail) - let signature_2 = sign_keypair_challenge( + let signature_2 = sign_challenge_with_backup_key( &backup_account_secret_key_2, challenge_response["challenge"].as_str().unwrap(), ); @@ -428,14 +439,14 @@ async fn test_reset_backup_challenge_token_reuse() { #[tokio::test] async fn test_reset_nonexistent_backup() { // Generate a keypair - let backup_account_secret_key = SecretKey::random(&mut OsRng); + let backup_account_secret_key = k256::SecretKey::random(&mut OsRng); let backup_account_id = derive_backup_account_id_from_keypair(&backup_account_secret_key); // Get a reset challenge (this should work even if backup doesn't exist) let challenge_response = get_reset_challenge(&backup_account_id).await; // Sign the challenge - let signature = sign_keypair_challenge( + let signature = sign_challenge_with_backup_key( &backup_account_secret_key, challenge_response["challenge"].as_str().unwrap(), ); @@ -458,9 +469,9 @@ async fn test_reset_nonexistent_backup() { #[tokio::test] async fn test_reset_backup_with_wrong_backup_account_id_in_token() { // Generate two keypairs for two different backup_account_ids - let backup_account_secret_key_1 = SecretKey::random(&mut OsRng); + let backup_account_secret_key_1 = k256::SecretKey::random(&mut OsRng); - let backup_account_secret_key_2 = SecretKey::random(&mut OsRng); + let backup_account_secret_key_2 = k256::SecretKey::random(&mut OsRng); let backup_account_id_2 = derive_backup_account_id_from_keypair(&backup_account_secret_key_2); // Create a backup with the first backup_account_id @@ -472,7 +483,7 @@ async fn test_reset_backup_with_wrong_backup_account_id_in_token() { let challenge_response = get_reset_challenge(&backup_account_id_2).await; // Sign the challenge with the first keypair - let signature = sign_keypair_challenge( + let signature = sign_challenge_with_backup_key( &backup_account_secret_key_1, challenge_response["challenge"].as_str().unwrap(), ); From 407ff6bfcc12b3750abca4f8611b9a109aa71b3f Mon Sep 17 00:00:00 2001 From: pd Date: Fri, 13 Mar 2026 13:55:14 -0600 Subject: [PATCH 4/4] fix --- tests/reset_backup_integration.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/reset_backup_integration.rs b/tests/reset_backup_integration.rs index c2ca413..3dad9f0 100644 --- a/tests/reset_backup_integration.rs +++ b/tests/reset_backup_integration.rs @@ -279,8 +279,8 @@ async fn test_reset_backup_with_incorrect_signature() { let challenge_response = get_reset_challenge(&backup_account_id).await; // Sign the challenge with a different keypair (incorrect signature) - let (_, wrong_secret_key) = generate_keypair(); - let wrong_signature = sign_keypair_challenge( + let wrong_secret_key = k256::SecretKey::random(&mut OsRng); + let wrong_signature = sign_challenge_with_backup_key( &wrong_secret_key, challenge_response["challenge"].as_str().unwrap(), );