From e3c79dfd85bbac82cee8f4c431e8a3f9c97e9b68 Mon Sep 17 00:00:00 2001 From: Emanuele Cesena Date: Sat, 23 May 2026 15:23:36 -0400 Subject: [PATCH] =?UTF-8?q?ctap2.1:=20=C2=A76.5.5=20PIN=20management=20?= =?UTF-8?q?=E2=80=94=20code-point=20validation=20+=20reject=20same-PIN=20u?= =?UTF-8?q?nder=20forcePINChange=20+=20align=20setPin/getPinToken=20error?= =?UTF-8?q?=20codes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: https://github.com/trussed-dev/fido-authenticator/issues/43 --- CHANGELOG.md | 1 + src/ctap2.rs | 88 ++++++++++-- src/state.rs | 13 +- tests/basic.rs | 308 +++++++++++++++++++++++++++++++++++++++++- tests/webauthn/mod.rs | 15 +- 5 files changed, 406 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6ecc02..aab39ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `toggleAlwaysUv` - `setMinPINLength` - Load full credential from filesstem for getAssertion if an allow list is used with a discoverable credential. +- Use UTF-8 code points instead of bytes when checking the minimum length for PINs. ## [v0.3.0](https://github.com/trussed-dev/fido-authenticator/releases/tag/v0.3.0) (2026-03-25) diff --git a/src/ctap2.rs b/src/ctap2.rs index 228ffe7..07c0116 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -772,8 +772,11 @@ impl Authenticator for crate::Authenti let pin_protocol = pin_protocol?; // 2. is pin already set + // CTAP 2.1 §6.5.5.4 step 3: a setPin request against an + // already-provisioned authenticator returns PinAuthInvalid. + // (Older CTAP 2.0 implementations returned NotAllowed.) if self.state.persistent.pin_is_set() { - return Err(Error::NotAllowed); + return Err(Error::PinAuthInvalid); } // 3. generate shared secret @@ -862,8 +865,40 @@ impl Authenticator for crate::Authenti shared_secret.delete(&mut self.trussed); - // 9. store hashed PIN - self.hash_store_pin(&new_pin)?; + // 8b. CTAP 2.1 §6.5.5.6: "If the forcePINChange member ... is + // true and LEFT(SHA-256(newPin), 16) is equal to its internal + // stored LEFT(SHA-256(curPin), 16) then authenticator returns + // CTAP2_ERR_PIN_POLICY_VIOLATION." We compute the new hash up + // front so the comparison is constant-time on a fixed-size + // array, and only return the error when force_pin_change is + // set — same-PIN change with the flag clear is allowed. + let new_pin_hash_32 = syscall!(self.trussed.hash_sha256(&new_pin)).hash; + let new_pin_hash: [u8; 16] = new_pin_hash_32[..16].try_into().unwrap(); + if self.state.persistent.force_pin_change() + && self.state.persistent.pin_hash() == Some(new_pin_hash) + { + return Err(Error::PinPolicyViolation); + } + + // 9. store hashed PIN + PINCodePointLength + // (CTAP 2.1 §6.5.5.5 — "Save the PIN with derived hash + // and PINCodePointLength"). `new_pin` was UTF-8-validated + // in `decrypt_pin_check_length` above, so the from_utf8 + // is infallible here; the unwrap_or is defensive. + let new_pin_code_point_length = core::str::from_utf8(&new_pin) + .map(|s| s.chars().count()) + .unwrap_or(new_pin.len()) as u8; + self.state.persistent.set_pin_hash( + &mut self.trussed, + new_pin_hash, + new_pin_code_point_length, + )?; + + // CTAP 2.1 §6.5.5.6 step 9: clear forcePINChange after a + // successful changePin. + self.state + .persistent + .set_force_pin_change(&mut self.trussed, false)?; self.pin_protocol(pin_protocol).reset_pin_tokens(); } @@ -913,7 +948,12 @@ impl Authenticator for crate::Authenti // 10. Reset PIN retries self.state.reset_retries(&mut self.trussed)?; - // 11. Check forcePINChange -- skipped + // 11. CTAP 2.1 §6.5.5.7.1 step 11: while forcePINChange is + // set, getPinToken returns PIN_INVALID until a successful + // changePin clears the flag. + if self.state.persistent.force_pin_change() { + return Err(Error::PinInvalid); + } // 12. Reset all PIN tokens // 13. Call beginUsingPinUvAuthToken @@ -993,7 +1033,12 @@ impl Authenticator for crate::Authenti // 10. Reset PIN retries self.state.reset_retries(&mut self.trussed)?; - // 11. Check forcePINChange -- skipped + // 11. CTAP 2.1 §6.5.5.7.3 step 11: while forcePINChange is + // set, this variant returns PIN_POLICY_VIOLATION (distinct + // from getPinToken's PIN_INVALID; see §6.5.5.7.1). + if self.state.persistent.force_pin_change() { + return Err(Error::PinPolicyViolation); + } // 12. Reset all PIN tokens // 13. Call beginUsingPinUvAuthToken @@ -1579,18 +1624,29 @@ impl crate::Authenticator { .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()); - let min_pin_length = self.state.persistent.min_pin_length().into(); - if pin_length < min_pin_length || pin_length >= 64 { + // CTAP 2.1 §6.5.5.5 / §6.5.5.6: "The authenticator drops all + // **trailing** 0x00 bytes from paddedNewPin to produce newPin." + // Embedded nulls stay (they will fail UTF-8 validation if invalid). + let stripped_len = pin.iter().rposition(|&b| b != 0).map_or(0, |last| last + 1); + + // CTAP 2.1 §6.5.5.3: "Maximum PIN Length: 63 bytes." + if stripped_len > ctap2::client_pin::MAX_PIN_LENGTH { return Err(Error::PinPolicyViolation); } - pin.resize_zero(pin_length).unwrap(); + // Issue #43: minimum PIN length is measured in **Unicode code points**, + // not bytes. UTF-8-decode the stripped bytes and count `chars()`. A + // platform that sends non-UTF-8 bytes violates the spec; we reject + // with the same PIN_POLICY_VIOLATION code we use for length issues. + let s = + core::str::from_utf8(&pin[..stripped_len]).map_err(|_| Error::PinPolicyViolation)?; + let code_points = s.chars().count(); + let min_pin_length = usize::from(self.state.persistent.min_pin_length()); + if code_points < min_pin_length { + return Err(Error::PinPolicyViolation); + } + + pin.resize_zero(stripped_len).unwrap(); Ok(pin) } @@ -1720,6 +1776,8 @@ impl crate::Authenticator { // // the idea is for multi-authnr scenario where platform // wants to enforce PIN and needs to figure out which authnrs support PIN + // (CTAP 2.1 §6.5.5.7 step 2 — was upstream PR #56; the older + // CTAP 2.0 reading was `PinAuthInvalid` for the "pin set" case.) if let Some(pin_auth) = pin_auth { if pin_auth.is_empty() { self.up @@ -1727,7 +1785,7 @@ impl crate::Authenticator { if !self.state.persistent.pin_is_set() { return Err(Error::PinNotSet); } else { - return Err(Error::PinAuthInvalid); + return Err(Error::PinInvalid); } } } diff --git a/src/state.rs b/src/state.rs index c5b8cc3..3223000 100644 --- a/src/state.rs +++ b/src/state.rs @@ -487,10 +487,21 @@ impl PersistentState { pin_hash: [u8; 16], pin_code_point_length: u8, ) -> Result<()> { + // Idempotent: if the same hash is being written and forcePINChange is + // already clear, skip the flash write. Also — and more importantly — + // if the platform "changes" the PIN to the same value, we must not + // clear `force_pin_change` (the user hasn't actually complied with + // the change request). The spec-mandated reject for "forcePINChange + // + new==old" lives in the changePIN handler; this check is a belt- + // and-braces against any other caller path. + if self.pin_hash == Some(pin_hash) { + return Ok(()); + } self.pin_hash = Some(pin_hash); self.pin_code_point_length = pin_code_point_length; // Successfully (re)setting the PIN clears any pending forcePINChange - // request — the platform has just complied (CTAP 2.1 §6.5.5.7). + // request — the platform has just complied (CTAP 2.1 §6.5.5.6 / + // §6.5.5.7). self.force_pin_change = false; self.save(trussed)?; Ok(()) diff --git a/tests/basic.rs b/tests/basic.rs index 32c7cdc..f283237 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -105,8 +105,10 @@ fn test_set_pin() { let shared_secret = get_shared_secret(&device, &key_agreement_key); let result = set_pin(&device, &key_agreement_key, &shared_secret, b"123456"); - // TODO: review error code - assert_eq!(result, Err(Ctap2Error(0x30))); + // CTAP 2.1 §6.5.5.4: "If a PIN has already been set, authenticator + // returns CTAP2_ERR_PIN_AUTH_INVALID error." (0x33). Previously this + // expected 0x30 (NotAllowed), the CTAP 2.0 reading. + assert_eq!(result, Err(Ctap2Error(0x33))); let reply = device.exec(GetInfo).unwrap(); let options = reply.options.unwrap(); @@ -1804,6 +1806,308 @@ fn test_always_uv_get_assertion_up_false_bypasses_uv_requirement() { }) } +// ---------------------------------------------------------------------------- +// PIN length validation (issue #43): count Unicode code points, not bytes +// ---------------------------------------------------------------------------- + +/// Multi-byte UTF-8 PIN with FEWER code points than minimum is rejected. +/// "héé" = 5 bytes (h + é + é where é = 0xC3 0xA9), 3 code points. Default +/// minimum is 4 → PIN_POLICY_VIOLATION. +#[test] +fn test_set_pin_short_codepoints_multibyte_rejected() { + let key_agreement_key = KeyAgreementKey::generate(); + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + // "héé" — 5 bytes, 3 code points. Pre-fix this would have passed + // because the BYTE length (5) >= 4. The fixed code rejects it. + let pin = "héé".as_bytes(); + assert_eq!(pin.len(), 5); + assert_eq!( + pin.iter().filter(|&&b| !(0x80..0xC0).contains(&b)).count(), + 3 + ); + let result = set_pin(&device, &key_agreement_key, &shared_secret, pin); + assert_eq!(result, Err(Ctap2Error(0x37))); + }) +} + +/// Multi-byte UTF-8 PIN with enough code points is accepted. +/// "héllo" = 6 bytes, 5 code points. Default minimum is 4 → succeeds. +#[test] +fn test_set_pin_codepoints_multibyte_accepted() { + let key_agreement_key = KeyAgreementKey::generate(); + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let pin = "héllo".as_bytes(); + assert_eq!(pin.len(), 6); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); + }) +} + +/// ASCII PIN at the lower bound: 4 bytes = 4 code points. Accepted. +#[test] +fn test_set_pin_four_byte_ascii_accepted() { + 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"abcd").unwrap(); + }) +} + +/// 3-byte ASCII PIN (3 code points) is rejected. +#[test] +fn test_set_pin_three_byte_ascii_rejected() { + let key_agreement_key = KeyAgreementKey::generate(); + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = set_pin(&device, &key_agreement_key, &shared_secret, b"abc"); + assert_eq!(result, Err(Ctap2Error(0x37))); + }) +} + +/// Invalid UTF-8 in the PIN bytes is rejected (PIN_POLICY_VIOLATION). Platforms +/// MUST send Normalized UTF-8 per CTAP 2.1 §6.5.5.5; bytes that don't decode +/// fail the PIN policy check. +#[test] +fn test_set_pin_invalid_utf8_rejected() { + let key_agreement_key = KeyAgreementKey::generate(); + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + // 0xC3 is a UTF-8 lead byte that must be followed by a continuation + // byte in [0x80, 0xBF]. Trailing it with 'x' makes the sequence + // invalid UTF-8. + let pin = b"abc\xC3x"; + let result = set_pin(&device, &key_agreement_key, &shared_secret, pin); + assert_eq!(result, Err(Ctap2Error(0x37))); + }) +} + +/// All-zero `paddedNewPin` strips to length 0 → 0 code points → reject. +/// Verifies the empty-PIN edge case (no leading bytes, no trailing +/// non-zero) is properly rejected against the spec floor of 4 cp. +#[test] +fn test_set_pin_empty_rejected() { + let key_agreement_key = KeyAgreementKey::generate(); + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = set_pin(&device, &key_agreement_key, &shared_secret, b""); + assert_eq!(result, Err(Ctap2Error(0x37))); + }) +} + +/// CTAP 2.1 §6.5.5.5 — UTF-8 representation of newPin MUST NOT exceed 63 +/// bytes. A 63-byte ASCII PIN (63 code points) sits exactly at the spec +/// boundary and MUST be accepted. +#[test] +fn test_set_pin_at_byte_limit_accepted() { + let key_agreement_key = KeyAgreementKey::generate(); + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + // 63 ASCII chars → 63 bytes → 63 code points. + let pin = vec![b'a'; 63]; + set_pin(&device, &key_agreement_key, &shared_secret, &pin).unwrap(); + }) +} + +/// CTAP 2.1 §6.5.5.5: a 64-byte non-zero PIN fills `paddedNewPin` +/// completely with no trailing 0x00 — the stripped length stays at 64 +/// which exceeds the spec's 63-byte UTF-8 cap, so the authenticator +/// MUST reject with PIN_POLICY_VIOLATION. +#[test] +fn test_set_pin_over_byte_limit_rejected() { + let key_agreement_key = KeyAgreementKey::generate(); + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + // 64 ASCII chars → 64 bytes → no padding room left. + let pin = vec![b'a'; 64]; + let result = set_pin(&device, &key_agreement_key, &shared_secret, &pin); + assert_eq!(result, Err(Ctap2Error(0x37))); + }) +} + +/// CTAP 2.1 §6.5.5.6 (changePIN) shares the same PIN-length validation +/// pipeline as setPIN (§6.5.5.5). Verify the code-point check applies +/// equally: a 3-byte ASCII new PIN under changePIN must be rejected +/// with PIN_POLICY_VIOLATION. +#[test] +fn test_change_pin_short_codepoints_rejected() { + let key_agreement_key = KeyAgreementKey::generate(); + let old_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, old_pin).unwrap(); + + // Attempt to change to a 3-cp PIN. + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = change_pin(&device, &key_agreement_key, &shared_secret, old_pin, b"abc"); + assert_eq!(result, Err(Ctap2Error(0x37))); + }) +} + +/// Multi-byte UTF-8 new PIN with too few code points is also rejected +/// by changePIN (parallel to the setPin multi-byte test). "héé" = +/// 5 bytes, 3 code points → reject. +#[test] +fn test_change_pin_short_codepoints_multibyte_rejected() { + let key_agreement_key = KeyAgreementKey::generate(); + let old_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, old_pin).unwrap(); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let new_pin = "héé".as_bytes(); + assert_eq!(new_pin.len(), 5); + let result = change_pin( + &device, + &key_agreement_key, + &shared_secret, + old_pin, + new_pin, + ); + assert_eq!(result, Err(Ctap2Error(0x37))); + }) +} + +/// CTAP 2.1 §6.5.5.6 changePIN: "If the forcePINChange member ... is true +/// and LEFT(SHA-256(newPin), 16) is equal to its internal stored +/// LEFT(SHA-256(curPin), 16) then authenticator returns +/// CTAP2_ERR_PIN_POLICY_VIOLATION." This blocks the trivial "rotate to the +/// same PIN" loophole when the platform is forcing a change. +#[test] +fn test_change_pin_same_pin_with_force_change_rejected() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + virt::run_ctap2(|device| { + // Setup: set PIN, then mark forcePINChange via setMinPINLength. + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); + let pin_token = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + pin, + PERM_AUTHENTICATOR_CONFIGURATION, + None, + ) + .unwrap(); + let params = AuthenticatorConfigParams { + force_change_pin: Some(true), + ..Default::default() + }; + set_min_pin_length(&device, &pin_token, params).unwrap(); + assert_eq!(device.exec(GetInfo).unwrap().force_pin_change, Some(true)); + + // Try to "change" to the same PIN — must be rejected. + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = change_pin(&device, &key_agreement_key, &shared_secret, pin, pin); + assert_eq!(result, Err(Ctap2Error(0x37))); + + // forcePINChange should still be true after the rejection. + assert_eq!(device.exec(GetInfo).unwrap().force_pin_change, Some(true)); + }) +} + +/// Counterpart: when forcePINChange is **not** set, a same-PIN changePIN +/// silently succeeds (spec doesn't reject this case, only when the flag is +/// set). This documents the current behaviour and locks it in. +#[test] +fn test_change_pin_same_pin_without_force_change_allowed() { + 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).unwrap(); + // forcePINChange is false by default. + assert_eq!(device.exec(GetInfo).unwrap().force_pin_change, Some(false)); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + change_pin(&device, &key_agreement_key, &shared_secret, pin, pin).unwrap(); + + // Still false. + assert_eq!(device.exec(GetInfo).unwrap().force_pin_change, Some(false)); + }) +} + +/// Successful changePIN to a NEW pin while forcePINChange is set must clear +/// the flag (CTAP 2.1 §6.5.5.6: "Authenticator sets the value of the +/// forcePINChange member ... to false"). +#[test] +fn test_change_pin_to_new_pin_clears_force_change() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin1 = b"123456"; + let pin2 = b"654321"; + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin1).unwrap(); + let pin_token = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + pin1, + PERM_AUTHENTICATOR_CONFIGURATION, + None, + ) + .unwrap(); + let params = AuthenticatorConfigParams { + force_change_pin: Some(true), + ..Default::default() + }; + set_min_pin_length(&device, &pin_token, params).unwrap(); + assert_eq!(device.exec(GetInfo).unwrap().force_pin_change, Some(true)); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + change_pin(&device, &key_agreement_key, &shared_secret, pin1, pin2).unwrap(); + + assert_eq!(device.exec(GetInfo).unwrap().force_pin_change, Some(false)); + }) +} + +/// CTAP 2.1 §6.1.2 step 1 (and §6.5.5.7 step 2): when the platform sends +/// a **zero-length** `pinUvAuthParam` (the CTAP 2.0 "is PIN supported?" +/// probe), the authenticator MUST request UP and then return +/// `CTAP2_ERR_PIN_INVALID` (0x31) if a PIN is set. The pre-audit code +/// returned `PIN_AUTH_INVALID` (0x33), the CTAP 2.0 reading. +#[test] +fn test_make_credential_zero_length_pin_auth_returns_0x31_when_pin_set() { + 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).unwrap(); + + let mut mc = MakeCredential::new( + vec![0u8; 32], + Rp::new("example.com"), + User::new(vec![1u8; 16]), + vec![PubKeyCredParam::new("public-key", -7)], + ); + // Zero-length pinUvAuthParam — the §6.1.2 step 1 probe. + mc.pin_auth_raw = Some(Vec::new()); + mc.pin_protocol = Some(2); + let result = device.exec(mc); + assert_eq!(result.err(), Some(Ctap2Error(0x31))); + }) +} + +/// Same probe, but with no PIN set on the device. CTAP 2.1 §6.1.2 step 1.3: +/// "return CTAP2_ERR_PIN_NOT_SET" (0x35). +#[test] +fn test_make_credential_zero_length_pin_auth_returns_0x35_when_pin_not_set() { + virt::run_ctap2(|device| { + let mut mc = MakeCredential::new( + vec![0u8; 32], + Rp::new("example.com"), + User::new(vec![1u8; 16]), + vec![PubKeyCredParam::new("public-key", -7)], + ); + mc.pin_auth_raw = Some(Vec::new()); + mc.pin_protocol = Some(2); + let result = device.exec(mc); + assert_eq!(result.err(), Some(Ctap2Error(0x35))); + }) +} + /// User-requested test: a `setMinPINLength` request derived from an INCORRECT /// PIN must fail — the platform never obtains a valid `pin_uv_auth_token`, /// so the `pin_auth` HMAC won't verify on the device side. diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index d89c3eb..4ab028b 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -391,6 +391,12 @@ pub struct MakeCredential { pub extensions: Option, pub options: Option, pub pin_auth: Option<[u8; 32]>, + /// Variable-length override for `pin_auth`, used by tests that exercise + /// the zero-length `pinUvAuthParam` path (CTAP 2.1 §6.1.2 step 1 + + /// §6.5.5.7 step 2 — "CTAP 2.0 backwards-compat" — where the platform + /// sends a 0-byte param to probe whether the authenticator has a PIN). + /// When `Some(_)`, this field is serialised instead of `pin_auth`. + pub pin_auth_raw: Option>, pub pin_protocol: Option, pub attestation_formats_preference: Option>, } @@ -410,6 +416,7 @@ impl MakeCredential { extensions: None, options: None, pin_auth: None, + pin_auth_raw: None, pin_protocol: None, attestation_formats_preference: None, } @@ -436,7 +443,13 @@ impl From for Value { if let Some(options) = request.options { map.push(7, options); } - if let Some(pin_auth) = request.pin_auth { + // `pin_auth_raw` takes precedence over the fixed-size `pin_auth` — + // tests that need a variable-length (e.g. zero-length) pinUvAuthParam + // use the raw field. The mutual-exclusion is a soft contract; the + // serializer just prefers raw when both are set. + if let Some(pin_auth_raw) = request.pin_auth_raw.as_ref() { + map.push(8, pin_auth_raw.as_slice()); + } else if let Some(pin_auth) = request.pin_auth { map.push(8, pin_auth.as_slice()); } if let Some(pin_protocol) = request.pin_protocol {