-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'upstream/main' into PM-8576-CONTRIB-PM-…
…7148-PM-7149-non-discoverable-credentials
- Loading branch information
Showing
8 changed files
with
385 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
); | ||
} | ||
} |
Oops, something went wrong.