Skip to content

Commit

Permalink
Implement HMAC secret extension (#83)
Browse files Browse the repository at this point in the history
This is working, but I'll mark it as WIP for now, as we may want to
incorporate extensions-related issues in here as well.

Noteworthy things here:
- `Ctap2PinUvAuthProtocol` got a new function to create a boxed
`PinUvAuthProtocol`-object out of a given protocol version, as handing
around the boxed dyn-trait object is quite annoying.
- I extended what will be saved in the channel wrt to auth_token-stuff,
as we need the shared_secret, the key_agreement and the used protocol
version in order to be able to process the HMAC input AND output. At
least, with this, I could de-duplicate `get_uv_auth_token()`. However,
there is also potentially a problem here, as we currently always create
a shared_secret, which we shouldn't do in the CTAP 2.0 case, see also
mozilla/authenticator-rs#341
- Extension input exists in two versions: `HMACGetSecretInput` as given
by the user via `GetAssertionResponseExtensions`. This is remapped to
`CalculatedHMACGetSecretInput` and `Ctap2GetAssertionRequestExtensions`,
respectively. `CalculatedHMACGetSecretInput` holds both the user-input
and the info we will send to the device (as we need to cache the
user-input somewhere, to calculate the actual hmac-secret input when we
have established a shared_secret).
- `HMACGetSecretOutput` works similarly in reverse, but I currently
don't rewrap to a new struct. The resulting output will contain the
encrypted message from the device and the decrypted info. The encrypted
message is marked private, though.

Maybe we can work out a more generic way here, to close some of the open
`[Extensions]`-issues.
  • Loading branch information
msirringhaus authored Mar 5, 2025
1 parent bbbc26c commit 60d6f5b
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 103 deletions.
6 changes: 5 additions & 1 deletion libwebauthn/examples/webauthn_extensions_hid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use rand::{thread_rng, Rng};
use tracing_subscriber::{self, EnvFilter};

use libwebauthn::ops::webauthn::{
GetAssertionRequest, GetAssertionRequestExtensions, MakeCredentialRequest,
GetAssertionRequest, GetAssertionRequestExtensions, HMACGetSecretInput, MakeCredentialRequest,
MakeCredentialsRequestExtensions, UserVerificationRequirement,
};
use libwebauthn::pin::{UvProvider, StdinPromptPinProvider};
Expand Down Expand Up @@ -100,6 +100,10 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
user_verification: UserVerificationRequirement::Discouraged,
extensions: Some(GetAssertionRequestExtensions {
cred_blob: Some(true),
hmac_secret: Some(HMACGetSecretInput {
salt1: [1; 32],
salt2: None,
}),
}),
timeout: TIMEOUT,
};
Expand Down
6 changes: 3 additions & 3 deletions libwebauthn/src/ops/u2f.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ impl UpgradableResponse<GetAssertionResponse, SignRequest> for SignResponse {
};

