Skip to content

Commit

Permalink
tests: Add basic tests for credential management
Browse files Browse the repository at this point in the history
  • Loading branch information
robin-nitrokey committed May 22, 2024
1 parent 487b469 commit eb455a7
Show file tree
Hide file tree
Showing 4 changed files with 496 additions and 5 deletions.
9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,22 @@ log-warn = []
log-error = []

[dev-dependencies]
aes = "0.8.4"
cbc = { version = "0.1.2", features = ["alloc"] }
ciborium = { version = "0.2.2" }
cipher = "0.4.4"
ctaphid = { version = "0.3.1", default-features = false }
delog = { version = "0.1.6", features = ["std-log"] }
env_logger = "0.11.0"
hmac = "0.12.1"
interchange = "0.3.0"
log = "0.4.21"
# quickcheck = "1"
p256 = { version = "0.13.2", features = ["ecdh"] }
rand = "0.8.4"
sha2 = "0.10"
trussed = { version = "0.1", features = ["virt"] }
trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] }
trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt"] }
trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] }
usbd-ctaphid = "0.1.0"

[package.metadata.docs.rs]
Expand Down
183 changes: 181 additions & 2 deletions tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@
mod virt;
mod webauthn;

use virt::Ctap2Error;
use webauthn::{GetInfo, MakeCredential, PubKeyCredParam, Rp, User};
use std::collections::BTreeMap;

use ciborium::Value;

use virt::{Ctap2, Ctap2Error};
use webauthn::{
ClientPin, CredentialManagement, CredentialManagementParams, GetInfo, KeyAgreementKey,
MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredParam, PublicKey, Rp, SharedSecret,
User,
};

#[test]
fn test_ping() {
Expand All @@ -19,9 +27,77 @@ fn test_get_info() {
let reply = device.exec(GetInfo).unwrap();
assert!(reply.versions.contains(&"FIDO_2_0".to_owned()));
assert!(reply.versions.contains(&"FIDO_2_1".to_owned()));
assert_eq!(reply.pin_protocols, Some(vec![2, 1]));
});
}

fn get_shared_secret(device: &Ctap2, platform_key_agreement: &KeyAgreementKey) -> SharedSecret {
let reply = device.exec(ClientPin::new(2, 2)).unwrap();
let authenticator_key_agreement: PublicKey = reply.key_agreement.unwrap().into();
platform_key_agreement.shared_secret(&authenticator_key_agreement)
}

fn set_pin(
device: &Ctap2,
key_agreement_key: &KeyAgreementKey,
shared_secret: &SharedSecret,
pin: &[u8],
) {
let mut padded_pin = [0; 64];
padded_pin[..pin.len()].copy_from_slice(pin);
let pin_enc = shared_secret.encrypt(&padded_pin);
let pin_auth = shared_secret.authenticate(&pin_enc);
let mut request = ClientPin::new(2, 3);
request.key_agreement = Some(key_agreement_key.public_key());
request.new_pin_enc = Some(pin_enc);
request.pin_auth = Some(pin_auth);
device.exec(request).unwrap();
}

#[test]
fn test_set_pin() {
let key_agreement_key = KeyAgreementKey::generate();
virt::run_ctap2(|device| {
let shared_secret = get_shared_secret(&device, &key_agreement_key);
set_pin(&device, &key_agreement_key, &shared_secret, b"123456");
})
}

fn get_pin_token(
device: &Ctap2,
key_agreement_key: &KeyAgreementKey,
shared_secret: &SharedSecret,
pin: &[u8],
permissions: u8,
rp_id: Option<String>,
) -> PinToken {
use sha2::{Digest as _, Sha256};

let mut hasher = Sha256::new();
hasher.update(pin);
let pin_hash = hasher.finalize();
let pin_hash_enc = shared_secret.encrypt(&pin_hash[..16]);
let mut request = ClientPin::new(2, 9);
request.key_agreement = Some(key_agreement_key.public_key());
request.pin_hash_enc = Some(pin_hash_enc);
request.permissions = Some(permissions);
request.rp_id = rp_id;
let reply = device.exec(request).unwrap();
let encrypted_pin_token = reply.pin_token.as_ref().unwrap().as_bytes().unwrap();
shared_secret.decrypt_pin_token(encrypted_pin_token)
}

