Skip to content

Commit ca9f71c

Browse files
authored
Merge pull request #51 from 1Password/progdrasil/rust-and-clippy-updates
Updates from internal usage
2 parents 90c1c28 + 4126715 commit ca9f71c

31 files changed

+143
-121
lines changed

CHANGELOG.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
# Changelog
22

33
## Unreleased
4-
- Added: support for controlling generated credential's ID length to passkey-authenticator ([#49](https://github.com/1Password/passkey-rs/pull/49))
4+
5+
## Passkey v0.4.0
6+
### passkey-authenticator v0.4.0
7+
8+
- Added: support for controlling generated credential's ID length to Authenticator ([#49](https://github.com/1Password/passkey-rs/pull/49))
9+
- ⚠ BREAKING: Removal of `Authenticator::set_display_name` and `Authenticator::display_name` methods ([#51](https://github.com/1Password/passkey-rs/pull/51))
10+
11+
### passkey-client v0.4.0
12+
- ⚠ BREAKING: Update android asset link verification ([#51](https://github.com/1Password/passkey-rs/pull/51))
13+
- Change `asset_link_url` parameter in `UnverifiedAssetLink::new` to be required rather than optional.
14+
- Remove internal string in `ValidationError::InvalidAssetLinkUrl` variant.
15+
- Remove special casing of responses for specific RPs ([#51](https://github.com/1Password/passkey-rs/pull/51))
16+
- Added `RpIdValidator::is_valid_rp_id` to verify that an rp_id is valid to be used as such ([#51](https://github.com/1Password/passkey-rs/pull/51))
17+
18+
### passkey-types v0.4.0
19+
- ⚠ BREAKING: Removal of `CredentialPropertiesOutput::authenticator_display_name` ([#51](https://github.com/1Password/passkey-rs/pull/51))
20+
521

622
## Passkey v0.3.0
723
### passkey-authenticator v0.3.0

passkey-authenticator/Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "passkey-authenticator"
3-
version = "0.3.0"
3+
version = "0.4.0"
44
description = "A webauthn authenticator supporting passkeys."
55
include = ["src/", "../LICENSE-APACHE", "../LICENSE-MIT"]
66
readme = "README.md"
@@ -25,14 +25,14 @@ coset = "0.3"
2525
log = "0.4"
2626
mockall = { version = "0.11", optional = true }
2727
p256 = { version = "0.13", features = ["pem", "arithmetic", "jwk"] }
28-
passkey-types = { path = "../passkey-types", version = "0.3" }
28+
passkey-types = { path = "../passkey-types", version = "0.4" }
2929
rand = "0.8"
3030
tokio = { version = "1", features = ["sync"], optional = true }
3131

3232
[dev-dependencies]
3333
mockall = { version = "0.11" }
34-
passkey-types = { path = "../passkey-types", version = "0.3", features = [
35-
"testable",
34+
passkey-types = { path = "../passkey-types", version = "0.4", features = [
35+
"testable",
3636
] }
3737
tokio = { version = "1", features = ["sync", "macros", "rt"] }
3838
generic-array = { version = "0.14", default-features = false }

passkey-authenticator/src/authenticator.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,6 @@ where
118118
}
119119
}
120120

121-
/// Set the authenticator's display name which will be returned if [`webauthn::CredentialPropertiesOutput`] is requested.
122-
pub fn set_display_name(&mut self, name: String) {
123-
self.extensions.display_name.replace(name);
124-
}
125-
126-
/// Get a reference to the authenticators display name to return in [`webauthn::CredentialPropertiesOutput`].
127-
pub fn display_name(&self) -> Option<&String> {
128-
self.extensions.display_name.as_ref()
129-
}
130-
131121
/// Set whether the authenticator should save new credentials with a signature counter.
132122
///
133123
/// NOTE: Using a counter with a credential that will sync is not recommended and can cause friction

passkey-authenticator/src/authenticator/extensions.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ use crate::Authenticator;
2828
#[derive(Debug, Default)]
2929
#[non_exhaustive]
3030
pub(super) struct Extensions {
31-
/// The display name given when a [`webauthn::CredentialPropertiesOutput`] is requested
32-
pub display_name: Option<String>,
33-
3431
/// Extension to retrieve a symmetric secret from the authenticator.
3532
pub hmac_secret: Option<HmacSecretConfig>,
3633
}

passkey-authenticator/src/authenticator/get_assertion.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use passkey_types::{
55
AuthenticatorData, Ctap2Error, Flags, StatusCode,
66
},
77
webauthn::PublicKeyCredentialUserEntity,
8-
Passkey,
98
};
109

1110
use crate::{private_key_from_cose_key, Authenticator, CredentialStore, UserValidationMethod};
@@ -14,7 +13,6 @@ impl<S: CredentialStore + Sync, U> Authenticator<S, U>
1413
where
1514
S: CredentialStore + Sync,
1615
U: UserValidationMethod<PasskeyItem = <S as CredentialStore>::PasskeyItem> + Sync,
17-
Passkey: TryFrom<<S as CredentialStore>::PasskeyItem> + Clone,
1816
{
1917
/// This method is used by a host to request cryptographic proof of user authentication as well
2018
/// as user consent to a given transaction, using a previously generated credential that is

passkey-authenticator/src/user_validation.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ pub struct UserCheck {
1515

1616
/// Pluggable trait for the [`Authenticator`] to do user interaction and verification.
1717
#[cfg_attr(any(test, feature = "testable"), mockall::automock(type PasskeyItem = Passkey;))]
18-
#[cfg_attr(any(test, feature = "testable"), allow(clippy::unused_async))] // Generated by the `mockall` macro.
1918
#[async_trait::async_trait]
2019
pub trait UserValidationMethod {
2120
/// The type of the passkey item that can be used to display additional information about the operation to the user.

passkey-client/Cargo.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "passkey-client"
3-
version = "0.3.0"
3+
version = "0.4.0"
44
description = "Webauthn client in Rust."
55
include = ["src/", "../LICENSE-APACHE", "../LICENSE-MIT"]
66
readme = "README.md"
@@ -22,8 +22,8 @@ android-asset-validation = ["dep:nom"]
2222
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
2323

2424
[dependencies]
25-
passkey-authenticator = { path = "../passkey-authenticator", version = "0.3" }
26-
passkey-types = { path = "../passkey-types", version = "0.3" }
25+
passkey-authenticator = { path = "../passkey-authenticator", version = "0.4" }
26+
passkey-types = { path = "../passkey-types", version = "0.4" }
2727
public-suffix = { path = "../public-suffix", version = "0.1" }
2828
serde = { version = "1", features = ["derive"] }
2929
serde_json = "1"
@@ -39,8 +39,8 @@ nom = { version = "7", features = ["alloc"], optional = true }
3939
[dev-dependencies]
4040
coset = "0.3"
4141
mockall = { version = "0.11" }
42-
passkey-authenticator = { path = "../passkey-authenticator", version = "0.3", features = [
43-
"tokio",
44-
"testable",
42+
passkey-authenticator = { path = "../passkey-authenticator", version = "0.4", features = [
43+
"tokio",
44+
"testable",
4545
] }
4646
tokio = { version = "1", features = ["macros", "rt"] }

passkey-client/src/android.rs

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use nom::{
88
use std::{borrow::Cow, fmt::Debug, str::from_utf8};
99
use url::Url;
1010

11-
#[derive(Debug)]
11+
#[derive(Debug, Clone)]
1212
/// An Unverified asset link.
1313
pub struct UnverifiedAssetLink<'a> {
1414
/// Application package name.
@@ -27,19 +27,21 @@ impl<'a> UnverifiedAssetLink<'a> {
2727
package_name: impl Into<Cow<'a, str>>,
2828
sha256_cert_fingerprint: &str,
2929
host: impl Into<Cow<'a, str>>,
30-
asset_link_url: Option<Url>,
30+
asset_link_url: Url,
3131
) -> Result<Self, ValidationError> {
32+
// Is this correct?
33+
// It looks like you can set your own url path.
34+
// https://developers.google.com/digital-asset-links/v1/statements#scaling-to-dozens-of-statements-or-more
35+
if !valid_asset_link_url(&asset_link_url) {
36+
return Err(ValidationError::InvalidAssetLinkUrl);
37+
}
3238
let host = host.into();
33-
let url = match asset_link_url {
34-
Some(u) => u,
35-
None => Url::parse(&format!("https://{host}/.well-known/assetlinks.json",))
36-
.map_err(|e| ValidationError::InvalidAssetLinkUrl(e.to_string()))?,
37-
};
39+
3840
valid_fingerprint(sha256_cert_fingerprint).map(|sha256_cert_fingerprint| Self {
3941
package_name: package_name.into(),
4042
sha256_cert_fingerprint,
4143
host,
42-
asset_link_url: url,
44+
asset_link_url,
4345
})
4446
}
4547

@@ -71,8 +73,8 @@ pub enum ValidationError {
7173
ParseFailed(String),
7274
/// The fingerprint had an invalid length.
7375
InvalidLength,
74-
/// The asset link url could not be parsed.
75-
InvalidAssetLinkUrl(String),
76+
/// The asset link url is not secure or incorrect path.
77+
InvalidAssetLinkUrl,
7678
}
7779

7880
impl<T> From<nom::Err<nom::error::Error<T>>> for ValidationError {
@@ -120,10 +122,15 @@ pub fn valid_fingerprint(fingerprint: &str) -> Result<Vec<u8>, ValidationError>
120122
.ok_or(ValidationError::InvalidLength)
121123
}
122124

125+
/// Check for secure and expected path.
126+
fn valid_asset_link_url(url: &Url) -> bool {
127+
url.scheme() == "https" && url.path() == "/.well-known/assetlinks.json"
128+
}
129+
123130
#[cfg(test)]
124131
mod test {
125-
use super::valid_fingerprint;
126-
use crate::ValidationError;
132+
use super::{valid_asset_link_url, valid_fingerprint, ValidationError};
133+
use url::Url;
127134

128135
#[test]
129136
fn check_valid_fingerprint() {
@@ -154,4 +161,22 @@ mod test {
154161
"Should be valid fingerprint"
155162
);
156163
}
164+
165+
#[test]
166+
fn asset_link_url_ok() {
167+
let url = Url::parse("https://www.facebook.com/.well-known/assetlinks.json").unwrap();
168+
assert!(valid_asset_link_url(&url));
169+
}
170+
171+
#[test]
172+
fn asset_link_url_not_secure() {
173+
let url = Url::parse("http://www.facebook.com/.well-known/assetlinks.json").unwrap();
174+
assert!(!valid_asset_link_url(&url));
175+
}
176+
177+
#[test]
178+
fn asset_link_url_unexpected_path() {
179+
let url = Url::parse("https://www.facebook.com/assetlinks.json").unwrap();
180+
assert!(!valid_asset_link_url(&url));
181+
}
157182
}

passkey-client/src/extensions.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ use passkey_types::{
1717
AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs,
1818
CredentialPropertiesOutput, PublicKeyCredentialRequestOptions,
1919
},
20-
Passkey,
2120
};
2221

2322
use crate::{Client, WebauthnError};
@@ -29,7 +28,6 @@ where
2928
S: CredentialStore + Sync,
3029
U: UserValidationMethod + Sync,
3130
P: public_suffix::EffectiveTLDProvider + Sync + 'static,
32-
Passkey: TryFrom<<S as CredentialStore>::PasskeyItem>,
3331
{
3432
/// Create the extension inputs to be passed to an authenticator over CTAP2
3533
/// during a registration request.
@@ -55,7 +53,6 @@ where
5553

5654
Some(CredentialPropertiesOutput {
5755
discoverable: Some(discoverable),
58-
authenticator_display_name: self.authenticator.display_name().cloned(),
5956
})
6057
} else {
6158
None

passkey-client/src/lib.rs

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
mod client_data;
1818
pub use client_data::*;
1919

20-
use std::{borrow::Cow, fmt::Display};
20+
use std::{borrow::Cow, fmt::Display, ops::ControlFlow};
2121

2222
use ciborium::{cbor, value::Value};
2323
use coset::{iana::EnumI64, Algorithm};
@@ -35,8 +35,6 @@ use typeshare::typeshare;
3535
use url::Url;
3636

3737
mod extensions;
38-
mod quirks;
39-
use quirks::QuirkyRp;
4038

4139
#[cfg(feature = "android-asset-validation")]
4240
mod android;
@@ -160,7 +158,6 @@ where
160158
S: CredentialStore + Sync,
161159
U: UserValidationMethod + Sync,
162160
P: public_suffix::EffectiveTLDProvider + Sync + 'static,
163-
Passkey: TryFrom<<S as CredentialStore>::PasskeyItem>,
164161
{
165162
authenticator: Authenticator<S, U>,
166163
rp_id_verifier: RpIdVerifier<P>,
@@ -187,7 +184,6 @@ where
187184
S: CredentialStore + Sync,
188185
U: UserValidationMethod<PasskeyItem = <S as CredentialStore>::PasskeyItem> + Sync,
189186
P: public_suffix::EffectiveTLDProvider + Sync + 'static,
190-
Passkey: TryFrom<<S as CredentialStore>::PasskeyItem>,
191187
{
192188
/// Create a `Client` with a given `Authenticator` and a custom TLD provider
193189
/// that implements `[public_suffix::EffectiveTLDProvider]`.
@@ -353,9 +349,7 @@ where
353349
client_extension_results,
354350
};
355351

356-
// Sanitize output before sending it back to the RP
357-
let maybe_quirky_rp = QuirkyRp::from_rp_id(rp_id);
358-
Ok(maybe_quirky_rp.map_create_credential(response))
352+
Ok(response)
359353
}
360354

361355
/// Authenticate a Webauthn request.
@@ -549,30 +543,59 @@ where
549543
effective_domain = rp_id;
550544
}
551545

552-
// guard against localhost effective domain, return early
553-
if effective_domain == "localhost" {
554-
return if self.allows_insecure_localhost {
555-
Ok(effective_domain)
556-
} else {
557-
Err(WebauthnError::InsecureLocalhostNotAllowed)
558-
};
546+
// Guard against local host and assert rp_id is not part of the public suffix list
547+
if let ControlFlow::Break(res) = self.assert_valid_rp_id(effective_domain) {
548+
return res;
559549
}
560550

561551
// Make sure origin uses https://
562552
if !(origin.scheme().eq_ignore_ascii_case("https")) {
563553
return Err(WebauthnError::UnprotectedOrigin);
564554
}
565555

556+
Ok(effective_domain)
557+
}
558+
559+
fn assert_valid_rp_id<'a>(
560+
&self,
561+
rp_id: &'a str,
562+
) -> ControlFlow<Result<&'a str, WebauthnError>, ()> {
563+
// guard against localhost effective domain, return early
564+
if rp_id == "localhost" {
565+
return if self.allows_insecure_localhost {
566+
ControlFlow::Break(Ok(rp_id))
567+
} else {
568+
ControlFlow::Break(Err(WebauthnError::InsecureLocalhostNotAllowed))
569+
};
570+
}
571+
566572
// assert rp_id is not part of the public suffix list and is a registerable domain.
567-
if decode_host(effective_domain)
573+
if decode_host(rp_id)
568574
.as_ref()
569575
.and_then(|s| self.tld_provider.effective_tld_plus_one(s).ok())
570576
.is_none()
571577
{
572-
return Err(WebauthnError::InvalidRpId);
578+
return ControlFlow::Break(Err(WebauthnError::InvalidRpId));
573579
}
574580

575-
Ok(effective_domain)
581+
ControlFlow::Continue(())
582+
}
583+
584+
/// Parse a given Relying Party ID and assert that it is valid to act as such.
585+
///
586+
/// This method is only to assert that an RP ID passes the required checks.
587+
/// In order to ensure that a request's origin is in accordance with it's claimed RP ID,
588+
/// [`Self::assert_domain`] should be used.
589+
///
590+
/// There are several checks that an RP ID must pass:
591+
/// 1. An RP ID set to `localhost` is only allowed when explicitly enabled with [`Self::allows_insecure_localhost`].
592+
/// 1. An RP ID must not be part of the [public suffix list],
593+
/// since that would allow it to act as a credential for unrelated services by other entities.
594+
pub fn is_valid_rp_id(&self, rp_id: &str) -> bool {
595+
match self.assert_valid_rp_id(rp_id) {
596+
ControlFlow::Continue(_) | ControlFlow::Break(Ok(_)) => true,
597+
ControlFlow::Break(Err(_)) => false,
598+
}
576599
}
577600

578601
#[cfg(feature = "android-asset-validation")]

0 commit comments

Comments
 (0)