// Let authenticatorGetAssertionResponse be a CBOR map with the following keys whose values are as follows: [..]
let upgraded_response: GetAssertionResponse = Ctap2GetAssertionResponse {
let response = Ctap2GetAssertionResponse {
credential_id: Some(Ctap2PublicKeyCredentialDescriptor {
r#type: Ctap2PublicKeyCredentialType::PublicKey,
id: ByteBuf::from(request.key_handle.clone()),
Expand All @@ -195,8 +195,8 @@ impl UpgradableResponse<GetAssertionResponse, SignRequest> for SignResponse {
unsigned_extension_outputs: None,
enterprise_attestation: None,
attestation_statement: None,
}
.into();
};
let upgraded_response = [response.into_assertion_output(None)].as_slice().into();

trace!(?upgraded_response);
Ok(upgraded_response)
Expand Down
104 changes: 76 additions & 28 deletions libwebauthn/src/ops/webauthn.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
use std::time::Duration;
use std::{collections::BTreeMap, time::Duration};

use ctap_types::ctap2::credential_management::CredentialProtectionPolicy;
use serde::{Deserialize, Serialize};
use serde_cbor::Value;
use sha2::{Digest, Sha256};
use tracing::{debug, instrument, trace};
use tracing::{debug, error, instrument, trace};

use crate::{
fido::AuthenticatorData,
pin::PinUvAuthProtocol,
proto::{
ctap1::{Ctap1RegisteredKey, Ctap1Version},
ctap2::{
Ctap2COSEAlgorithmIdentifier, Ctap2CredentialType, Ctap2GetAssertionResponse,
Ctap2AttestationStatement, Ctap2COSEAlgorithmIdentifier, Ctap2CredentialType,
Ctap2MakeCredentialResponse, Ctap2PublicKeyCredentialDescriptor,
Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialUserEntity,
},
Expand Down Expand Up @@ -145,55 +148,100 @@ pub struct GetAssertionRequest {
pub timeout: Duration,
}

#[derive(Debug, Default, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct HMACGetSecretInput {
pub salt1: [u8; 32],
pub salt2: Option<[u8; 32]>,
}

#[derive(Debug, Default, Clone)]
pub struct GetAssertionRequestExtensions {
#[serde(skip_serializing_if = "Option::is_none")]
pub cred_blob: Option<bool>,
// Thanks, FIDO-spec for this consistent naming scheme...
// #[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")]
// TODO: Do this properly with the salts
// pub hmac_secret: Option<Vec<u8>>,
pub hmac_secret: Option<HMACGetSecretInput>,
}

impl GetAssertionRequestExtensions {
pub fn skip_serializing(&self) -> bool {
self.cred_blob.is_none() /* && self.hmac_secret.is_none() */
#[derive(Clone, Debug, Default)]
pub struct HMACGetSecretOutput {
pub output1: [u8; 32],
pub output2: Option<[u8; 32]>,
}

#[derive(Clone, Debug, Default, Deserialize)]
#[serde(transparent)]
pub struct Ctap2HMACGetSecretOutput {
// We get this from the device, but have to decrypt it, and
// potentially split it into 2 arrays
#[serde(with = "serde_bytes")]
pub(crate) encrypted_output: Vec<u8>,
}

impl Ctap2HMACGetSecretOutput {
pub(crate) fn decrypt_output(
self,
shared_secret: &[u8],
uv_proto: &Box<dyn PinUvAuthProtocol>,
) -> Option<HMACGetSecretOutput> {
let output = match uv_proto.decrypt(shared_secret, &self.encrypted_output) {
Ok(o) => o,
Err(e) => {
error!("Failed to decrypt HMAC Secret output with the shared secret: {e:?}. Skipping HMAC extension");
return None;
}
};
let mut res = HMACGetSecretOutput::default();
if output.len() == 32 {
res.output1.copy_from_slice(&output);
} else if output.len() == 64 {
let (o1, o2) = output.split_at(32);
res.output1.copy_from_slice(&o1);
res.output2 = Some(o2.try_into().unwrap());
} else {
error!("Failed to split HMAC Secret outputs. Unexpected output length: {}. Skipping HMAC extension", output.len());
return None;
}

Some(res)
}
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[derive(Debug, Default, Clone)]
pub struct GetAssertionResponseExtensions {
// Stored credBlob
#[serde(default, skip_serializing_if = "Option::is_none", with = "serde_bytes")]
pub cred_blob: Option<Vec<u8>>,

// Thanks, FIDO-spec for this consistent naming scheme...
#[serde(
rename = "hmac-secret",
default,
skip_serializing_if = "Option::is_none",
with = "serde_bytes"
)]
pub hmac_secret: Option<Vec<u8>>,
pub hmac_secret: Option<HMACGetSecretOutput>,
}

#[derive(Debug, Clone)]
pub struct GetAssertionResponse {
pub assertions: Vec<Ctap2GetAssertionResponse>,
pub assertions: Vec<Assertion>,
}

#[derive(Debug, Clone)]
pub struct Assertion {
pub credential_id: Option<Ctap2PublicKeyCredentialDescriptor>,
pub authenticator_data: AuthenticatorData<GetAssertionResponseExtensions>,
pub signature: Vec<u8>,
pub user: Option<Ctap2PublicKeyCredentialUserEntity>,
pub credentials_count: Option<u32>,
pub user_selected: Option<bool>,
pub large_blob_key: Option<Vec<u8>>,
pub unsigned_extension_outputs: Option<BTreeMap<Value, Value>>,
pub enterprise_attestation: Option<bool>,
pub attestation_statement: Option<Ctap2AttestationStatement>,
}

impl From<&[Ctap2GetAssertionResponse]> for GetAssertionResponse {
fn from(assertions: &[Ctap2GetAssertionResponse]) -> Self {
impl From<&[Assertion]> for GetAssertionResponse {
fn from(assertions: &[Assertion]) -> Self {
Self {
assertions: assertions.to_owned(),
}
}
}

impl From<Ctap2GetAssertionResponse> for GetAssertionResponse {
fn from(assertion: Ctap2GetAssertionResponse) -> Self {
impl From<Assertion> for GetAssertionResponse {
fn from(assertion: Assertion) -> Self {
Self {
assertions: vec![assertion],
}
Expand Down
11 changes: 11 additions & 0 deletions libwebauthn/src/proto/ctap2/model/client_pin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use serde_bytes::ByteBuf;
use serde_indexed::{DeserializeIndexed, SerializeIndexed};
use serde_repr::{Deserialize_repr, Serialize_repr};

use crate::pin::{PinUvAuthProtocol, PinUvAuthProtocolOne, PinUvAuthProtocolTwo};

#[derive(Debug, Clone, SerializeIndexed)]
#[serde_indexed(offset = 1)]
pub struct Ctap2ClientPinRequest {
Expand Down Expand Up @@ -212,6 +214,15 @@ pub enum Ctap2PinUvAuthProtocol {
Two = 2,
}

impl Ctap2PinUvAuthProtocol {
pub(crate) fn create_protocol_object(&self) -> Box<dyn PinUvAuthProtocol> {
match self {
Ctap2PinUvAuthProtocol::One => Box::new(PinUvAuthProtocolOne::new()),
Ctap2PinUvAuthProtocol::Two => Box::new(PinUvAuthProtocolTwo::new()),
}
}
}

#[repr(u32)]
#[derive(Debug, Clone, FromPrimitive, PartialEq, Serialize_repr, Deserialize_repr)]
pub enum Ctap2PinUvAuthProtocolCommand {
Expand Down
Loading

0 comments on commit 60d6f5b

Please sign in to comment.