Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

[workspace]
members = [".", "test-utils"]

Expand All @@ -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"
Expand Down Expand Up @@ -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"] }
Expand Down
20 changes: 12 additions & 8 deletions src/routes/reset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
cursor[bot] marked this conversation as resolved.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tracing::Instrument;
Expand Down Expand Up @@ -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(
Comment thread
paolodamico marked this conversation as resolved.
backup_account_id: &str,
signature_base64: &str,
Expand Down Expand Up @@ -178,17 +181,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();
Expand Down
45 changes: 28 additions & 17 deletions tests/reset_backup_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) =
Expand Down Expand Up @@ -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(),
);
Expand Down Expand Up @@ -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);
Comment thread
cursor[bot] marked this conversation as resolved.

// Create a backup with this backup_account_id
let (backup_account_id, _, _) =
Expand Down Expand Up @@ -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, _, _) =
Expand Down Expand Up @@ -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, _, _) =
Expand All @@ -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(),
);
Expand All @@ -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(),
);
Expand Down Expand Up @@ -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(),
);
Expand All @@ -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
Expand All @@ -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(),
);
Expand Down