#[test]
fn test_get_pin_token() {
let key_agreement_key = KeyAgreementKey::generate();
let pin = b"123456";
virt::run_ctap2(|device| {
let shared_secret = get_shared_secret(&device, &key_agreement_key);
set_pin(&device, &key_agreement_key, &shared_secret, pin);
get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None);
})
}

#[test]
fn test_make_credential() {
virt::run_ctap2(|device| {
Expand Down Expand Up @@ -51,3 +127,106 @@ fn test_make_credential_invalid_params() {
assert_eq!(result, Err(Ctap2Error(0x26)));
});
}

#[derive(Debug)]
struct TestListCredentials {
pin_token_rp_id: bool,
}

impl TestListCredentials {
fn run(&self) {
let key_agreement_key = KeyAgreementKey::generate();
let pin = b"123456";
let rp_id = "example.com";
let user_id = b"id123";
virt::run_ctap2(|device| {
let shared_secret = get_shared_secret(&device, &key_agreement_key);
set_pin(&device, &key_agreement_key, &shared_secret, pin);

let pin_token =
get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None);
// TODO: client data
let client_data_hash = b"";
let pin_auth = pin_token.authenticate(client_data_hash);

let rp = Rp::new(rp_id);
let user = User::new(user_id).name("john.doe").display_name("John Doe");
let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)];
let mut request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params);
request.options = Some(MakeCredentialOptions::default().rk(true));
request.pin_auth = Some(pin_auth);
request.pin_protocol = Some(2);
let reply = device.exec(request).unwrap();
let auth_data = reply.auth_data.as_bytes().unwrap();
assert!(auth_data.len() >= 37, "{}", auth_data.len());
assert_eq!(
auth_data[32] & 0b1,
0b1,
"up flag not set in auth_data: 0b{:b}",
auth_data[32]
);
assert_eq!(
auth_data[32] & 0b100,
0b100,
"uv flag not set in auth_data: 0b{:b}",
auth_data[32]
);

let pin_token =
get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x04, None);
let pin_auth = pin_token.authenticate(&[0x02]);
let request = CredentialManagement {
subcommand: 0x02,
subcommand_params: None,
pin_protocol: 2,
pin_auth,
};
let reply = device.exec(request).unwrap();
let rp: BTreeMap<String, Value> = reply.rp.unwrap().deserialized().unwrap();
// TODO: check rp ID hash
assert!(reply.rp_id_hash.is_some());
assert_eq!(reply.total_rps, Some(1));
assert_eq!(rp.get("id").unwrap(), &Value::from(rp_id));

let pin_token_rp_id = self.pin_token_rp_id.then(|| rp_id.to_owned());
let pin_token = get_pin_token(
&device,
&key_agreement_key,
&shared_secret,
pin,
0x04,
pin_token_rp_id,
);
let params = CredentialManagementParams {
rp_id_hash: reply.rp_id_hash.unwrap().as_bytes().unwrap().to_owned(),
};
let mut pin_auth_param = vec![0x04];
pin_auth_param.extend_from_slice(&params.serialized());
let pin_auth = pin_token.authenticate(&pin_auth_param);
let request = CredentialManagement {
subcommand: 0x04,
subcommand_params: Some(params),
pin_protocol: 2,
pin_auth,
};
let reply = device.exec(request).unwrap();
let user: BTreeMap<String, Value> = reply.user.unwrap().deserialized().unwrap();
assert_eq!(reply.total_credentials, Some(1));
assert_eq!(user.get("id").unwrap(), &Value::from(user_id.as_slice()));
});
}
}

#[test]
fn test_list_credentials() {
// true is omitted because it currently fails, see:
// https://github.com/Nitrokey/fido-authenticator/issues/80
for pin_token_rp_id in [false] {
let test = TestListCredentials { pin_token_rp_id };
println!("{}", "=".repeat(80));
println!("Running test:");
println!("{test:#?}");
println!();
test.run();
}
}
6 changes: 5 additions & 1 deletion tests/virt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,11 @@ impl Ctap2<'_> {
}
err => panic!("failed to execute CTAP2 command: {err:?}"),
})?;
let value: Value = ciborium::from_reader(reply.as_slice()).unwrap();
let value: Value = if reply.is_empty() {
Value::Map(Vec::new())
} else {
ciborium::from_reader(reply.as_slice()).unwrap()
};
log::debug!("Received reply {value:?}");
Ok(value.into())
}
Expand Down
Loading

0 comments on commit eb455a7

Please sign in to comment.