Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into PM-8576-CONTRIB-PM-…
Browse files Browse the repository at this point in the history
…7148-PM-7149-non-discoverable-credentials
  • Loading branch information
coroiu committed Jul 23, 2024
2 parents 48a75bd + 7a349f0 commit dba4fb6
Show file tree
Hide file tree
Showing 8 changed files with 385 additions and 42 deletions.
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ handles client data and its hash.
- ⚠ BREAKING: Changed: Custom client data hashes are now specified using `DefaultClientDataWithCustomHash(Vec<u8>)` instead of
`Some(Vec<u8>)`.
- Added: Additional fields can be added to the client data using `DefaultClientDataWithExtra(ExtraData)`.
- Added: The `Client` now has the ability to adjust the response for quirky relying parties
when a fully featured response would break their server side validation. ([#31](https://github.com/1Password/passkey-rs/pull/31))
- ⚠ BREAKING: Added the `Origin` enum which is now the origin parameter for the following methods ([#32](https://github.com/1Password/passkey-rs/pull/27)):
- `Client::register` takes an `impl Into<Origin>` instead of a `&Url`
- `Client::authenticate` takes an `impl Into<Origin>` instead of a `&Url`
- `RpIdValidator::assert_domain` takes an `&Origin` instead of a `&Url`
- ⚠ BREAKING: The collected client data will now have the android app signature as the origin when a request comes from an app directly. ([#32](https://github.com/1Password/passkey-rs/pull/27))

## passkey-types

- `CollectedClientData` is now generic and supports additional strongly typed fields.
- Changed: `CollectedClientData` has changed to `CollectedClientData<E = ()>`
- The `Client` now returns `CredProps::rk` depending on the authenticator's capabilities.
Expand Down Expand Up @@ -76,4 +86,4 @@ Most of these changes are adding fields to structs which are breaking changes du

### public-suffix v0.1.1

- Update the public suffix list
- Update the public suffix list
6 changes: 2 additions & 4 deletions passkey-authenticator/src/user_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,12 @@ impl MockUserValidationMethod {
/// Sets up the mock for returning true for the verification.
pub fn verified_user(times: usize) -> Self {
let mut user_mock = MockUserValidationMethod::new();
user_mock.expect_is_presence_enabled().returning(|| true);
user_mock
.expect_is_verification_enabled()
.returning(|| Some(true))
.times(..);
user_mock
.expect_is_presence_enabled()
.returning(|| true)
.times(..);
user_mock.expect_is_presence_enabled().returning(|| true);
user_mock
.expect_check_user()
.with(
Expand Down
6 changes: 4 additions & 2 deletions passkey-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ workspace = true
[features]
tokio = ["dep:tokio"]
testable = ["dep:mockall"]
android-asset-validation = ["dep:nom"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

Expand All @@ -33,12 +34,13 @@ idna = "0.5"
url = "2"
coset = "0.3"
tokio = { version = "1", features = ["sync"], optional = true }
nom = { version = "7", features = ["alloc"], optional = true }

[dev-dependencies]
coset = "0.3"
mockall = { version = "0.11" }
passkey-authenticator = { path = "../passkey-authenticator", features = [
"tokio",
"testable",
"tokio",
"testable",
] }
tokio = { version = "1", features = ["macros", "rt"] }
157 changes: 157 additions & 0 deletions passkey-client/src/android.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
use nom::{
bytes::complete::{tag, take_while_m_n},
character::is_hex_digit,
combinator::map_res,
multi::separated_list1,
IResult,
};
use std::{borrow::Cow, fmt::Debug, str::from_utf8};
use url::Url;

#[derive(Debug)]
/// An Unverified asset link.
pub struct UnverifiedAssetLink<'a> {
/// Application package name.
package_name: Cow<'a, str>,
/// Fingerprint to compare.
sha256_cert_fingerprint: Vec<u8>,
/// Host to lookup the well known asset link.
host: Cow<'a, str>,
/// When sourced from the application statement list or parsed from host for passkeys.
asset_link_url: Url,
}

impl<'a> UnverifiedAssetLink<'a> {
/// Create a new [`UnverifiedAssetLink`].
pub fn new(
package_name: impl Into<Cow<'a, str>>,
sha256_cert_fingerprint: &str,
host: impl Into<Cow<'a, str>>,
asset_link_url: Option<Url>,
) -> Result<Self, ValidationError> {
let host = host.into();
let url = match asset_link_url {
Some(u) => u,
None => Url::parse(&format!("https://{host}/.well-known/assetlinks.json",))
.map_err(|e| ValidationError::InvalidAssetLinkUrl(e.to_string()))?,
};
valid_fingerprint(sha256_cert_fingerprint).map(|sha256_cert_fingerprint| Self {
package_name: package_name.into(),
sha256_cert_fingerprint,
host,
asset_link_url: url,
})
}

/// Fingerprint of the application's signing certificate
pub fn sha256_cert_fingerprint(&self) -> &[u8] {
self.sha256_cert_fingerprint.as_slice()
}

/// The application's package name
pub fn package_name(&self) -> &str {
&self.package_name
}

/// The host to lookup the well-known assetlinks
pub fn host(&self) -> &str {
&self.host
}

/// Get the digital asset Url for validation
pub fn asset_link_url(&self) -> Url {
self.asset_link_url.clone()
}
}

/// Digital asset fingerprint validation error.
#[derive(Debug)]
pub enum ValidationError {
/// The fingerprint could not be parsed.
ParseFailed(String),
/// The fingerprint had an invalid length.
InvalidLength,
/// The asset link url could not be parsed.
InvalidAssetLinkUrl(String),
}

impl<T> From<nom::Err<nom::error::Error<T>>> for ValidationError {
fn from(value: nom::Err<nom::error::Error<T>>) -> Self {
let code_msg = value.map(|err| format!("{:?}", err.code));
let message = match code_msg {
nom::Err::Incomplete(_) => "Parsing incomplete".to_owned(),
nom::Err::Error(msg) => format!("Parsing error: {msg}"),
nom::Err::Failure(msg) => format!("Parsing failure: {msg}"),
};

ValidationError::ParseFailed(message)
}
}

/// Make sure we have an expected fingerprint. Characters have to be uppercase.
///
/// <https://developer.android.com/training/app-links/verify-android-applinks#fix-errors>
/// * Having a lower case signature in assetlinks.json. The signature should be
/// in upper case.
pub fn valid_fingerprint(fingerprint: &str) -> Result<Vec<u8>, ValidationError> {
#[derive(Debug)]
enum HexError {
Utf8,
ParseInt,
}

fn parse_fingerprint(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
separated_list1(
tag(":"),
map_res(
take_while_m_n(2, 2, |c| is_hex_digit(c) && !c.is_ascii_lowercase()),
|hex| {
u8::from_str_radix(from_utf8(hex).map_err(|_| HexError::Utf8)?, 16)
.map_err(|_| HexError::ParseInt)
},
),
)(input)
}

let (left, parsed) = parse_fingerprint(fingerprint.as_bytes())?;

(left.is_empty() && parsed.len() == 32)
.then_some(parsed)
.ok_or(ValidationError::InvalidLength)
}

#[cfg(test)]
mod test {
use super::valid_fingerprint;
use crate::ValidationError;

#[test]
fn check_valid_fingerprint() {
assert!(
valid_fingerprint("B3:5B:68:D5:CE:84:50:55:7C:6A:55:FD:64:B5:1F:EA:C1:10:CB:36:D6:A3:52:1C:59:48:DB:3A:38:0A:34:A9").is_ok(),
"Should be valid fingerprint"
);
}

#[test]
fn check_invalid_fingerprint_lowercase() {
let result = valid_fingerprint("b3:5b:68:d5:ce:84:50:55:7c:6a:55:fd:64:b5:1f:ea:c1:10:cb:36:d6:a3:52:1c:59:48:db:3a:38:0a:34:a9");
assert!(result.is_err(), "Should be invalid fingerprint");
assert!(matches!(result, Err(ValidationError::ParseFailed(..))))
}

#[test]
fn check_invalid_fingerprint_length() {
let result = valid_fingerprint("B3:5B:68:D5:CE:84:50:55:7C:6A:55");
assert!(result.is_err(), "Should be invalid fingerprint");
assert!(matches!(result, Err(ValidationError::InvalidLength)))
}

#[test]
fn check_invalid_fingerprint_non_hex() {
assert!(
valid_fingerprint("B3:5B:68:X5:CE:84:50:55:7C:6A:55:FD:64:B5:1F:EA:C1:10:CB:36:D6:A3:52:1C:59:48:DB:3A:38:0A:34:A9").is_err(),
"Should be valid fingerprint"
);
}
}
Loading

0 comments on commit dba4fb6

Please sign in to comment.