You've already forked fido-authenticator
mirror of
https://github.com/trussed-dev/fido-authenticator.git
synced 2026-03-11 16:36:21 -07:00
This fixes compatibility with CTAP 2.1. Fixes: https://github.com/Nitrokey/fido-authenticator/issues/118
2086 lines
78 KiB
Rust
2086 lines
78 KiB
Rust
//! The `ctap_types::ctap2::Authenticator` implementation.
|
|
|
|
use credential_management::CredentialManagement;
|
|
use ctap_types::{
|
|
ctap2::{
|
|
self, client_pin::Permissions, AttestationFormatsPreference, AttestationStatement,
|
|
AttestationStatementFormat, Authenticator, NoneAttestationStatement,
|
|
PackedAttestationStatement, VendorOperation,
|
|
},
|
|
heapless::{String, Vec},
|
|
heapless_bytes::Bytes,
|
|
sizes,
|
|
webauthn::PublicKeyCredentialUserEntity,
|
|
ByteArray, Error,
|
|
};
|
|
use littlefs2_core::{path, Path, PathBuf};
|
|
use sha2::{Digest as _, Sha256};
|
|
|
|
use trussed_core::{
|
|
syscall, try_syscall,
|
|
types::{KeyId, Location, Mechanism, MediumData, Message, StorageAttributes},
|
|
};
|
|
|
|
use crate::{
|
|
constants::{self, MAX_RESIDENT_CREDENTIALS_GUESSTIMATE},
|
|
credential::{self, Credential, FullCredential, Key, StrippedCredential},
|
|
format_hex, state, Result, SigningAlgorithm, TrussedRequirements, UserPresence,
|
|
};
|
|
|
|
#[allow(unused_imports)]
|
|
use crate::msp;
|
|
|
|
pub mod credential_management;
|
|
pub mod large_blobs;
|
|
pub mod pin;
|
|
|
|
use pin::{PinProtocol, PinProtocolVersion, RpScope, SharedSecret};
|
|
|
|
pub const RK_DIR: &Path = path!("rk");
|
|
|
|
/// Implement `ctap2::Authenticator` for our Authenticator.
|
|
impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenticator<UP, T> {
|
|
#[inline(never)]
|
|
fn get_info(&mut self) -> ctap2::get_info::Response {
|
|
use ctap2::get_info::{Extension, Transport, Version};
|
|
|
|
debug_now!("remaining stack size: {} bytes", msp() - 0x2000_0000);
|
|
|
|
let mut versions = Vec::new();
|
|
versions.push(Version::U2fV2).unwrap();
|
|
versions.push(Version::Fido2_0).unwrap();
|
|
versions.push(Version::Fido2_1).unwrap();
|
|
|
|
let mut extensions = Vec::new();
|
|
extensions.push(Extension::CredProtect).unwrap();
|
|
extensions.push(Extension::HmacSecret).unwrap();
|
|
if self.config.supports_large_blobs() {
|
|
extensions.push(Extension::LargeBlobKey).unwrap();
|
|
}
|
|
extensions.push(Extension::ThirdPartyPayment).unwrap();
|
|
|
|
let mut pin_protocols = Vec::new();
|
|
for pin_protocol in self.pin_protocols() {
|
|
pin_protocols.push(u8::from(*pin_protocol)).unwrap();
|
|
}
|
|
|
|
let mut options = ctap2::get_info::CtapOptions::default();
|
|
options.rk = true;
|
|
options.up = true;
|
|
options.plat = Some(false);
|
|
options.cred_mgmt = Some(true);
|
|
options.client_pin = match self.state.persistent.pin_is_set() {
|
|
true => Some(true),
|
|
false => Some(false),
|
|
};
|
|
options.large_blobs = Some(self.config.supports_large_blobs());
|
|
options.pin_uv_auth_token = Some(true);
|
|
options.make_cred_uv_not_rqd = Some(true);
|
|
|
|
let mut transports = Vec::new();
|
|
if self.config.nfc_transport {
|
|
transports.push(Transport::Nfc).unwrap();
|
|
}
|
|
transports.push(Transport::Usb).unwrap();
|
|
|
|
let mut attestation_formats = Vec::new();
|
|
attestation_formats
|
|
.push(AttestationStatementFormat::Packed)
|
|
.unwrap();
|
|
attestation_formats
|
|
.push(AttestationStatementFormat::None)
|
|
.unwrap();
|
|
|
|
let (_, aaguid) = self.state.identity.attestation(&mut self.trussed);
|
|
|
|
let mut response = ctap2::get_info::Response::default();
|
|
response.versions = versions;
|
|
response.extensions = Some(extensions);
|
|
response.aaguid = Bytes::from_slice(&aaguid).unwrap();
|
|
response.options = Some(options);
|
|
response.transports = Some(transports);
|
|
// 1200
|
|
response.max_msg_size = Some(self.config.max_msg_size);
|
|
response.pin_protocols = Some(pin_protocols);
|
|
response.max_creds_in_list = Some(ctap_types::sizes::MAX_CREDENTIAL_COUNT_IN_LIST);
|
|
response.max_cred_id_length = Some(ctap_types::sizes::MAX_CREDENTIAL_ID_LENGTH);
|
|
response.attestation_formats = Some(attestation_formats);
|
|
response
|
|
}
|
|
|
|
#[inline(never)]
|
|
fn get_next_assertion(&mut self) -> Result<ctap2::get_assertion::Response> {
|
|
// 3. previous GA/GNA >30s ago -> discard stat
|
|
// this is optional over NFC
|
|
if false {
|
|
self.state.runtime.clear_credential_cache();
|
|
self.state.runtime.active_get_assertion = None;
|
|
return Err(Error::NotAllowed);
|
|
}
|
|
//
|
|
// 1./2. don't remember / don't have left any credentials
|
|
// 4. select credential
|
|
// let data = syscall!(self.trussed.read_file(
|
|
// timestamp_hash.location,
|
|
// timestamp_hash.path,
|
|
// )).data;
|
|
if self.state.runtime.active_get_assertion.is_none() {
|
|
return Err(Error::NotAllowed);
|
|
}
|
|
let credential = self
|
|
.state
|
|
.runtime
|
|
.pop_credential(&mut self.trussed)
|
|
.ok_or(Error::NotAllowed)?;
|
|
|
|
// 5. suppress PII if no UV was performed in original GA
|
|
|
|
// 6. sign
|
|
// 7. reset timer
|
|
// 8. increment credential counter (not applicable)
|
|
|
|
self.assert_with_credential(None, Credential::Full(credential))
|
|
}
|
|
|
|
#[inline(never)]
|
|
fn make_credential(
|
|
&mut self,
|
|
parameters: &ctap2::make_credential::Request,
|
|
) -> Result<ctap2::make_credential::Response> {
|
|
let rp_id_hash = self.hash(parameters.rp.id.as_ref());
|
|
|
|
// 1-4.
|
|
if let Some(options) = parameters.options.as_ref() {
|
|
// up option is not valid for make_credential
|
|
if options.up.is_some() {
|
|
return Err(Error::InvalidOption);
|
|
}
|
|
}
|
|
if parameters.enterprise_attestation.is_some() {
|
|
return Err(Error::InvalidParameter);
|
|
}
|
|
let uv_performed = self.pin_prechecks(
|
|
¶meters.options,
|
|
parameters.pin_auth.map(AsRef::as_ref),
|
|
parameters.pin_protocol,
|
|
parameters.client_data_hash.as_ref(),
|
|
Permissions::MAKE_CREDENTIAL,
|
|
¶meters.rp.id,
|
|
)?;
|
|
|
|
// 5. "persist credProtect value for this credential"
|
|
// --> seems out of place here, see 9.
|
|
|
|
// 6. excludeList present, contains credential ID on this authenticator bound to RP?
|
|
// --> wait for UP, error CredentialExcluded
|
|
if let Some(exclude_list) = ¶meters.exclude_list {
|
|
for descriptor in exclude_list.iter() {
|
|
let result = Credential::try_from(self, &rp_id_hash, descriptor);
|
|
if let Ok(excluded_cred) = result {
|
|
use credential::CredentialProtectionPolicy;
|
|
// If UV is not performed, than CredProtectRequired credentials should not be visibile.
|
|
if !(excluded_cred.cred_protect() == Some(CredentialProtectionPolicy::Required))
|
|
|| uv_performed
|
|
{
|
|
info_now!("Excluded!");
|
|
self.up
|
|
.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?;
|
|
return Err(Error::CredentialExcluded);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 7. check pubKeyCredParams algorithm is valid + supported COSE identifier
|
|
|
|
let mut algorithm: Option<SigningAlgorithm> = None;
|
|
for param in parameters.pub_key_cred_params.0.iter() {
|
|
match param.alg {
|
|
-7 => {
|
|
if algorithm.is_none() {
|
|
algorithm = Some(SigningAlgorithm::P256);
|
|
}
|
|
}
|
|
-8 => {
|
|
algorithm = Some(SigningAlgorithm::Ed25519);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
let algorithm = algorithm.ok_or(Error::UnsupportedAlgorithm)?;
|
|
info_now!("algo: {:?}", algorithm as i32);
|
|
|
|
// 8. process options; on known but unsupported error UnsupportedOption
|
|
|
|
let mut rk_requested = false;
|
|
// TODO: why is this unused?
|
|
let mut _uv_requested = false;
|
|
let _up_requested = true; // can't be toggled
|
|
|
|
info_now!("MC options: {:?}", ¶meters.options);
|
|
if let Some(ref options) = ¶meters.options {
|
|
if Some(true) == options.rk {
|
|
rk_requested = true;
|
|
}
|
|
if Some(true) == options.uv {
|
|
_uv_requested = true;
|
|
}
|
|
}
|
|
|
|
// 9. process extensions
|
|
let mut hmac_secret_requested = None;
|
|
// let mut cred_protect_requested = CredentialProtectionPolicy::Optional;
|
|
let mut cred_protect_requested = None;
|
|
let mut large_blob_key_requested = false;
|
|
let mut third_party_payment_requested = false;
|
|
if let Some(extensions) = ¶meters.extensions {
|
|
hmac_secret_requested = extensions.hmac_secret;
|
|
|
|
if let Some(policy) = &extensions.cred_protect {
|
|
cred_protect_requested =
|
|
Some(credential::CredentialProtectionPolicy::try_from(*policy)?);
|
|
}
|
|
|
|
if self.config.supports_large_blobs() {
|
|
if let Some(large_blob_key) = extensions.large_blob_key {
|
|
if large_blob_key {
|
|
if !rk_requested {
|
|
// the largeBlobKey extension is only available for resident keys
|
|
return Err(Error::InvalidOption);
|
|
}
|
|
large_blob_key_requested = true;
|
|
} else {
|
|
// large_blob_key must be Some(true) or omitted, Some(false) is invalid
|
|
return Err(Error::InvalidOption);
|
|
}
|
|
}
|
|
}
|
|
|
|
third_party_payment_requested = extensions.third_party_payment.unwrap_or_default();
|
|
}
|
|
|
|
// debug_now!("hmac-secret = {:?}, credProtect = {:?}", hmac_secret_requested, cred_protect_requested);
|
|
|
|
// 10. get UP, if denied error OperationDenied
|
|
self.up
|
|
.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?;
|
|
|
|
// 11. generate credential keypair
|
|
let location = match rk_requested {
|
|
true => Location::Internal,
|
|
false => Location::Volatile,
|
|
};
|
|
let private_key = algorithm.generate_private_key(&mut self.trussed, location);
|
|
let cose_public_key = algorithm.derive_public_key(&mut self.trussed, private_key);
|
|
|
|
// 12. if `rk` is set, store or overwrite key pair, if full error KeyStoreFull
|
|
|
|
// 12.a generate credential
|
|
let key_parameter = match rk_requested {
|
|
true => Key::ResidentKey(private_key),
|
|
false => {
|
|
// WrappedKey version
|
|
let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?;
|
|
let wrapped_key = syscall!(self.trussed.wrap_key_chacha8poly1305(
|
|
wrapping_key,
|
|
private_key,
|
|
&[],
|
|
None
|
|
))
|
|
.wrapped_key;
|
|
|
|
// 32B key, 12B nonce, 16B tag + some info on algorithm (P256/Ed25519)
|
|
// Turns out it's size 92 (enum serialization not optimized yet...)
|
|
// let mut wrapped_key = Bytes::<60>::new();
|
|
// wrapped_key.extend_from_slice(&wrapped_key_msg).unwrap();
|
|
Key::WrappedKey(wrapped_key.to_bytes().map_err(|_| Error::Other)?)
|
|
}
|
|
};
|
|
|
|
// injecting this is a bit mehhh..
|
|
let nonce = self.nonce();
|
|
info_now!("nonce = {:?}", &nonce);
|
|
|
|
// 12.b generate credential ID { = AEAD(Serialize(Credential)) }
|
|
let kek = self
|
|
.state
|
|
.persistent
|
|
.key_encryption_key(&mut self.trussed)?;
|
|
|
|
// store it.
|
|
// TODO: overwrite, error handling with KeyStoreFull
|
|
|
|
let large_blob_key = if large_blob_key_requested {
|
|
let key = syscall!(self.trussed.random_bytes(32)).bytes;
|
|
Some(ByteArray::new(key.as_slice().try_into().unwrap()))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let credential = FullCredential::new(
|
|
credential::CtapVersion::Fido21Pre,
|
|
¶meters.rp,
|
|
¶meters.user,
|
|
algorithm as i32,
|
|
key_parameter,
|
|
self.state.persistent.timestamp(&mut self.trussed)?,
|
|
hmac_secret_requested,
|
|
cred_protect_requested,
|
|
large_blob_key,
|
|
third_party_payment_requested.then_some(true),
|
|
nonce,
|
|
);
|
|
|
|
// note that this does the "stripping" of OptionalUI etc.
|
|
let credential_id =
|
|
StrippedCredential::from(&credential).id(&mut self.trussed, kek, &rp_id_hash)?;
|
|
|
|
if rk_requested {
|
|
// serialization with all metadata
|
|
let serialized_credential = credential.serialize()?;
|
|
|
|
// first delete any other RK cred with same RP + UserId if there is one.
|
|
self.delete_resident_key_by_user_id(&rp_id_hash, credential.user.id())
|
|
.ok();
|
|
|
|
let mut key_store_full = self.can_fit(serialized_credential.len()) == Some(false)
|
|
|| CredentialManagement::new(self).count_credentials()?
|
|
>= self
|
|
.config
|
|
.max_resident_credential_count
|
|
.unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE);
|
|
|
|
if !key_store_full {
|
|
// then store key, making it resident
|
|
let credential_id_hash = self.hash(credential_id.0.as_ref());
|
|
let result = try_syscall!(self.trussed.write_file(
|
|
Location::Internal,
|
|
rk_path(&rp_id_hash, &credential_id_hash),
|
|
serialized_credential,
|
|
// user attribute for later easy lookup
|
|
// Some(rp_id_hash.clone()),
|
|
None,
|
|
));
|
|
key_store_full = result.is_err();
|
|
}
|
|
|
|
if key_store_full {
|
|
return Err(Error::KeyStoreFull);
|
|
}
|
|
}
|
|
|
|
// 13. generate and return attestation statement using clientDataHash
|
|
|
|
// 13.a AuthenticatorData and its serialization
|
|
use ctap2::AuthenticatorDataFlags as Flags;
|
|
info_now!("MC created cred id");
|
|
|
|
let (attestation_maybe, aaguid) = self.state.identity.attestation(&mut self.trussed);
|
|
|
|
let authenticator_data = ctap2::make_credential::AuthenticatorData {
|
|
rp_id_hash: &rp_id_hash,
|
|
|
|
flags: {
|
|
let mut flags = Flags::USER_PRESENCE;
|
|
if uv_performed {
|
|
flags |= Flags::USER_VERIFIED;
|
|
}
|
|
if true {
|
|
flags |= Flags::ATTESTED_CREDENTIAL_DATA;
|
|
}
|
|
if hmac_secret_requested.is_some() || cred_protect_requested.is_some() {
|
|
flags |= Flags::EXTENSION_DATA;
|
|
}
|
|
flags
|
|
},
|
|
|
|
sign_count: self.state.persistent.timestamp(&mut self.trussed)?,
|
|
|
|
attested_credential_data: {
|
|
// debug_now!("acd in, cid len {}, pk len {}", credential_id.0.len(), cose_public_key.len());
|
|
let attested_credential_data = ctap2::make_credential::AttestedCredentialData {
|
|
aaguid: &aaguid,
|
|
credential_id: &credential_id.0,
|
|
credential_public_key: &cose_public_key,
|
|
};
|
|
// debug_now!("cose PK = {:?}", &attested_credential_data.credential_public_key);
|
|
Some(attested_credential_data)
|
|
},
|
|
|
|
extensions: {
|
|
if hmac_secret_requested.is_some() || cred_protect_requested.is_some() {
|
|
let mut extensions = ctap2::make_credential::Extensions::default();
|
|
extensions.cred_protect = parameters.extensions.as_ref().unwrap().cred_protect;
|
|
extensions.hmac_secret = parameters.extensions.as_ref().unwrap().hmac_secret;
|
|
Some(extensions)
|
|
} else {
|
|
None
|
|
}
|
|
},
|
|
};
|
|
// debug_now!("authData = {:?}", &authenticator_data);
|
|
|
|
let serialized_auth_data = authenticator_data.serialize()?;
|
|
|
|
// select attestation format or use packed attestation as default
|
|
let att_stmt_fmt = parameters
|
|
.attestation_formats_preference
|
|
.as_ref()
|
|
.map(SupportedAttestationFormat::select)
|
|
.unwrap_or(Some(SupportedAttestationFormat::Packed));
|
|
let att_stmt = if let Some(format) = att_stmt_fmt {
|
|
match format {
|
|
SupportedAttestationFormat::None => {
|
|
Some(AttestationStatement::None(NoneAttestationStatement {}))
|
|
}
|
|
SupportedAttestationFormat::Packed => {
|
|
let mut commitment = Bytes::<1024>::new();
|
|
commitment
|
|
.extend_from_slice(&serialized_auth_data)
|
|
.map_err(|_| Error::Other)?;
|
|
commitment
|
|
.extend_from_slice(parameters.client_data_hash)
|
|
.map_err(|_| Error::Other)?;
|
|
|
|
let (attestation_key, attestation_algorithm) = attestation_maybe
|
|
.as_ref()
|
|
.map(|attestation| (attestation.0, SigningAlgorithm::P256))
|
|
.unwrap_or((private_key, algorithm));
|
|
let signature =
|
|
attestation_algorithm.sign(&mut self.trussed, attestation_key, &commitment);
|
|
let packed = PackedAttestationStatement {
|
|
alg: attestation_algorithm.into(),
|
|
sig: signature.to_bytes().map_err(|_| Error::Other)?,
|
|
x5c: attestation_maybe.as_ref().map(|attestation| {
|
|
// See: https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements
|
|
let cert = attestation.1.clone();
|
|
let mut x5c = Vec::new();
|
|
x5c.push(cert).ok();
|
|
x5c
|
|
}),
|
|
};
|
|
Some(AttestationStatement::Packed(packed))
|
|
}
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if !rk_requested {
|
|
let _success = syscall!(self.trussed.delete(private_key)).success;
|
|
info_now!("deleted private credential key: {}", _success);
|
|
}
|
|
|
|
let mut attestation_object = ctap2::make_credential::ResponseBuilder {
|
|
fmt: att_stmt_fmt
|
|
.map(From::from)
|
|
.unwrap_or(AttestationStatementFormat::None),
|
|
auth_data: serialized_auth_data,
|
|
}
|
|
.build();
|
|
attestation_object.att_stmt = att_stmt;
|
|
attestation_object.large_blob_key = large_blob_key;
|
|
Ok(attestation_object)
|
|
}
|
|
|
|
#[inline(never)]
|
|
fn reset(&mut self) -> Result<()> {
|
|
// 1. >10s after bootup -> NotAllowed
|
|
let uptime = syscall!(self.trussed.uptime()).uptime;
|
|
debug_now!("uptime: {:?}", uptime);
|
|
if uptime.as_secs() > 10 {
|
|
#[cfg(not(feature = "disable-reset-time-window"))]
|
|
return Err(Error::NotAllowed);
|
|
}
|
|
// 2. check for user presence
|
|
// denied -> OperationDenied
|
|
// timeout -> UserActionTimeout
|
|
self.up
|
|
.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?;
|
|
|
|
// Delete resident keys
|
|
syscall!(self.trussed.delete_all(Location::Internal));
|
|
syscall!(self
|
|
.trussed
|
|
.remove_dir_all(Location::Internal, RK_DIR.into()));
|
|
|
|
// Delete large-blob array
|
|
large_blobs::reset(&mut self.trussed);
|
|
|
|
// b. delete persistent state
|
|
self.state.persistent.reset(&mut self.trussed)?;
|
|
|
|
// c. Reset runtime state
|
|
self.state.runtime.reset(&mut self.trussed);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn selection(&mut self) -> Result<()> {
|
|
self.up
|
|
.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)
|
|
}
|
|
|
|
#[inline(never)]
|
|
fn client_pin(
|
|
&mut self,
|
|
parameters: &ctap2::client_pin::Request<'_>,
|
|
) -> Result<ctap2::client_pin::Response> {
|
|
use ctap2::client_pin::PinV1Subcommand as Subcommand;
|
|
debug_now!("CTAP2.PIN...");
|
|
// info_now!("{:?}", parameters);
|
|
|
|
let pin_protocol = parameters
|
|
.pin_protocol
|
|
.ok_or(Error::MissingParameter)
|
|
.and_then(|pin_protocol| self.parse_pin_protocol(pin_protocol));
|
|
let mut response = ctap2::client_pin::Response::default();
|
|
|
|
match parameters.sub_command {
|
|
Subcommand::GetRetries => {
|
|
debug_now!("CTAP2.Pin.GetRetries");
|
|
|
|
response.retries = Some(self.state.persistent.retries());
|
|
}
|
|
|
|
Subcommand::GetKeyAgreement => {
|
|
debug_now!("CTAP2.Pin.GetKeyAgreement");
|
|
|
|
let pin_protocol = pin_protocol?;
|
|
response.key_agreement = Some(self.pin_protocol(pin_protocol).key_agreement_key());
|
|
}
|
|
|
|
Subcommand::SetPin => {
|
|
debug_now!("CTAP2.Pin.SetPin");
|
|
// 1. check mandatory parameters
|
|
let platform_kek = match parameters.key_agreement.as_ref() {
|
|
Some(key) => key,
|
|
None => {
|
|
return Err(Error::MissingParameter);
|
|
}
|
|
};
|
|
let new_pin_enc = match parameters.new_pin_enc.as_ref() {
|
|
Some(pin) => pin,
|
|
None => {
|
|
return Err(Error::MissingParameter);
|
|
}
|
|
};
|
|
let pin_auth = match parameters.pin_auth.as_ref() {
|
|
Some(auth) => auth,
|
|
None => {
|
|
return Err(Error::MissingParameter);
|
|
}
|
|
};
|
|
let pin_protocol = pin_protocol?;
|
|
|
|
// 2. is pin already set
|
|
if self.state.persistent.pin_is_set() {
|
|
return Err(Error::NotAllowed);
|
|
}
|
|
|
|
// 3. generate shared secret
|
|
let mut pin_protocol = self.pin_protocol(pin_protocol);
|
|
let shared_secret = pin_protocol.shared_secret(platform_kek)?;
|
|
|
|
// TODO: there are moar early returns!!
|
|
// - implement Drop?
|
|
// - do garbage collection outside of this?
|
|
|
|
// 4. verify pinAuth
|
|
pin_protocol.verify_pin_auth(&shared_secret, new_pin_enc, pin_auth)?;
|
|
|
|
// 5. decrypt and verify new PIN
|
|
let new_pin = self.decrypt_pin_check_length(&shared_secret, new_pin_enc)?;
|
|
|
|
shared_secret.delete(&mut self.trussed);
|
|
|
|
// 6. store LEFT(SHA-256(newPin), 16), set retries to 8
|
|
self.hash_store_pin(&new_pin)?;
|
|
self.state
|
|
.reset_retries(&mut self.trussed)
|
|
.map_err(|_| Error::Other)?;
|
|
}
|
|
|
|
Subcommand::ChangePin => {
|
|
debug_now!("CTAP2.Pin.ChangePin");
|
|
|
|
// 1. check mandatory parameters
|
|
let platform_kek = match parameters.key_agreement.as_ref() {
|
|
Some(key) => key,
|
|
None => {
|
|
return Err(Error::MissingParameter);
|
|
}
|
|
};
|
|
let pin_hash_enc = match parameters.pin_hash_enc.as_ref() {
|
|
Some(hash) => hash,
|
|
None => {
|
|
return Err(Error::MissingParameter);
|
|
}
|
|
};
|
|
let new_pin_enc = match parameters.new_pin_enc.as_ref() {
|
|
Some(pin) => pin,
|
|
None => {
|
|
return Err(Error::MissingParameter);
|
|
}
|
|
};
|
|
let pin_auth = match parameters.pin_auth.as_ref() {
|
|
Some(auth) => auth,
|
|
None => {
|
|
return Err(Error::MissingParameter);
|
|
}
|
|
};
|
|
let pin_protocol = pin_protocol?;
|
|
|
|
// 2. fail if no retries left
|
|
self.state.pin_blocked()?;
|
|
|
|
// 3. generate shared secret
|
|
let mut pin_protocol_impl = self.pin_protocol(pin_protocol);
|
|
let shared_secret = pin_protocol_impl.shared_secret(platform_kek)?;
|
|
|
|
// 4. verify pinAuth
|
|
let mut data = MediumData::new();
|
|
data.extend_from_slice(new_pin_enc)
|
|
.map_err(|_| Error::InvalidParameter)?;
|
|
data.extend_from_slice(pin_hash_enc)
|
|
.map_err(|_| Error::InvalidParameter)?;
|
|
pin_protocol_impl.verify_pin_auth(&shared_secret, &data, pin_auth)?;
|
|
|
|
// 5. decrement retries
|
|
self.state.decrement_retries(&mut self.trussed)?;
|
|
|
|
// 6. decrypt pinHashEnc, compare with stored
|
|
self.decrypt_pin_hash_and_maybe_escalate(
|
|
pin_protocol,
|
|
&shared_secret,
|
|
pin_hash_enc,
|
|
)?;
|
|
|
|
// 7. reset retries
|
|
self.state.reset_retries(&mut self.trussed)?;
|
|
|
|
// 8. decrypt and verify new PIN
|
|
let new_pin = self.decrypt_pin_check_length(&shared_secret, new_pin_enc)?;
|
|
|
|
shared_secret.delete(&mut self.trussed);
|
|
|
|
// 9. store hashed PIN
|
|
self.hash_store_pin(&new_pin)?;
|
|
|
|
self.pin_protocol(pin_protocol).reset_pin_tokens();
|
|
}
|
|
|
|
// § 6.5.5.7.1 No 4
|
|
Subcommand::GetPinToken => {
|
|
debug_now!("CTAP2.Pin.GetPinToken");
|
|
|
|
// 1. Check mandatory parameters
|
|
let key_agreement = parameters
|
|
.key_agreement
|
|
.as_ref()
|
|
.ok_or(Error::MissingParameter)?;
|
|
let pin_hash_enc = parameters
|
|
.pin_hash_enc
|
|
.as_ref()
|
|
.ok_or(Error::MissingParameter)?;
|
|
|
|
// 2. Check PIN protocol
|
|
let pin_protocol = pin_protocol?;
|
|
|
|
// 3. + 4. Check invalid parameters
|
|
if parameters.permissions.is_some() || parameters.rp_id.is_some() {
|
|
return Err(Error::InvalidParameter);
|
|
}
|
|
|
|
// 5. Check PIN retries
|
|
self.state.pin_blocked()?;
|
|
|
|
// 6. Obtain shared secret
|
|
let shared_secret = self
|
|
.pin_protocol(pin_protocol)
|
|
.shared_secret(key_agreement)?;
|
|
|
|
// 7. Request user consent using display -- skipped
|
|
|
|
// 8. Decrement PIN retries
|
|
self.state.decrement_retries(&mut self.trussed)?;
|
|
|
|
// 9. Check PIN
|
|
self.decrypt_pin_hash_and_maybe_escalate(
|
|
pin_protocol,
|
|
&shared_secret,
|
|
pin_hash_enc,
|
|
)?;
|
|
|
|
// 10. Reset PIN retries
|
|
self.state.reset_retries(&mut self.trussed)?;
|
|
|
|
// 11. Check forcePINChange -- skipped
|
|
|
|
// 12. Reset all PIN tokens
|
|
// 13. Call beginUsingPinUvAuthToken
|
|
let mut pin_protocol = self.pin_protocol(pin_protocol);
|
|
let mut pin_token = pin_protocol.reset_and_begin_using_pin_token(false);
|
|
|
|
// 14. Assign the default permissions
|
|
let mut permissions = Permissions::empty();
|
|
permissions.insert(Permissions::MAKE_CREDENTIAL);
|
|
permissions.insert(Permissions::GET_ASSERTION);
|
|
pin_token.restrict(permissions, None);
|
|
|
|
// 15. Return PIN token
|
|
response.pin_token = Some(pin_token.encrypt(&shared_secret)?);
|
|
|
|
shared_secret.delete(&mut self.trussed);
|
|
}
|
|
|
|
// § 6.5.5.7.2 No 4
|
|
Subcommand::GetPinUvAuthTokenUsingPinWithPermissions => {
|
|
debug_now!("CTAP2.Pin.GetPinUvAuthTokenUsingPinWithPermissions");
|
|
|
|
// 1. Check mandatory parameters
|
|
let key_agreement = parameters
|
|
.key_agreement
|
|
.as_ref()
|
|
.ok_or(Error::MissingParameter)?;
|
|
let pin_hash_enc = parameters
|
|
.pin_hash_enc
|
|
.as_ref()
|
|
.ok_or(Error::MissingParameter)?;
|
|
let permissions = parameters.permissions.ok_or(Error::MissingParameter)?;
|
|
|
|
// 2. Check PIN protocol
|
|
let pin_protocol = pin_protocol?;
|
|
|
|
// 3. Check that permissions are not empty
|
|
let permissions = Permissions::from_bits_truncate(permissions);
|
|
if permissions.is_empty() {
|
|
return Err(Error::InvalidParameter);
|
|
}
|
|
|
|
// 4. Check that all requested permissions are supported
|
|
let mut unauthorized_permissions = Permissions::empty();
|
|
unauthorized_permissions.insert(Permissions::BIO_ENROLLMENT);
|
|
if !self.config.supports_large_blobs() {
|
|
unauthorized_permissions.insert(Permissions::LARGE_BLOB_WRITE);
|
|
}
|
|
unauthorized_permissions.insert(Permissions::AUTHENTICATOR_CONFIGURATION);
|
|
if permissions.intersects(unauthorized_permissions) {
|
|
return Err(Error::UnauthorizedPermission);
|
|
}
|
|
|
|
// 5. Check PIN retries
|
|
self.state.pin_blocked()?;
|
|
|
|
// 6. Obtain shared secret
|
|
let shared_secret = self
|
|
.pin_protocol(pin_protocol)
|
|
.shared_secret(key_agreement)?;
|
|
|
|
// 7. Request user consent using display -- skipped
|
|
|
|
// 8. Decrement PIN retries
|
|
self.state.decrement_retries(&mut self.trussed)?;
|
|
|
|
// 9. Check PIN
|
|
self.decrypt_pin_hash_and_maybe_escalate(
|
|
pin_protocol,
|
|
&shared_secret,
|
|
pin_hash_enc,
|
|
)?;
|
|
|
|
// 10. Reset PIN retries
|
|
self.state.reset_retries(&mut self.trussed)?;
|
|
|
|
// 11. Check forcePINChange -- skipped
|
|
|
|
// 12. Reset all PIN tokens
|
|
// 13. Call beginUsingPinUvAuthToken
|
|
let mut pin_protocol = self.pin_protocol(pin_protocol);
|
|
let mut pin_token = pin_protocol.reset_and_begin_using_pin_token(false);
|
|
|
|
// 14. Assign the requested permissions
|
|
// 15. Assign the requested RP id
|
|
let rp_id = parameters
|
|
.rp_id
|
|
.map(TryInto::try_into)
|
|
.transpose()
|
|
.map_err(|_| Error::InvalidParameter)?;
|
|
pin_token.restrict(permissions, rp_id);
|
|
|
|
// 16. Return PIN token
|
|
response.pin_token = Some(pin_token.encrypt(&shared_secret)?);
|
|
|
|
shared_secret.delete(&mut self.trussed);
|
|
}
|
|
|
|
Subcommand::GetPinUvAuthTokenUsingUvWithPermissions | Subcommand::GetUVRetries => {
|
|
// todo!("not implemented yet")
|
|
return Err(Error::InvalidParameter);
|
|
}
|
|
|
|
_ => {
|
|
return Err(Error::InvalidParameter);
|
|
}
|
|
}
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
#[inline(never)]
|
|
fn credential_management(
|
|
&mut self,
|
|
parameters: &ctap2::credential_management::Request<'_>,
|
|
) -> Result<ctap2::credential_management::Response> {
|
|
use credential_management as cm;
|
|
use ctap2::credential_management::Subcommand;
|
|
|
|
self.verify_credential_management_pin_auth(parameters)?;
|
|
|
|
let mut cred_mgmt = cm::CredentialManagement::new(self);
|
|
let sub_parameters = ¶meters.sub_command_params;
|
|
// TODO: use custom enum of known commands
|
|
match parameters.sub_command {
|
|
// 0x1
|
|
Subcommand::GetCredsMetadata => cred_mgmt.get_creds_metadata(),
|
|
|
|
// 0x2
|
|
Subcommand::EnumerateRpsBegin => cred_mgmt.first_relying_party(),
|
|
|
|
// 0x3
|
|
Subcommand::EnumerateRpsGetNextRp => cred_mgmt.next_relying_party(),
|
|
|
|
// 0x4
|
|
Subcommand::EnumerateCredentialsBegin => {
|
|
let sub_parameters = sub_parameters.as_ref().ok_or(Error::MissingParameter)?;
|
|
|
|
cred_mgmt.first_credential(
|
|
sub_parameters
|
|
.rp_id_hash
|
|
.as_ref()
|
|
.ok_or(Error::MissingParameter)?,
|
|
)
|
|
}
|
|
|
|
// 0x5
|
|
Subcommand::EnumerateCredentialsGetNextCredential => cred_mgmt.next_credential(),
|
|
|
|
// 0x6
|
|
Subcommand::DeleteCredential => {
|
|
let sub_parameters = sub_parameters.as_ref().ok_or(Error::MissingParameter)?;
|
|
|
|
cred_mgmt.delete_credential(
|
|
sub_parameters
|
|
.credential_id
|
|
.as_ref()
|
|
.ok_or(Error::MissingParameter)?,
|
|
)
|
|
}
|
|
|
|
// 0x7
|
|
Subcommand::UpdateUserInformation => {
|
|
let sub_parameters = sub_parameters.as_ref().ok_or(Error::MissingParameter)?;
|
|
let credential_id = sub_parameters
|
|
.credential_id
|
|
.as_ref()
|
|
.ok_or(Error::MissingParameter)?;
|
|
let user = sub_parameters
|
|
.user
|
|
.as_ref()
|
|
.ok_or(Error::MissingParameter)?;
|
|
|
|
cred_mgmt.update_user_information(credential_id, user)
|
|
}
|
|
|
|
_ => Err(Error::InvalidParameter),
|
|
}
|
|
}
|
|
|
|
#[inline(never)]
|
|
fn vendor(&mut self, op: VendorOperation) -> Result<()> {
|
|
info_now!("hello VO {:?}", &op);
|
|
match op.into() {
|
|
0x79 => syscall!(self.trussed.debug_dump_store()),
|
|
_ => return Err(Error::InvalidCommand),
|
|
};
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[inline(never)]
|
|
fn get_assertion(
|
|
&mut self,
|
|
parameters: &ctap2::get_assertion::Request,
|
|
) -> Result<ctap2::get_assertion::Response> {
|
|
debug_now!("remaining stack size: {} bytes", msp() - 0x2000_0000);
|
|
|
|
let rp_id_hash = self.hash(parameters.rp_id.as_ref());
|
|
|
|
// 1-4.
|
|
let uv_performed = match self.pin_prechecks(
|
|
¶meters.options,
|
|
parameters.pin_auth.map(AsRef::as_ref),
|
|
parameters.pin_protocol,
|
|
parameters.client_data_hash.as_ref(),
|
|
Permissions::GET_ASSERTION,
|
|
parameters.rp_id,
|
|
) {
|
|
Ok(b) => b,
|
|
Err(Error::PinRequired) => {
|
|
// UV is optional for get_assertion
|
|
false
|
|
}
|
|
Err(err) => return Err(err),
|
|
};
|
|
|
|
// 5. Locate eligible credentials
|
|
//
|
|
// Note: If allowList is passed, credential is Some(credential)
|
|
// If no allowList is passed, credential is None and the retrieved credentials
|
|
// are stored in state.runtime.credential_heap
|
|
let (credential, num_credentials) = self
|
|
.prepare_credentials(&rp_id_hash, ¶meters.allow_list, uv_performed)?
|
|
.ok_or(Error::NoCredentials)?;
|
|
|
|
info_now!("found {:?} applicable credentials", num_credentials);
|
|
info_now!("{:?}", &credential);
|
|
|
|
// 6. process any options present
|
|
|
|
// RK is not supported in get_assertion
|
|
if parameters
|
|
.options
|
|
.as_ref()
|
|
.and_then(|options| options.rk)
|
|
.is_some()
|
|
{
|
|
return Err(Error::InvalidOption);
|
|
}
|
|
|
|
// UP occurs by default, but option could specify not to.
|
|
let do_up = if let Some(options) = parameters.options.as_ref() {
|
|
options.up.unwrap_or(true)
|
|
} else {
|
|
true
|
|
};
|
|
|
|
// 7. collect user presence
|
|
let up_performed = if do_up {
|
|
if !self.skip_up_check() {
|
|
info_now!("asking for up");
|
|
self.up
|
|
.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?;
|
|
}
|
|
true
|
|
} else {
|
|
info_now!("not asking for up");
|
|
false
|
|
};
|
|
|
|
let multiple_credentials = num_credentials > 1;
|
|
self.state.runtime.active_get_assertion = Some(state::ActiveGetAssertionData {
|
|
rp_id_hash: {
|
|
let mut buf = [0u8; 32];
|
|
buf.copy_from_slice(&rp_id_hash);
|
|
buf
|
|
},
|
|
client_data_hash: {
|
|
let mut buf = [0u8; 32];
|
|
buf.copy_from_slice(parameters.client_data_hash);
|
|
buf
|
|
},
|
|
uv_performed,
|
|
up_performed,
|
|
multiple_credentials,
|
|
extensions: parameters.extensions.clone(),
|
|
attestation_formats_preference: parameters.attestation_formats_preference.clone(),
|
|
});
|
|
|
|
let num_credentials = match num_credentials {
|
|
1 => None,
|
|
n => Some(n),
|
|
};
|
|
|
|
self.assert_with_credential(num_credentials, credential)
|
|
}
|
|
|
|
#[inline(never)]
|
|
fn large_blobs(
|
|
&mut self,
|
|
request: &ctap2::large_blobs::Request,
|
|
) -> Result<ctap2::large_blobs::Response> {
|
|
let Some(config) = self.config.large_blobs else {
|
|
return Err(Error::InvalidCommand);
|
|
};
|
|
|
|
// 1. offset is validated by serde
|
|
|
|
// 2.-3. Exactly one of get or set must be present
|
|
match (request.get, request.set) {
|
|
(None, None) | (Some(_), Some(_)) => Err(Error::InvalidParameter),
|
|
// 4. Implement get subcommand
|
|
(Some(get), None) => self.large_blobs_get(request, config, get),
|
|
// 5. Implement set subcommand
|
|
(None, Some(set)) => self.large_blobs_set(request, config, set),
|
|
}
|
|
}
|
|
}
|
|
|
|
// impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenticator<UP, T>
|
|
impl<UP: UserPresence, T: TrussedRequirements> crate::Authenticator<UP, T> {
|
|
fn parse_pin_protocol(&self, version: impl TryInto<u8>) -> Result<PinProtocolVersion> {
|
|
if let Ok(version) = version.try_into() {
|
|
for pin_protocol in self.pin_protocols() {
|
|
if u8::from(*pin_protocol) == version {
|
|
return Ok(*pin_protocol);
|
|
}
|
|
}
|
|
}
|
|
Err(Error::InvalidParameter)
|
|
}
|
|
|
|
// This is the single source of truth for the supported PIN protocols.
|
|
fn pin_protocols(&self) -> &'static [PinProtocolVersion] {
|
|
&[PinProtocolVersion::V2, PinProtocolVersion::V1]
|
|
}
|
|
|
|
fn pin_protocol(&mut self, pin_protocol: PinProtocolVersion) -> PinProtocol<'_, T> {
|
|
let state = self.state.runtime.pin_protocol(&mut self.trussed);
|
|
PinProtocol::new(&mut self.trussed, state, pin_protocol)
|
|
}
|
|
|
|
#[inline(never)]
|
|
fn check_credential_applicable(
|
|
&mut self,
|
|
credential: &Credential,
|
|
allowlist_passed: bool,
|
|
uv_performed: bool,
|
|
) -> bool {
|
|
if !self.check_key_exists(credential.algorithm(), credential.key()) {
|
|
return false;
|
|
}
|
|
|
|
if !{
|
|
use credential::CredentialProtectionPolicy as Policy;
|
|
debug_now!("CredentialProtectionPolicy {:?}", credential.cred_protect());
|
|
match credential.cred_protect() {
|
|
None | Some(Policy::Optional) => true,
|
|
Some(Policy::OptionalWithCredentialIdList) => allowlist_passed || uv_performed,
|
|
Some(Policy::Required) => uv_performed,
|
|
}
|
|
} {
|
|
return false;
|
|
}
|
|
true
|
|
}
|
|
|
|
#[inline(never)]
|
|
fn prepare_credentials(
|
|
&mut self,
|
|
rp_id_hash: &[u8; 32],
|
|
allow_list: &Option<ctap2::get_assertion::AllowList>,
|
|
uv_performed: bool,
|
|
) -> Result<Option<(Credential, u32)>> {
|
|
debug_now!("remaining stack size: {} bytes", msp() - 0x2000_0000);
|
|
|
|
self.state.runtime.clear_credential_cache();
|
|
self.state.runtime.active_get_assertion = None;
|
|
|
|
// NB: CTAP 2.1 specifies to return the first applicable credential, and set
|
|
// numberOfCredentials to None.
|
|
// However, CTAP 2.0 says to send numberOfCredentials that are applicable,
|
|
// which implies we'd have to respond to GetNextAssertion.
|
|
//
|
|
// We are using CTAP 2.1 behaviour here, as it allows us not to cache the (length)
|
|
// credential IDs. Presumably, most clients use this to just get any old signatures,
|
|
// but we did change the github.com/solokeys/fido2-tests to accommodate this change
|
|
// of behaviour.
|
|
if let Some(allow_list) = allow_list {
|
|
debug_now!("Allowlist of len {} passed, filtering", allow_list.len());
|
|
// we will have at most one credential, and an empty cache.
|
|
|
|
// client is not supposed to send Some(empty list):
|
|
// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#:~:text=A%20platform%20MUST%20NOT%20send%20an%20empty%20allowList%E2%80%94if%20it%20would%20be%20empty%20it%20MUST%20be%20omitted>
|
|
// but some still do (and CTAP 2.0 does not rule it out).
|
|
// they probably meant to send None.
|
|
if !allow_list.is_empty() {
|
|
for credential_id in allow_list {
|
|
let credential = match Credential::try_from(self, rp_id_hash, credential_id) {
|
|
Ok(credential) => credential,
|
|
_ => continue,
|
|
};
|
|
|
|
if !self.check_credential_applicable(&credential, true, uv_performed) {
|
|
continue;
|
|
}
|
|
|
|
return Ok(Some((credential, 1)));
|
|
}
|
|
|
|
// we don't recognize any credentials in the allowlist
|
|
return Ok(None);
|
|
}
|
|
}
|
|
|
|
// we are only dealing with discoverable credentials.
|
|
debug_now!("Allowlist not passed, fetching RKs");
|
|
self.prepare_cache(rp_id_hash, uv_performed)?;
|
|
|
|
let num_credentials = self.state.runtime.remaining_credentials();
|
|
let credential = self.state.runtime.pop_credential(&mut self.trussed);
|
|
Ok(credential.map(|credential| (Credential::Full(credential), num_credentials)))
|
|
}
|
|
|
|
/// Populate the cache with the RP credentials.
|
|
#[inline(never)]
|
|
fn prepare_cache(&mut self, rp_id_hash: &[u8; 32], uv_performed: bool) -> Result<()> {
|
|
use crate::state::CachedCredential;
|
|
use core::str::FromStr;
|
|
|
|
let file_name_prefix = rp_file_name_prefix(rp_id_hash);
|
|
let mut maybe_entry = syscall!(self.trussed.read_dir_first_alphabetical(
|
|
Location::Internal,
|
|
PathBuf::from(RK_DIR),
|
|
Some(file_name_prefix.clone())
|
|
))
|
|
.entry;
|
|
|
|
while let Some(entry) = maybe_entry.take() {
|
|
if !entry
|
|
.file_name()
|
|
.as_ref()
|
|
.starts_with(file_name_prefix.as_ref())
|
|
{
|
|
// We got past all credentials for the relevant RP
|
|
break;
|
|
}
|
|
|
|
if entry.file_name() == &*file_name_prefix {
|
|
debug_assert!(entry.metadata().is_dir());
|
|
error!("Migration missing");
|
|
return Err(Error::Other);
|
|
}
|
|
|
|
let credential_data = syscall!(self
|
|
.trussed
|
|
.read_file(Location::Internal, entry.path().into(),))
|
|
.data;
|
|
|
|
let credential = FullCredential::deserialize(&credential_data).map_err(|_err| {
|
|
error!("Failed to deserialize credential: {_err:?}");
|
|
Error::Other
|
|
})?;
|
|
let timestamp = credential.creation_time;
|
|
let credential = Credential::Full(credential);
|
|
|
|
if self.check_credential_applicable(&credential, false, uv_performed) {
|
|
self.state.runtime.push_credential(CachedCredential {
|
|
timestamp,
|
|
path: String::from_str(entry.path().as_str_ref_with_trailing_nul())
|
|
.map_err(|_| Error::Other)?,
|
|
});
|
|
}
|
|
|
|
maybe_entry = syscall!(self.trussed.read_dir_next()).entry;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn decrypt_pin_hash_and_maybe_escalate(
|
|
&mut self,
|
|
pin_protocol: PinProtocolVersion,
|
|
shared_secret: &SharedSecret,
|
|
pin_hash_enc: &[u8],
|
|
) -> Result<()> {
|
|
let pin_hash = shared_secret
|
|
.decrypt(&mut self.trussed, pin_hash_enc)
|
|
.ok_or(Error::Other)?;
|
|
|
|
let stored_pin_hash = match self.state.persistent.pin_hash() {
|
|
Some(hash) => hash,
|
|
None => {
|
|
return Err(Error::PinNotSet);
|
|
}
|
|
};
|
|
|
|
if pin_hash != stored_pin_hash {
|
|
// I) generate new KEK
|
|
self.pin_protocol(pin_protocol).regenerate();
|
|
self.state.pin_blocked()?;
|
|
return Err(Error::PinInvalid);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn hash_store_pin(&mut self, pin: &Message) -> Result<()> {
|
|
let pin_hash_32 = syscall!(self.trussed.hash_sha256(pin)).hash;
|
|
let pin_hash: [u8; 16] = pin_hash_32[..16].try_into().unwrap();
|
|
self.state
|
|
.persistent
|
|
.set_pin_hash(&mut self.trussed, pin_hash)
|
|
.unwrap();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn decrypt_pin_check_length(
|
|
&mut self,
|
|
shared_secret: &SharedSecret,
|
|
pin_enc: &[u8],
|
|
) -> Result<Message> {
|
|
// pin is expected to be filled with null bytes to length at least 64
|
|
if pin_enc.len() < 64 {
|
|
// correct error?
|
|
return Err(Error::PinPolicyViolation);
|
|
}
|
|
|
|
let mut pin = shared_secret
|
|
.decrypt(&mut self.trussed, pin_enc)
|
|
.ok_or(Error::Other)?;
|
|
|
|
// // temp
|
|
// let pin_length = pin.iter().position(|&b| b == b'\0').unwrap_or(pin.len());
|
|
// info_now!("pin.len() = {}, pin_length = {}, = {:?}",
|
|
// pin.len(), pin_length, &pin);
|
|
// chop off null bytes
|
|
let pin_length = pin.iter().position(|&b| b == b'\0').unwrap_or(pin.len());
|
|
if !(4..64).contains(&pin_length) {
|
|
return Err(Error::PinPolicyViolation);
|
|
}
|
|
|
|
pin.resize_default(pin_length).unwrap();
|
|
|
|
Ok(pin)
|
|
}
|
|
|
|
fn verify_credential_management_pin_auth(
|
|
&mut self,
|
|
parameters: &ctap2::credential_management::Request,
|
|
) -> Result<()> {
|
|
use ctap2::credential_management::Subcommand;
|
|
let rp_scope = match parameters.sub_command {
|
|
Subcommand::EnumerateCredentialsBegin => {
|
|
let rp_id_hash = parameters
|
|
.sub_command_params
|
|
.as_ref()
|
|
.and_then(|subparams| subparams.rp_id_hash)
|
|
.ok_or(Error::MissingParameter)?;
|
|
RpScope::RpIdHash(rp_id_hash)
|
|
}
|
|
Subcommand::DeleteCredential | Subcommand::UpdateUserInformation => {
|
|
// TODO: determine RP ID from credential ID
|
|
RpScope::All
|
|
}
|
|
_ => RpScope::All,
|
|
};
|
|
match parameters.sub_command {
|
|
Subcommand::GetCredsMetadata
|
|
| Subcommand::EnumerateRpsBegin
|
|
| Subcommand::EnumerateCredentialsBegin
|
|
| Subcommand::DeleteCredential
|
|
| Subcommand::UpdateUserInformation => {
|
|
// check pinProtocol
|
|
let pin_protocol = parameters.pin_protocol.ok_or(Error::MissingParameter)?;
|
|
let pin_protocol = self.parse_pin_protocol(pin_protocol)?;
|
|
|
|
// check pinAuth
|
|
let mut data: Bytes<{ sizes::MAX_CREDENTIAL_ID_LENGTH_PLUS_256 }> =
|
|
Bytes::from_slice(&[parameters.sub_command as u8]).unwrap();
|
|
let len = 1 + match parameters.sub_command {
|
|
Subcommand::EnumerateCredentialsBegin
|
|
| Subcommand::DeleteCredential
|
|
| Subcommand::UpdateUserInformation => {
|
|
data.resize_to_capacity();
|
|
// ble, need to reserialize
|
|
ctap_types::serde::cbor_serialize(
|
|
¶meters
|
|
.sub_command_params
|
|
.as_ref()
|
|
.ok_or(Error::MissingParameter)?,
|
|
&mut data[1..],
|
|
)
|
|
.map_err(|_| Error::LimitExceeded)?
|
|
.len()
|
|
}
|
|
_ => 0,
|
|
};
|
|
|
|
let pin_auth = parameters
|
|
.pin_auth
|
|
.as_ref()
|
|
.ok_or(Error::MissingParameter)?;
|
|
|
|
let mut pin_protocol = self.pin_protocol(pin_protocol);
|
|
if let Ok(pin_token) = pin_protocol.verify_pin_token(&data[..len], pin_auth) {
|
|
info_now!("passed pinauth");
|
|
pin_token.require_permissions(Permissions::CREDENTIAL_MANAGEMENT)?;
|
|
pin_token.require_valid_for_rp(rp_scope)?;
|
|
Ok(())
|
|
} else {
|
|
info_now!("failed pinauth!");
|
|
self.state.decrement_retries(&mut self.trussed)?;
|
|
let maybe_blocked = self.state.pin_blocked();
|
|
if maybe_blocked.is_err() {
|
|
info_now!("blocked");
|
|
maybe_blocked
|
|
} else {
|
|
info_now!("pinAuthInvalid");
|
|
Err(Error::PinAuthInvalid)
|
|
}
|
|
}
|
|
}
|
|
|
|
// don't need the PIN auth, they're continuations
|
|
// of already checked CredMgmt subcommands
|
|
Subcommand::EnumerateRpsGetNextRp
|
|
| Subcommand::EnumerateCredentialsGetNextCredential => Ok(()),
|
|
|
|
_ => Err(Error::InvalidParameter),
|
|
}
|
|
}
|
|
|
|
/// Returns whether UV was performed.
|
|
fn pin_prechecks(
|
|
&mut self,
|
|
options: &Option<ctap2::AuthenticatorOptions>,
|
|
pin_auth: Option<&[u8]>,
|
|
pin_protocol: Option<u32>,
|
|
data: &[u8],
|
|
permissions: Permissions,
|
|
rp_id: &str,
|
|
) -> Result<bool> {
|
|
// 1. pinAuth zero length -> wait for user touch, then
|
|
// return PinNotSet if not set, PinInvalid if set
|
|
//
|
|
// the idea is for multi-authnr scenario where platform
|
|
// wants to enforce PIN and needs to figure out which authnrs support PIN
|
|
if let Some(pin_auth) = pin_auth {
|
|
if pin_auth.is_empty() {
|
|
self.up
|
|
.user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?;
|
|
if !self.state.persistent.pin_is_set() {
|
|
return Err(Error::PinNotSet);
|
|
} else {
|
|
return Err(Error::PinAuthInvalid);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. check PIN protocol is 1 if pinAuth was sent
|
|
let pin_protocol = if pin_auth.is_some() {
|
|
let pin_protocol = pin_protocol.ok_or(Error::MissingParameter)?;
|
|
let pin_protocol = self.parse_pin_protocol(pin_protocol)?;
|
|
Some(pin_protocol)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// 3. if no PIN is set (we have no other form of UV),
|
|
// and platform sent `uv` or `pinAuth`, return InvalidOption
|
|
if !self.state.persistent.pin_is_set() {
|
|
if let Some(ref options) = &options {
|
|
if Some(true) == options.uv {
|
|
return Err(Error::InvalidOption);
|
|
}
|
|
}
|
|
if pin_auth.is_some() {
|
|
return Err(Error::InvalidOption);
|
|
}
|
|
}
|
|
|
|
// 4. If authenticator is protected by som form of user verification, do it
|
|
|
|
// Reject uv = true as we do not support built-in user verification
|
|
if pin_auth.is_none() && options.as_ref().and_then(|options| options.uv) == Some(true) {
|
|
return Err(Error::InvalidOption);
|
|
}
|
|
|
|
if self.state.persistent.pin_is_set() {
|
|
// let mut uv_performed = false;
|
|
if let Some(pin_auth) = pin_auth {
|
|
// seems a bit redundant to check here in light of 2.
|
|
// I guess the CTAP spec writers aren't implementers :D
|
|
if let Some(pin_protocol) = pin_protocol {
|
|
// 5. if pinAuth is present and pinProtocol = 1, verify
|
|
// success --> set uv = 1
|
|
// error --> PinAuthInvalid
|
|
let mut pin_protocol = self.pin_protocol(pin_protocol);
|
|
let pin_token = pin_protocol.verify_pin_token(data, pin_auth)?;
|
|
pin_token.require_permissions(permissions)?;
|
|
pin_token.require_valid_for_rp(RpScope::RpId(rp_id))?;
|
|
|
|
return Ok(true);
|
|
} else {
|
|
// 7. pinAuth present + pinProtocol != 1 --> error PinAuthInvalid
|
|
return Err(Error::PinAuthInvalid);
|
|
}
|
|
} else {
|
|
// 6. pinAuth not present + clientPin set + rk = true --> error PinRequired
|
|
if options.as_ref().and_then(|options| options.rk) == Some(true) {
|
|
return Err(Error::PinRequired);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
#[inline(never)]
|
|
fn check_key_exists(&mut self, alg: i32, key: &Key) -> bool {
|
|
match key {
|
|
// TODO: should check if wrapped key is valid AEAD
|
|
// On the other hand, we already decrypted a valid AEAD
|
|
Key::WrappedKey(_) => true,
|
|
Key::ResidentKey(key) => {
|
|
debug_now!("checking if ResidentKey {:?} exists", key);
|
|
SigningAlgorithm::try_from(alg)
|
|
.map(|alg| syscall!(self.trussed.exists(alg.mechanism(), *key)).exists)
|
|
.unwrap_or_default()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline(never)]
|
|
fn process_assertion_extensions(
|
|
&mut self,
|
|
get_assertion_state: &state::ActiveGetAssertionData,
|
|
extensions: &ctap2::get_assertion::ExtensionsInput,
|
|
credential: &Credential,
|
|
credential_key: KeyId,
|
|
) -> Result<Option<ctap2::get_assertion::ExtensionsOutput>> {
|
|
let mut output = ctap2::get_assertion::ExtensionsOutput::default();
|
|
|
|
if let Some(hmac_secret) = &extensions.hmac_secret {
|
|
let pin_protocol = hmac_secret
|
|
.pin_protocol
|
|
.map(|i| self.parse_pin_protocol(i))
|
|
.transpose()?
|
|
.unwrap_or(PinProtocolVersion::V1);
|
|
|
|
if !get_assertion_state.up_performed {
|
|
return Err(Error::UnsupportedOption);
|
|
}
|
|
|
|
// We derive credRandom as an hmac of the existing private key.
|
|
// UV is used as input data since credRandom should depend UV
|
|
// i.e. credRandom = HMAC(private_key, uv)
|
|
let cred_random = syscall!(self.trussed.derive_key(
|
|
Mechanism::HmacSha256,
|
|
credential_key,
|
|
Some(Bytes::from_slice(&[get_assertion_state.uv_performed as u8]).unwrap()),
|
|
StorageAttributes::new().set_persistence(Location::Volatile)
|
|
))
|
|
.key;
|
|
|
|
// Verify the auth tag, which uses the same process as the pinAuth
|
|
let mut pin_protocol = self.pin_protocol(pin_protocol);
|
|
let shared_secret = pin_protocol.shared_secret(&hmac_secret.key_agreement)?;
|
|
pin_protocol.verify_pin_auth(
|
|
&shared_secret,
|
|
&hmac_secret.salt_enc,
|
|
&hmac_secret.salt_auth,
|
|
)?;
|
|
|
|
// decrypt input salt_enc to get salt1 or (salt1 || salt2)
|
|
let salts = shared_secret
|
|
.decrypt(&mut self.trussed, &hmac_secret.salt_enc)
|
|
.ok_or(Error::InvalidOption)?;
|
|
|
|
if salts.len() != 32 && salts.len() != 64 {
|
|
debug_now!("invalid hmac-secret length");
|
|
return Err(Error::InvalidLength);
|
|
}
|
|
|
|
let mut salt_output: Bytes<64> = Bytes::new();
|
|
|
|
// output1 = hmac_sha256(credRandom, salt1)
|
|
let output1 =
|
|
syscall!(self.trussed.sign_hmacsha256(cred_random, &salts[0..32])).signature;
|
|
|
|
salt_output.extend_from_slice(&output1).unwrap();
|
|
|
|
if salts.len() == 64 {
|
|
// output2 = hmac_sha256(credRandom, salt2)
|
|
let output2 =
|
|
syscall!(self.trussed.sign_hmacsha256(cred_random, &salts[32..64])).signature;
|
|
|
|
salt_output.extend_from_slice(&output2).unwrap();
|
|
}
|
|
|
|
syscall!(self.trussed.delete(cred_random));
|
|
|
|
// output_enc = aes256-cbc(sharedSecret, IV=0, output1 || output2)
|
|
let output_enc = shared_secret.encrypt(&mut self.trussed, &salt_output);
|
|
|
|
shared_secret.delete(&mut self.trussed);
|
|
|
|
output.hmac_secret = Some(Bytes::from_slice(&output_enc).unwrap());
|
|
}
|
|
|
|
if extensions.third_party_payment.unwrap_or_default() {
|
|
output.third_party_payment = Some(credential.third_party_payment().unwrap_or_default());
|
|
}
|
|
|
|
Ok(output.is_set().then_some(output))
|
|
}
|
|
|
|
#[inline(never)]
|
|
fn assert_with_credential(
|
|
&mut self,
|
|
num_credentials: Option<u32>,
|
|
credential: Credential,
|
|
) -> Result<ctap2::get_assertion::Response> {
|
|
let data = self.state.runtime.active_get_assertion.clone().unwrap();
|
|
let rp_id_hash = &data.rp_id_hash;
|
|
|
|
let (key, is_rk) = match credential.key().clone() {
|
|
Key::ResidentKey(key) => (key, true),
|
|
Key::WrappedKey(bytes) => {
|
|
let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?;
|
|
// info_now!("unwrapping {:?} with wrapping key {:?}", &bytes, &wrapping_key);
|
|
let key_result = syscall!(self.trussed.unwrap_key_chacha8poly1305(
|
|
wrapping_key,
|
|
&bytes,
|
|
&[],
|
|
Location::Volatile,
|
|
))
|
|
.key;
|
|
// debug_now!("key result: {:?}", &key_result);
|
|
info_now!("key result");
|
|
match key_result {
|
|
Some(key) => (key, false),
|
|
None => {
|
|
return Err(Error::Other);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// 8. process any extensions present
|
|
let mut large_blob_key_requested = false;
|
|
let extensions_output = if let Some(extensions) = &data.extensions {
|
|
if self.config.supports_large_blobs() {
|
|
if extensions.large_blob_key == Some(false) {
|
|
// large_blob_key must be Some(true) or omitted
|
|
return Err(Error::InvalidOption);
|
|
}
|
|
large_blob_key_requested = extensions.large_blob_key == Some(true);
|
|
}
|
|
self.process_assertion_extensions(&data, extensions, &credential, key)?
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// 9./10. sign clientDataHash || authData with "first" credential
|
|
|
|
// info_now!("signing with credential {:?}", &credential);
|
|
let kek = self
|
|
.state
|
|
.persistent
|
|
.key_encryption_key(&mut self.trussed)?;
|
|
let credential_id = credential.id(&mut self.trussed, kek, rp_id_hash)?;
|
|
|
|
use ctap2::AuthenticatorDataFlags as Flags;
|
|
|
|
let sig_count = self.state.persistent.timestamp(&mut self.trussed)?;
|
|
|
|
let authenticator_data = ctap2::get_assertion::AuthenticatorData {
|
|
rp_id_hash,
|
|
|
|
flags: {
|
|
let mut flags = Flags::empty();
|
|
if data.up_performed {
|
|
flags |= Flags::USER_PRESENCE;
|
|
}
|
|
if data.uv_performed {
|
|
flags |= Flags::USER_VERIFIED;
|
|
}
|
|
if extensions_output.is_some() {
|
|
flags |= Flags::EXTENSION_DATA;
|
|
}
|
|
flags
|
|
},
|
|
|
|
sign_count: sig_count,
|
|
attested_credential_data: None,
|
|
extensions: extensions_output,
|
|
};
|
|
|
|
let serialized_auth_data = authenticator_data.serialize()?;
|
|
|
|
let mut commitment = Bytes::<1024>::new();
|
|
commitment
|
|
.extend_from_slice(&serialized_auth_data)
|
|
.map_err(|_| Error::Other)?;
|
|
commitment
|
|
.extend_from_slice(&data.client_data_hash)
|
|
.map_err(|_| Error::Other)?;
|
|
|
|
let signing_algorithm =
|
|
SigningAlgorithm::try_from(credential.algorithm()).map_err(|_| Error::Other)?;
|
|
let signature = signing_algorithm
|
|
.sign(&mut self.trussed, key, &commitment)
|
|
.to_bytes()
|
|
.unwrap();
|
|
|
|
// select preferred format or skip attestation statement
|
|
let att_stmt_fmt = data
|
|
.attestation_formats_preference
|
|
.as_ref()
|
|
.and_then(SupportedAttestationFormat::select);
|
|
let att_stmt = if let Some(format) = att_stmt_fmt {
|
|
match format {
|
|
SupportedAttestationFormat::None => {
|
|
Some(AttestationStatement::None(NoneAttestationStatement {}))
|
|
}
|
|
SupportedAttestationFormat::Packed => {
|
|
let (attestation_maybe, _) = self.state.identity.attestation(&mut self.trussed);
|
|
let (signature, attestation_algorithm) = {
|
|
if let Some(attestation) = attestation_maybe.as_ref() {
|
|
let signing_algorithm = SigningAlgorithm::P256;
|
|
let signature = signing_algorithm.sign(
|
|
&mut self.trussed,
|
|
attestation.0,
|
|
&commitment,
|
|
);
|
|
(
|
|
signature.to_bytes().map_err(|_| Error::Other)?,
|
|
signing_algorithm.into(),
|
|
)
|
|
} else {
|
|
(signature.clone(), credential.algorithm())
|
|
}
|
|
};
|
|
let packed = PackedAttestationStatement {
|
|
alg: attestation_algorithm,
|
|
sig: signature,
|
|
x5c: attestation_maybe.as_ref().map(|attestation| {
|
|
// See: https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements
|
|
let cert = attestation.1.clone();
|
|
let mut x5c = Vec::new();
|
|
x5c.push(cert).ok();
|
|
x5c
|
|
}),
|
|
};
|
|
Some(AttestationStatement::Packed(packed))
|
|
}
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if !is_rk {
|
|
syscall!(self.trussed.delete(key));
|
|
}
|
|
|
|
let mut response = ctap2::get_assertion::ResponseBuilder {
|
|
credential: credential_id.into(),
|
|
auth_data: serialized_auth_data,
|
|
signature,
|
|
}
|
|
.build();
|
|
response.number_of_credentials = num_credentials;
|
|
response.att_stmt = att_stmt;
|
|
|
|
// User with empty IDs are ignored for compatibility
|
|
if is_rk {
|
|
if let Credential::Full(credential) = &credential {
|
|
if !credential.user.id().is_empty() {
|
|
let mut user: PublicKeyCredentialUserEntity = credential.user.clone().into();
|
|
// User identifiable information (name, DisplayName, icon) MUST not
|
|
// be returned if user verification is not done by the authenticator.
|
|
// For single account per RP case, authenticator returns "id" field.
|
|
if !data.uv_performed || !data.multiple_credentials {
|
|
user.icon = None;
|
|
user.name = None;
|
|
user.display_name = None;
|
|
}
|
|
response.user = Some(user);
|
|
}
|
|
}
|
|
|
|
if large_blob_key_requested {
|
|
debug!("Sending largeBlobKey in getAssertion");
|
|
response.large_blob_key = match credential {
|
|
Credential::Stripped(stripped) => stripped.large_blob_key,
|
|
Credential::Full(full) => full.data.large_blob_key,
|
|
};
|
|
}
|
|
}
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
#[inline(never)]
|
|
fn delete_resident_key_by_user_id(
|
|
&mut self,
|
|
rp_id_hash: &[u8; 32],
|
|
user_id: &Bytes<64>,
|
|
) -> Result<()> {
|
|
// Prepare to iterate over all credentials associated to RP.
|
|
let file_name_prefix = rp_file_name_prefix(rp_id_hash);
|
|
let mut maybe_entry = syscall!(self.trussed.read_dir_first_alphabetical(
|
|
Location::Internal,
|
|
PathBuf::from(RK_DIR),
|
|
Some(file_name_prefix.clone())
|
|
))
|
|
.entry;
|
|
|
|
while let Some(entry) = maybe_entry.take() {
|
|
if !entry
|
|
.file_name()
|
|
.as_ref()
|
|
.starts_with(file_name_prefix.as_ref())
|
|
{
|
|
// We got past all credentials for the relevant RP
|
|
break;
|
|
}
|
|
|
|
if entry.file_name() == &*file_name_prefix {
|
|
debug_assert!(entry.metadata().is_dir());
|
|
error!("Migration missing");
|
|
return Err(Error::Other);
|
|
}
|
|
|
|
info_now!("this may be an RK: {:?}", &entry);
|
|
let rk_path = PathBuf::from(entry.path());
|
|
|
|
info_now!("checking RK {:?} for userId ", &rk_path);
|
|
let credential_data =
|
|
syscall!(self.trussed.read_file(Location::Internal, rk_path.clone(),)).data;
|
|
let credential_maybe = FullCredential::deserialize(&credential_data);
|
|
|
|
if let Ok(old_credential) = credential_maybe {
|
|
if old_credential.user.id() == user_id {
|
|
match old_credential.key {
|
|
credential::Key::ResidentKey(key) => {
|
|
info_now!(":: deleting resident key");
|
|
syscall!(self.trussed.delete(key));
|
|
}
|
|
_ => {
|
|
warn_now!(":: WARNING: unexpected server credential in rk.");
|
|
}
|
|
}
|
|
syscall!(self.trussed.remove_file(Location::Internal, rk_path,));
|
|
|
|
info_now!("Overwriting previous rk tied to this userId.");
|
|
break;
|
|
}
|
|
} else {
|
|
warn_now!("WARNING: Could not read RK.");
|
|
}
|
|
|
|
// prepare for next loop iteration
|
|
maybe_entry = syscall!(self.trussed.read_dir_next()).entry;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[inline(never)]
|
|
pub(crate) fn delete_resident_key_by_path(&mut self, rk_path: &Path) -> Result<()> {
|
|
info_now!("deleting RK {:?}", &rk_path);
|
|
let credential_data = syscall!(self
|
|
.trussed
|
|
.read_file(Location::Internal, PathBuf::from(rk_path),))
|
|
.data;
|
|
let credential_maybe = FullCredential::deserialize(&credential_data);
|
|
// info_now!("deleting credential {:?}", &credential);
|
|
|
|
if let Ok(credential) = credential_maybe {
|
|
match credential.key {
|
|
credential::Key::ResidentKey(key) => {
|
|
info_now!(":: deleting resident key");
|
|
syscall!(self.trussed.delete(key));
|
|
}
|
|
credential::Key::WrappedKey(_) => {}
|
|
}
|
|
} else {
|
|
// If for some reason there becomes a corrupt credential,
|
|
// we can still at least orphan the key rather then crash.
|
|
info_now!("Warning! Orpaning a key.");
|
|
}
|
|
|
|
info_now!(":: deleting RK file {:?} itself", &rk_path);
|
|
syscall!(self
|
|
.trussed
|
|
.remove_file(Location::Internal, PathBuf::from(rk_path),));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn large_blobs_get(
|
|
&mut self,
|
|
request: &ctap2::large_blobs::Request,
|
|
config: large_blobs::Config,
|
|
length: u32,
|
|
) -> Result<ctap2::large_blobs::Response> {
|
|
debug!(
|
|
"large_blobs_get: length = {length}, offset = {}",
|
|
request.offset
|
|
);
|
|
// 1.-2. Validate parameters
|
|
if request.length.is_some()
|
|
|| request.pin_uv_auth_param.is_some()
|
|
|| request.pin_uv_auth_protocol.is_some()
|
|
{
|
|
error!("length/pin set");
|
|
return Err(Error::InvalidParameter);
|
|
}
|
|
// 3. Validate length
|
|
let Ok(length) = usize::try_from(length) else {
|
|
return Err(Error::InvalidLength);
|
|
};
|
|
if length > self.config.max_msg_size.saturating_sub(64) {
|
|
return Err(Error::InvalidLength);
|
|
}
|
|
// 4. Validate offset
|
|
let Ok(offset) = usize::try_from(request.offset) else {
|
|
error!("offset too large");
|
|
return Err(Error::InvalidParameter);
|
|
};
|
|
let stored_length = large_blobs::size(&mut self.trussed, config.location)?;
|
|
if offset > stored_length {
|
|
error!("offset: {offset}, stored_length: {stored_length}");
|
|
return Err(Error::InvalidParameter);
|
|
};
|
|
// 5. Return requested data
|
|
info!("Reading large-blob array from offset {offset}");
|
|
let data = large_blobs::read_chunk(&mut self.trussed, config.location, offset, length)?;
|
|
let mut response = ctap2::large_blobs::Response::default();
|
|
response.config = Some(data);
|
|
Ok(response)
|
|
}
|
|
|
|
fn large_blobs_set(
|
|
&mut self,
|
|
request: &ctap2::large_blobs::Request,
|
|
config: large_blobs::Config,
|
|
data: &[u8],
|
|
) -> Result<ctap2::large_blobs::Response> {
|
|
debug!(
|
|
"large_blobs_set: |data| = {}, offset = {}, length = {:?}",
|
|
data.len(),
|
|
request.offset,
|
|
request.length
|
|
);
|
|
// 1. Validate data
|
|
if data.len() > self.config.max_msg_size.saturating_sub(64) {
|
|
return Err(Error::InvalidLength);
|
|
}
|
|
if request.offset == 0 {
|
|
// 2. Calculate expected length and offset
|
|
// 2.1. Require length
|
|
let Some(length) = request.length else {
|
|
return Err(Error::InvalidParameter);
|
|
};
|
|
// 2.2. Check that length is not too big
|
|
let Ok(length) = usize::try_from(length) else {
|
|
return Err(Error::LargeBlobStorageFull);
|
|
};
|
|
if length > config.max_size() {
|
|
return Err(Error::LargeBlobStorageFull);
|
|
}
|
|
// 2.3. Check that length is not too small
|
|
if length < large_blobs::MIN_SIZE {
|
|
return Err(Error::InvalidParameter);
|
|
}
|
|
// 2.4-5. Set expected length and offset
|
|
self.state.runtime.large_blobs.expected_length = length;
|
|
self.state.runtime.large_blobs.expected_next_offset = 0;
|
|
} else {
|
|
// 3. Validate parameters
|
|
if request.length.is_some() {
|
|
return Err(Error::InvalidParameter);
|
|
}
|
|
}
|
|
|
|
// 4. Validate offset
|
|
let Ok(offset) = usize::try_from(request.offset) else {
|
|
return Err(Error::InvalidSeq);
|
|
};
|
|
if offset != self.state.runtime.large_blobs.expected_next_offset {
|
|
return Err(Error::InvalidSeq);
|
|
}
|
|
|
|
// 5. Perform uv
|
|
// TODO: support alwaysUv
|
|
if self.state.persistent.pin_is_set() {
|
|
let Some(pin_uv_auth_param) = request.pin_uv_auth_param else {
|
|
return Err(Error::PinRequired);
|
|
};
|
|
let Some(pin_uv_auth_protocol) = request.pin_uv_auth_protocol else {
|
|
return Err(Error::PinRequired);
|
|
};
|
|
if pin_uv_auth_protocol != 1 {
|
|
return Err(Error::PinAuthInvalid);
|
|
}
|
|
let pin_protocol = self.parse_pin_protocol(pin_uv_auth_protocol)?;
|
|
// TODO: check pinUvAuthToken
|
|
let pin_auth: [u8; 16] = pin_uv_auth_param
|
|
.as_ref()
|
|
.try_into()
|
|
.map_err(|_| Error::PinAuthInvalid)?;
|
|
|
|
let mut auth_data: Bytes<70> = Bytes::new();
|
|
// 32x 0xff
|
|
auth_data.resize(32, 0xff).unwrap();
|
|
// h'0c00'
|
|
auth_data.push(0x0c).unwrap();
|
|
auth_data.push(0x00).unwrap();
|
|
// uint32LittleEndian(offset)
|
|
auth_data
|
|
.extend_from_slice(&request.offset.to_le_bytes())
|
|
.unwrap();
|
|
// SHA-256(data)
|
|
auth_data.extend_from_slice(&Sha256::digest(data)).unwrap();
|
|
|
|
let mut pin_protocol = self.pin_protocol(pin_protocol);
|
|
let pin_token = pin_protocol.verify_pin_token(&pin_auth, &auth_data)?;
|
|
pin_token.require_permissions(Permissions::LARGE_BLOB_WRITE)?;
|
|
}
|
|
|
|
// 6. Validate data length
|
|
if offset + data.len() > self.state.runtime.large_blobs.expected_length {
|
|
return Err(Error::InvalidParameter);
|
|
}
|
|
|
|
// 7.-11. Write the buffer
|
|
info!("Writing large-blob array to offset {offset}");
|
|
large_blobs::write_chunk(
|
|
&mut self.trussed,
|
|
&mut self.state.runtime.large_blobs,
|
|
config.location,
|
|
data,
|
|
)?;
|
|
|
|
Ok(ctap2::large_blobs::Response::default())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
enum SupportedAttestationFormat {
|
|
None,
|
|
Packed,
|
|
}
|
|
|
|
impl SupportedAttestationFormat {
|
|
fn select(preference: &AttestationFormatsPreference) -> Option<Self> {
|
|
if preference.known_formats() == [AttestationStatementFormat::None]
|
|
&& !preference.includes_unknown_formats()
|
|
{
|
|
// platform requested only None --> omit attestation statement
|
|
return None;
|
|
}
|
|
// use first known and supported format, or default to packed format
|
|
let format = preference
|
|
.known_formats()
|
|
.iter()
|
|
.copied()
|
|
.flat_map(Self::try_from)
|
|
.next()
|
|
.unwrap_or(Self::Packed);
|
|
Some(format)
|
|
}
|
|
}
|
|
|
|
impl From<SupportedAttestationFormat> for AttestationStatementFormat {
|
|
fn from(format: SupportedAttestationFormat) -> Self {
|
|
match format {
|
|
SupportedAttestationFormat::None => Self::None,
|
|
SupportedAttestationFormat::Packed => Self::Packed,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<AttestationStatementFormat> for SupportedAttestationFormat {
|
|
type Error = Error;
|
|
|
|
fn try_from(format: AttestationStatementFormat) -> core::result::Result<Self, Self::Error> {
|
|
match format {
|
|
AttestationStatementFormat::None => Ok(Self::None),
|
|
AttestationStatementFormat::Packed => Ok(Self::Packed),
|
|
_ => Err(Error::Other),
|
|
}
|
|
}
|
|
}
|
|
|
|
// The new path scheme for disvoerable credentials (= resident keys) is:
|
|
// rk/<rp_id_hash>.<credential_id_hash>
|
|
// The hashes are truncated to the first eight bytes and formatted as hex strings.
|
|
// We use the following terms for the components:
|
|
// rk_path: rk/<rp_id_hash>.<credential_id_hash>
|
|
// rp_file_name_prefix: <rp_id_hash>
|
|
|
|
fn rp_file_name_prefix(rp_id_hash: &[u8; 32]) -> PathBuf {
|
|
let mut hex = [b'0'; 16];
|
|
super::format_hex(&rp_id_hash[..8], &mut hex);
|
|
PathBuf::try_from(&hex).unwrap()
|
|
}
|
|
|
|
fn rk_path(rp_id_hash: &[u8; 32], credential_id_hash: &[u8; 32]) -> PathBuf {
|
|
// 16 bytes per hash + dot + trailing zero = 34
|
|
let mut buf = [0; 34];
|
|
buf[16] = b'.';
|
|
format_hex(&rp_id_hash[..8], &mut buf[..16]);
|
|
format_hex(&credential_id_hash[..8], &mut buf[17..33]);
|
|
|
|
let mut path = PathBuf::from(RK_DIR);
|
|
path.push(Path::from_bytes_with_nul(&buf).unwrap());
|
|
path
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{rk_path, rp_file_name_prefix};
|
|
|
|
const TEST_HASH: &[u8; 32] = &[
|
|
134, 54, 157, 96, 10, 28, 233, 79, 219, 59, 195, 125, 165, 251, 120, 14, 49, 152, 212, 191,
|
|
114, 137, 180, 207, 255, 177, 187, 106, 173, 1, 203, 171,
|
|
];
|
|
const TEST_HASH_HEX: &str = "86369d600a1ce94f";
|
|
|
|
#[test]
|
|
fn test_rp_file_name_prefix() {
|
|
assert_eq!(rp_file_name_prefix(&[0; 32]).as_str(), "0000000000000000");
|
|
assert_eq!(rp_file_name_prefix(TEST_HASH).as_str(), TEST_HASH_HEX);
|
|
}
|
|
|
|
#[test]
|
|
fn test_rk_path() {
|
|
fn test(rp_id_hash: &[u8; 32], credential_id_hash: &[u8; 32], expected: &str) {
|
|
println!("rp_id_hash: {rp_id_hash:?}");
|
|
println!("credential_id_hash: {credential_id_hash:?}");
|
|
let actual = rk_path(rp_id_hash, credential_id_hash);
|
|
assert_eq!(actual.as_str(), expected);
|
|
}
|
|
|
|
let input_zero = &[0; 32];
|
|
let output_zero = "0000000000000000";
|
|
let input_nonzero = TEST_HASH;
|
|
let output_nonzero = TEST_HASH_HEX;
|
|
|
|
test(
|
|
input_zero,
|
|
input_zero,
|
|
&format!("rk/{output_zero}.{output_zero}"),
|
|
);
|
|
test(
|
|
input_zero,
|
|
input_nonzero,
|
|
&format!("rk/{output_zero}.{output_nonzero}"),
|
|
);
|
|
test(
|
|
input_nonzero,
|
|
input_zero,
|
|
&format!("rk/{output_nonzero}.{output_zero}"),
|
|
);
|
|
test(
|
|
input_nonzero,
|
|
input_nonzero,
|
|
&format!("rk/{output_nonzero}.{output_nonzero}"),
|
|
);
|
|
}
|
|
}
|