Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 60 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ _Looking for the D-Bus API proposal?_ Check out [credentialsd][credentialsd].
- 🟢 Hybrid transport (caBLE v2): QR-initiated transactions
- 🟢 Hybrid transport (caBLE v2): State-assisted transactions (remember this phone)

## Runtime requirements

Validating the relying party ID against the calling origin requires the [Public Suffix List][psl]. The built-in loader reads it from the standard system path. The `publicsuffix` package on Debian/Ubuntu or `publicsuffix-list` on Fedora and Arch installs it there, but these are not always present on minimal installs. Install explicitly if needed. Callers wiring their own list don't need a system package.

## Transports

| | FIDO U2F | WebAuthn (FIDO2) |
Expand Down Expand Up @@ -79,3 +83,4 @@ If you don't know where to start, check out the _Issues_ tab.
[#17]: https://github.com/linux-credentials/libwebauthn/issues/17
[#18]: https://github.com/linux-credentials/libwebauthn/issues/18
[#31]: https://github.com/linux-credentials/libwebauthn/issues/31
[psl]: https://publicsuffix.org/
1 change: 1 addition & 0 deletions libwebauthn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ base64-url = "3.0.0"
dbus = "0.9.5"
tracing = "0.1.29"
idna = "1.0.3"
publicsuffix = { version = "1.5", default-features = false }
url = "2.5"
maplit = "1.0.2"
sha2 = "0.10.2"
Expand Down
11 changes: 7 additions & 4 deletions libwebauthn/examples/webauthn_json_hid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use tokio::sync::broadcast::Receiver;
use tracing_subscriber::{self, EnvFilter};

use libwebauthn::ops::webauthn::{
GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin, WebAuthnIDL as _,
WebAuthnIDLResponse as _,
DatFilePublicSuffixList, GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin,
WebAuthnIDL as _, WebAuthnIDLResponse as _,
};
use libwebauthn::pin::PinRequestReason;
use libwebauthn::transport::hid::list_devices;
Expand Down Expand Up @@ -81,6 +81,9 @@ pub async fn main() -> Result<(), Box<dyn Error>> {

let request_origin: RequestOrigin =
"https://example.org".try_into().expect("Invalid origin");
let psl = DatFilePublicSuffixList::from_system_file().expect(
"PSL not available; install the publicsuffix-list package or pass an explicit path",
);
let request_json = r#"
{
"rp": {
Expand All @@ -106,7 +109,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
}
"#;
let make_credentials_request: MakeCredentialRequest =
MakeCredentialRequest::from_json(&request_origin, request_json)
MakeCredentialRequest::from_json(&request_origin, &psl, request_json)
.expect("Failed to parse request JSON");
println!(
"WebAuthn MakeCredential request: {:?}",
Expand Down Expand Up @@ -158,7 +161,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
}
"#;
let get_assertion: GetAssertionRequest =
GetAssertionRequest::from_json(&request_origin, request_json)
GetAssertionRequest::from_json(&request_origin, &psl, request_json)
.expect("Failed to parse request JSON");
println!("WebAuthn GetAssertion request: {:?}", get_assertion);

Expand Down
75 changes: 61 additions & 14 deletions libwebauthn/src/ops/webauthn/get_assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ use crate::{
HmacGetSecretInputJson, LargeBlobInputJson, PrfInputJson,
PublicKeyCredentialRequestOptionsJSON,
},
origin::is_registrable_domain_suffix_or_equal,
response::{
AuthenticationExtensionsClientOutputsJSON, AuthenticationResponseJSON,
AuthenticatorAssertionResponseJSON, HMACGetSecretOutputJSON, LargeBlobOutputJSON,
PRFOutputJSON, PRFValuesJSON, ResponseSerializationError, WebAuthnIDLResponse,
},
Base64UrlString, FromIdlModel, JsonError,
},
psl::PublicSuffixList,
Operation, WebAuthnIDL,
},
pin::PinUvAuthProtocol,
Expand Down Expand Up @@ -115,21 +117,25 @@ impl FromIdlModel<PublicKeyCredentialRequestOptionsJSON, GetAssertionRequestPars
{
fn from_idl_model(
request_origin: &RequestOrigin,
psl: &dyn PublicSuffixList,
inner: PublicKeyCredentialRequestOptionsJSON,
) -> Result<Self, GetAssertionRequestParsingError> {
let effective_rp_id = request_origin.origin.host.as_str();
if let Some(relying_party_id) = inner.relying_party_id.as_deref() {
let resolved_rp_id = if let Some(relying_party_id) = inner.relying_party_id.as_deref() {
let parsed = RelyingPartyId::try_from(relying_party_id).map_err(|err| {
GetAssertionRequestParsingError::InvalidRelyingPartyId(err.to_string())
})?;
// TODO(#160): Add support for related origin per WebAuthn Level 3.
if parsed.0 != effective_rp_id {
// TODO(#160): Add related-origins fallback per WebAuthn L3 §5.11.
if !is_registrable_domain_suffix_or_equal(&parsed.0, effective_rp_id, psl) {
return Err(GetAssertionRequestParsingError::MismatchingRelyingPartyId(
parsed.0,
effective_rp_id.to_string(),
));
}
}
parsed.0
} else {
effective_rp_id.to_string()
};

let prf = match inner.extensions.as_ref() {
Some(ext) => match &ext.prf {
Expand Down Expand Up @@ -158,7 +164,7 @@ impl FromIdlModel<PublicKeyCredentialRequestOptionsJSON, GetAssertionRequestPars
.unwrap_or(DEFAULT_TIMEOUT);

Ok(GetAssertionRequest {
relying_party_id: effective_rp_id.to_string(),
relying_party_id: resolved_rp_id,
challenge: inner.challenge.to_vec(),
origin: request_origin.origin.to_string(),
top_origin: request_origin.top_origin.as_ref().map(|o| o.to_string()),
Expand Down Expand Up @@ -575,6 +581,7 @@ mod tests {

use serde_bytes::ByteBuf;

use crate::ops::webauthn::psl::MockPublicSuffixList;
use crate::ops::webauthn::{GetAssertionRequest, RequestOrigin};
use crate::proto::ctap2::Ctap2PublicKeyCredentialType;

Expand Down Expand Up @@ -629,8 +636,12 @@ mod tests {
#[test]
fn test_request_from_json_base() {
let request_origin: RequestOrigin = "https://example.org".parse().unwrap();
let req: GetAssertionRequest =
GetAssertionRequest::from_json(&request_origin, REQUEST_BASE_JSON).unwrap();
let req: GetAssertionRequest = GetAssertionRequest::from_json(
&request_origin,
&MockPublicSuffixList,
REQUEST_BASE_JSON,
)
.unwrap();
assert_eq!(req, request_base());
}

Expand All @@ -640,7 +651,8 @@ mod tests {
let req_json = json_field_rm(REQUEST_BASE_JSON, "rpId");

let req: GetAssertionRequest =
GetAssertionRequest::from_json(&request_origin, &req_json).unwrap();
GetAssertionRequest::from_json(&request_origin, &MockPublicSuffixList, &req_json)
.unwrap();
assert_eq!(req, request_base());
}

Expand All @@ -649,7 +661,8 @@ mod tests {
let request_origin: RequestOrigin = "https://example.org".parse().unwrap();
let req_json = json_field_add(REQUEST_BASE_JSON, "rpId", r#""example.org.""#);

let result = GetAssertionRequest::from_json(&request_origin, &req_json);
let result =
GetAssertionRequest::from_json(&request_origin, &MockPublicSuffixList, &req_json);
assert!(matches!(
result,
Err(GetAssertionRequestParsingError::InvalidRelyingPartyId(_))
Expand All @@ -661,7 +674,37 @@ mod tests {
let request_origin: RequestOrigin = "https://example.org".parse().unwrap();
let req_json = json_field_add(REQUEST_BASE_JSON, "rpId", r#""other.example.org""#);

let result = GetAssertionRequest::from_json(&request_origin, &req_json);
let result =
GetAssertionRequest::from_json(&request_origin, &MockPublicSuffixList, &req_json);
assert!(matches!(
result,
Err(GetAssertionRequestParsingError::MismatchingRelyingPartyId(
_,
_
))
));
}

#[test]
fn test_request_from_json_rp_id_is_parent_registrable_suffix() {
// origin = login.example.org, rp.id = example.org -> accepted.
let request_origin: RequestOrigin = "https://login.example.org".parse().unwrap();
let req_json = json_field_add(REQUEST_BASE_JSON, "rpId", r#""example.org""#);

let req = GetAssertionRequest::from_json(&request_origin, &MockPublicSuffixList, &req_json)
.unwrap();
assert_eq!(req.relying_party_id, "example.org");
assert_eq!(req.origin, "https://login.example.org");
}

#[test]
fn test_request_from_json_rp_id_is_etld_rejected() {
// origin = example.co.uk, rp.id = co.uk (a public suffix) -> rejected.
let request_origin: RequestOrigin = "https://example.co.uk".parse().unwrap();
let req_json = json_field_add(REQUEST_BASE_JSON, "rpId", r#""co.uk""#);

let result =
GetAssertionRequest::from_json(&request_origin, &MockPublicSuffixList, &req_json);
assert!(matches!(
result,
Err(GetAssertionRequestParsingError::MismatchingRelyingPartyId(
Expand All @@ -677,7 +720,8 @@ mod tests {
let req_json = json_field_rm(REQUEST_BASE_JSON, "allowCredentials");

let req: GetAssertionRequest =
GetAssertionRequest::from_json(&request_origin, &req_json).unwrap();
GetAssertionRequest::from_json(&request_origin, &MockPublicSuffixList, &req_json)
.unwrap();
assert_eq!(
req,
GetAssertionRequest {
Expand All @@ -693,7 +737,8 @@ mod tests {
let req_json = json_field_rm(REQUEST_BASE_JSON, "timeout");

let req: GetAssertionRequest =
GetAssertionRequest::from_json(&request_origin, &req_json).unwrap();
GetAssertionRequest::from_json(&request_origin, &MockPublicSuffixList, &req_json)
.unwrap();
assert_eq!(req.timeout, DEFAULT_TIMEOUT);
}

Expand All @@ -706,7 +751,8 @@ mod tests {
let req_json = json_field_add(REQUEST_BASE_JSON, "extensions", r#"{}"#);

let req: GetAssertionRequest =
GetAssertionRequest::from_json(&request_origin, &req_json).unwrap();
GetAssertionRequest::from_json(&request_origin, &MockPublicSuffixList, &req_json)
.unwrap();
assert_eq!(
req.extensions,
Some(GetAssertionRequestExtensions::default())
Expand All @@ -724,7 +770,8 @@ mod tests {
);

let req: GetAssertionRequest =
GetAssertionRequest::from_json(&request_origin, &req_json).unwrap();
GetAssertionRequest::from_json(&request_origin, &MockPublicSuffixList, &req_json)
.unwrap();
if let Some(GetAssertionRequestExtensions {
prf:
Some(PrfInput {
Expand Down
16 changes: 13 additions & 3 deletions libwebauthn/src/ops/webauthn/idl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub use response::{

use origin::RequestOrigin;

use super::psl::PublicSuffixList;

use serde::de::DeserializeOwned;
use serde_json;

Expand All @@ -33,9 +35,13 @@ where
/// The JSON model that this IDL can deserialize from.
type IdlModel: DeserializeOwned;

fn from_json(request_origin: &RequestOrigin, json: &str) -> Result<Self, Self::Error> {
fn from_json(
request_origin: &RequestOrigin,
psl: &dyn PublicSuffixList,
json: &str,
) -> Result<Self, Self::Error> {
let idl_model: Self::IdlModel = serde_json::from_str(json)?;
Self::from_idl_model(request_origin, idl_model).map_err(From::from)
Self::from_idl_model(request_origin, psl, idl_model).map_err(From::from)
}
}

Expand All @@ -44,5 +50,9 @@ where
T: DeserializeOwned,
E: std::error::Error,
{
fn from_idl_model(request_origin: &RequestOrigin, model: T) -> Result<Self, E>;
fn from_idl_model(
request_origin: &RequestOrigin,
psl: &dyn PublicSuffixList,
model: T,
) -> Result<Self, E>;
}
Loading
Loading