Bug 1810851 - add the authenticator attachment field to PublicKeyCredential. r=keeler,webidl,smaug

Differential Revision: https://phabricator.services.mozilla.com/D189397
This commit is contained in:
John Schanck 2023-10-07 20:09:06 +00:00
Родитель 931903c87a
Коммит 5553a8b85d
14 изменённых файлов: 163 добавлений и 47 удалений

Просмотреть файл

@ -109,6 +109,7 @@ struct WebAuthnMakeCredentialResult {
uint8_t[] KeyHandle;
nsString[] Transports;
WebAuthnExtensionResult[] Extensions;
nsString? AuthenticatorAttachment;
};
struct WebAuthnGetAssertionInfo {
@ -130,6 +131,7 @@ struct WebAuthnGetAssertionResult {
uint8_t[] AuthenticatorData;
WebAuthnExtensionResult[] Extensions;
uint8_t[] UserHandle;
nsString? AuthenticatorAttachment;
};
[ManualDealloc]

Просмотреть файл

@ -75,6 +75,15 @@ void PublicKeyCredential::GetRawId(JSContext* aCx,
aValue.set(mRawIdCachedObj);
}
void PublicKeyCredential::GetAuthenticatorAttachment(
DOMString& aAuthenticatorAttachment) {
if (mAuthenticatorAttachment.isSome()) {
aAuthenticatorAttachment.SetKnownLiveString(mAuthenticatorAttachment.ref());
} else {
aAuthenticatorAttachment.SetNull();
}
}
already_AddRefed<AuthenticatorResponse> PublicKeyCredential::Response() const {
if (mAttestationResponse) {
return do_AddRef(mAttestationResponse);
@ -89,6 +98,11 @@ void PublicKeyCredential::SetRawId(const nsTArray<uint8_t>& aBuffer) {
mRawId.Assign(aBuffer);
}
void PublicKeyCredential::SetAuthenticatorAttachment(
const Maybe<nsString>& aAuthenticatorAttachment) {
mAuthenticatorAttachment = aAuthenticatorAttachment;
}
void PublicKeyCredential::SetAttestationResponse(
const RefPtr<AuthenticatorAttestationResponse>& aAttestationResponse) {
mAttestationResponse = aAttestationResponse;
@ -200,7 +214,10 @@ void PublicKeyCredential::ToJSON(JSContext* aCx,
if (aError.Failed()) {
return;
}
// TODO(bug 1810851): authenticatorAttachment
if (mAuthenticatorAttachment.isSome()) {
json.mAuthenticatorAttachment.Construct();
json.mAuthenticatorAttachment.Value() = mAuthenticatorAttachment.ref();
}
if (mClientExtensionOutputs.mCredProps.WasPassed()) {
json.mClientExtensionResults.mCredProps.Construct(
mClientExtensionOutputs.mCredProps.Value());
@ -222,7 +239,10 @@ void PublicKeyCredential::ToJSON(JSContext* aCx,
if (aError.Failed()) {
return;
}
// TODO(bug 1810851): authenticatorAttachment
if (mAuthenticatorAttachment.isSome()) {
json.mAuthenticatorAttachment.Construct();
json.mAuthenticatorAttachment.Value() = mAuthenticatorAttachment.ref();
}
if (mClientExtensionOutputs.mAppid.WasPassed()) {
json.mClientExtensionResults.mAppid.Construct(
mClientExtensionOutputs.mAppid.Value());

Просмотреть файл

@ -36,10 +36,15 @@ class PublicKeyCredential final : public Credential {
void GetRawId(JSContext* aCx, JS::MutableHandle<JSObject*> aValue,
ErrorResult& aRv);
void GetAuthenticatorAttachment(DOMString& aAuthenticatorAttachment);
already_AddRefed<AuthenticatorResponse> Response() const;
void SetRawId(const nsTArray<uint8_t>& aBuffer);
void SetAuthenticatorAttachment(
const Maybe<nsString>& aAuthenticatorAttachment);
void SetAttestationResponse(
const RefPtr<AuthenticatorAttestationResponse>& aAttestationResponse);
void SetAssertionResponse(
@ -80,6 +85,7 @@ class PublicKeyCredential final : public Credential {
private:
nsTArray<uint8_t> mRawId;
JS::Heap<JSObject*> mRawIdCachedObj;
Maybe<nsString> mAuthenticatorAttachment;
RefPtr<AuthenticatorAttestationResponse> mAttestationResponse;
RefPtr<AuthenticatorAssertionResponse> mAssertionResponse;
AuthenticationExtensionsClientOutputs mClientExtensionOutputs;

Просмотреть файл

@ -744,6 +744,7 @@ void WebAuthnManager::FinishMakeCredential(
credential->SetType(u"public-key"_ns);
credential->SetRawId(aResult.KeyHandle());
credential->SetAttestationResponse(attestation);
credential->SetAuthenticatorAttachment(aResult.AuthenticatorAttachment());
// Forward client extension results.
for (const auto& ext : aResult.Extensions()) {
@ -797,6 +798,7 @@ void WebAuthnManager::FinishGetAssertion(
credential->SetType(u"public-key"_ns);
credential->SetRawId(aResult.KeyHandle());
credential->SetAssertionResponse(assertion);
credential->SetAuthenticatorAttachment(aResult.AuthenticatorAttachment());
// Forward client extension results.
for (const auto& ext : aResult.Extensions()) {

Просмотреть файл

@ -71,6 +71,16 @@ WebAuthnRegisterResult::SetCredPropsRk(bool aCredPropsRk) {
return NS_OK;
}
NS_IMETHODIMP
WebAuthnRegisterResult::GetAuthenticatorAttachment(
nsAString& aAuthenticatorAttachment) {
if (mAuthenticatorAttachment.isSome()) {
aAuthenticatorAttachment = mAuthenticatorAttachment.ref();
return NS_OK;
}
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMPL_ISUPPORTS(WebAuthnSignResult, nsIWebAuthnSignResult)
NS_IMETHODIMP
@ -108,4 +118,14 @@ WebAuthnSignResult::GetUsedAppId(bool* aUsedAppId) {
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP
WebAuthnSignResult::GetAuthenticatorAttachment(
nsAString& aAuthenticatorAttachment) {
if (mAuthenticatorAttachment.isSome()) {
aAuthenticatorAttachment = mAuthenticatorAttachment.ref();
return NS_OK;
}
return NS_ERROR_NOT_AVAILABLE;
}
} // namespace mozilla::dom

Просмотреть файл

@ -21,8 +21,11 @@ class WebAuthnRegisterResult final : public nsIWebAuthnRegisterResult {
WebAuthnRegisterResult(const nsTArray<uint8_t>& aAttestationObject,
const nsCString& aClientDataJSON,
const nsTArray<uint8_t>& aCredentialId,
const nsTArray<nsString>& aTransports)
: mClientDataJSON(aClientDataJSON), mCredPropsRk(Nothing()) {
const nsTArray<nsString>& aTransports,
const Maybe<nsString>& aAuthenticatorAttachment)
: mClientDataJSON(aClientDataJSON),
mCredPropsRk(Nothing()),
mAuthenticatorAttachment(aAuthenticatorAttachment) {
mAttestationObject.AppendElements(aAttestationObject);
mCredentialId.AppendElements(aCredentialId);
mTransports.AppendElements(aTransports);
@ -49,6 +52,8 @@ class WebAuthnRegisterResult final : public nsIWebAuthnRegisterResult {
mTransports.AppendElement(
jni::String::LocalRef(transports->GetElement(i))->ToString());
}
// authenticator attachment is not available on Android
mAuthenticatorAttachment = Nothing();
}
#endif
@ -60,6 +65,7 @@ class WebAuthnRegisterResult final : public nsIWebAuthnRegisterResult {
nsTArray<nsString> mTransports;
nsCString mClientDataJSON;
Maybe<bool> mCredPropsRk;
Maybe<nsString> mAuthenticatorAttachment;
};
class WebAuthnSignResult final : public nsIWebAuthnSignResult {
@ -71,8 +77,10 @@ class WebAuthnSignResult final : public nsIWebAuthnSignResult {
const nsCString& aClientDataJSON,
const nsTArray<uint8_t>& aCredentialId,
const nsTArray<uint8_t>& aSignature,
const nsTArray<uint8_t>& aUserHandle)
: mClientDataJSON(aClientDataJSON) {
const nsTArray<uint8_t>& aUserHandle,
const Maybe<nsString>& aAuthenticatorAttachment)
: mClientDataJSON(aClientDataJSON),
mAuthenticatorAttachment(aAuthenticatorAttachment) {
mAuthenticatorData.AppendElements(aAuthenticatorData);
mCredentialId.AppendElements(aCredentialId);
mSignature.AppendElements(aSignature);
@ -103,6 +111,8 @@ class WebAuthnSignResult final : public nsIWebAuthnSignResult {
reinterpret_cast<uint8_t*>(
aResponse->UserHandle()->GetElements().Elements()),
aResponse->UserHandle()->Length());
// authenticator attachment is not available on Android
mAuthenticatorAttachment = Nothing();
}
#endif
@ -114,6 +124,7 @@ class WebAuthnSignResult final : public nsIWebAuthnSignResult {
nsTArray<uint8_t> mCredentialId;
nsTArray<uint8_t> mSignature;
nsTArray<uint8_t> mUserHandle;
Maybe<nsString> mAuthenticatorAttachment;
};
} // namespace mozilla::dom

Просмотреть файл

@ -86,6 +86,22 @@ mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestRegister(
return;
}
Maybe<nsString> authenticatorAttachment;
nsString maybeAuthenticatorAttachment;
rv = aValue->GetAuthenticatorAttachment(
maybeAuthenticatorAttachment);
if (rv != NS_ERROR_NOT_AVAILABLE) {
if (NS_WARN_IF(NS_FAILED(rv))) {
Telemetry::ScalarAdd(
Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
u"CTAPRegisterAbort"_ns, 1);
Unused << parent->SendAbort(aTransactionId,
NS_ERROR_DOM_NOT_ALLOWED_ERR);
return;
}
authenticatorAttachment = Some(maybeAuthenticatorAttachment);
}
nsTArray<WebAuthnExtensionResult> extensions;
bool credPropsRk;
rv = aValue->GetCredPropsRk(&credPropsRk);
@ -103,7 +119,8 @@ mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestRegister(
}
WebAuthnMakeCredentialResult result(
clientData, attObj, credentialId, transports, extensions);
clientData, attObj, credentialId, transports, extensions,
authenticatorAttachment);
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
u"CTAPRegisterFinish"_ns, 1);
@ -202,6 +219,22 @@ mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestSign(
nsTArray<uint8_t> userHandle;
Unused << aValue->GetUserHandle(userHandle); // optional
Maybe<nsString> authenticatorAttachment;
nsString maybeAuthenticatorAttachment;
rv = aValue->GetAuthenticatorAttachment(
maybeAuthenticatorAttachment);
if (rv != NS_ERROR_NOT_AVAILABLE) {
if (NS_WARN_IF(NS_FAILED(rv))) {
Telemetry::ScalarAdd(
Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
u"CTAPSignAbort"_ns, 1);
Unused << parent->SendAbort(aTransactionId,
NS_ERROR_DOM_NOT_ALLOWED_ERR);
return;
}
authenticatorAttachment = Some(maybeAuthenticatorAttachment);
}
nsTArray<WebAuthnExtensionResult> extensions;
bool usedAppId;
rv = aValue->GetUsedAppId(&usedAppId);
@ -217,9 +250,9 @@ mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestSign(
extensions.AppendElement(WebAuthnExtensionResultAppId(usedAppId));
}
WebAuthnGetAssertionResult result(clientData, credentialId,
signature, authenticatorData,
extensions, userHandle);
WebAuthnGetAssertionResult result(
clientData, credentialId, signature, authenticatorData,
extensions, userHandle, authenticatorAttachment);
Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
u"CTAPSignFinish"_ns, 1);

Просмотреть файл

@ -543,8 +543,20 @@ void WinWebAuthnManager::Register(
}
}
Maybe<nsString> authenticatorAttachment;
if (pWebAuthNCredentialAttestation->dwVersion >=
WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_3) {
if (pWebAuthNCredentialAttestation->dwUsedTransport &
WEBAUTHN_CTAP_TRANSPORT_INTERNAL) {
authenticatorAttachment = Some(u"platform"_ns);
} else {
authenticatorAttachment = Some(u"cross-platform"_ns);
}
}
WebAuthnMakeCredentialResult result(aInfo.ClientDataJSON(), attObject,
credentialId, transports, extensions);
credentialId, transports, extensions,
authenticatorAttachment);
Unused << mTransactionParent->SendConfirmRegister(aTransactionId, result);
ClearTransaction();
@ -735,9 +747,11 @@ void WinWebAuthnManager::Sign(PWebAuthnTransactionParent* aTransactionParent,
extensions.AppendElement(WebAuthnExtensionResultAppId(true));
}
Maybe<nsString> authenticatorAttachment = Nothing(); // not available
WebAuthnGetAssertionResult result(aInfo.ClientDataJSON(), keyHandle,
signature, authenticatorData, extensions,
userHandle);
userHandle, authenticatorAttachment);
Unused << mTransactionParent->SendConfirmSign(aTransactionId, result);
ClearTransaction();

Просмотреть файл

@ -13,9 +13,10 @@ use authenticator::{
ctap2::attestation::AttestationObject,
ctap2::commands::get_info::AuthenticatorVersion,
ctap2::server::{
AuthenticationExtensionsClientInputs, PublicKeyCredentialDescriptor,
PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty,
ResidentKeyRequirement, UserVerificationRequirement,
AuthenticationExtensionsClientInputs, AuthenticatorAttachment,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters,
PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement,
UserVerificationRequirement,
},
errors::AuthenticatorError,
statecallback::StateCallback,
@ -29,7 +30,7 @@ use nserror::{
NS_ERROR_FAILURE, NS_ERROR_INVALID_ARG, NS_ERROR_NOT_AVAILABLE, NS_ERROR_NOT_IMPLEMENTED,
NS_ERROR_NULL_POINTER, NS_OK,
};
use nsstring::{nsACString, nsCString, nsString};
use nsstring::{nsACString, nsAString, nsCString, nsString};
use serde::Serialize;
use serde_cbor;
use serde_json::json;
@ -160,10 +161,16 @@ impl WebAuthnRegisterResult {
xpcom_method!(get_transports => GetTransports() -> ThinVec<nsString>);
fn get_transports(&self) -> Result<ThinVec<nsString>, nsresult> {
// The list that we return here might be included in a future GetAssertion request as a
// hint as to which transports to try. We currently only support the USB transport. If
// that changes, we will need a mechanism to track which transport was used for a
// request.
Ok(thin_vec![nsString::from("usb")])
// hint as to which transports to try. In production, we only support the "usb" transport.
// In tests, the result is not very important, but we can at least return "internal" if
// we're simulating platform attachment.
if static_prefs::pref!("security.webauth.webauthn_enable_softtoken")
&& self.result.attachment == AuthenticatorAttachment::Platform
{
Ok(thin_vec![nsString::from("internal")])
} else {
Ok(thin_vec![nsString::from("usb")])
}
}
xpcom_method!(get_cred_props_rk => GetCredPropsRk() -> bool);
@ -178,6 +185,15 @@ impl WebAuthnRegisterResult {
fn set_cred_props_rk(&self, _cred_props_rk: bool) -> Result<(), nsresult> {
Err(NS_ERROR_NOT_IMPLEMENTED)
}
xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString);
fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> {
match self.result.attachment {
AuthenticatorAttachment::CrossPlatform => Ok(nsString::from("cross-platform")),
AuthenticatorAttachment::Platform => Ok(nsString::from("platform")),
AuthenticatorAttachment::Unknown => Err(NS_ERROR_NOT_AVAILABLE),
}
}
}
#[xpcom(implement(nsIWebAuthnAttObj), atomic)]
@ -264,6 +280,15 @@ impl WebAuthnSignResult {
Ok(nsCString::from(name))
}
xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString);
fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> {
match self.result.attachment {
AuthenticatorAttachment::CrossPlatform => Ok(nsString::from("cross-platform")),
AuthenticatorAttachment::Platform => Ok(nsString::from("platform")),
AuthenticatorAttachment::Unknown => Err(NS_ERROR_NOT_AVAILABLE),
}
}
xpcom_method!(get_used_app_id => GetUsedAppId() -> bool);
fn get_used_app_id(&self) -> Result<bool, nsresult> {
self.result.extensions.app_id.ok_or(NS_ERROR_NOT_AVAILABLE)

Просмотреть файл

@ -25,6 +25,8 @@ interface nsIWebAuthnRegisterResult : nsISupports {
// readonly attribute bool hmacCreateSecret;
[must_use] attribute bool credPropsRk;
[must_use] readonly attribute AString authenticatorAttachment;
};
// The nsIWebAuthnSignResult interface is used to construct IPDL-defined
@ -52,4 +54,6 @@ interface nsIWebAuthnSignResult : nsISupports {
// appId field of AuthenticationExtensionsClientOutputs (Optional)
[must_use] readonly attribute bool usedAppId;
[must_use] readonly attribute AString authenticatorAttachment;
};

Просмотреть файл

@ -252,7 +252,7 @@
};
let registrationResponse = await navigator.credentials.create({publicKey});
let registrationResponseJSON = registrationResponse.toJSON();
is(Object.keys(registrationResponseJSON).length, 5, "registrationResponseJSON should have 5 properties");
is(Object.keys(registrationResponseJSON).length, 6, "registrationResponseJSON should have 6 properties");
is(registrationResponseJSON.id, registrationResponseJSON.rawId, "registrationResponseJSON.id and rawId should be the same");
ok(isUrlsafeBase64(registrationResponseJSON.id), "registrationResponseJSON.id should be urlsafe base64");
is(Object.keys(registrationResponseJSON.response).length, 6, "registrationResponseJSON.response should have 6 properties");
@ -262,7 +262,8 @@
ok(isUrlsafeBase64(registrationResponseJSON.response.attestationObject), "registrationResponseJSON.response.attestationObject should be urlsafe base64");
is(registrationResponseJSON.response.publicKeyAlgorithm, cose_alg_ECDSA_w_SHA256, "registrationResponseJSON.response.publicKeyAlgorithm should be ECDSA with SHA256 (COSE)");
is(registrationResponseJSON.response.transports.length, 1, "registrationResponseJSON.response.transports.length should be 1");
is(registrationResponseJSON.response.transports[0], "usb", "registrationResponseJSON.response.transports[0] should be usb");
is(registrationResponseJSON.response.transports[0], "internal", "registrationResponseJSON.response.transports[0] should be internal");
is(registrationResponseJSON.authenticatorAttachment, "platform", "registrationResponseJSON.authenticatorAttachment should be platform");
is(registrationResponseJSON.clientExtensionResults?.credProps?.rk, false, "registrationResponseJSON.clientExtensionResults.credProps.rk should be false");
is(registrationResponseJSON.type, "public-key", "registrationResponseJSON.type should be public-key");
});
@ -286,13 +287,14 @@
};
let assertionResponse = await navigator.credentials.get(assertionRequest);
let assertionResponseJSON = assertionResponse.toJSON();
is(Object.keys(assertionResponseJSON).length, 5, "assertionResponseJSON should have 5 properties");
is(Object.keys(assertionResponseJSON).length, 6, "assertionResponseJSON should have 6 properties");
is(assertionResponseJSON.id, assertionResponseJSON.rawId, "assertionResponseJSON.id and rawId should be the same");
ok(isUrlsafeBase64(assertionResponseJSON.id), "assertionResponseJSON.id should be urlsafe base64");
is(Object.keys(assertionResponseJSON.response).length, 3, "assertionResponseJSON.response should have 3 properties");
ok(isUrlsafeBase64(assertionResponseJSON.response.clientDataJSON), "assertionResponseJSON.response.clientDataJSON should be urlsafe base64");
ok(isUrlsafeBase64(assertionResponseJSON.response.authenticatorData), "assertionResponseJSON.response.authenticatorData should be urlsafe base64");
ok(isUrlsafeBase64(assertionResponseJSON.response.signature), "assertionResponseJSON.response.signature should be urlsafe base64");
is(assertionResponseJSON.authenticatorAttachment, "platform", "assertionResponseJSON.authenticatorAttachment should be platform");
is(Object.keys(assertionResponseJSON.clientExtensionResults).length, 0, "assertionResponseJSON.clientExtensionResults should be an empty dictionary");
is(assertionResponseJSON.type, "public-key", "assertionResponseJSON.type should be public-key");
});

Просмотреть файл

@ -14,6 +14,7 @@
interface PublicKeyCredential : Credential {
[SameObject, Throws] readonly attribute ArrayBuffer rawId;
[SameObject] readonly attribute AuthenticatorResponse response;
readonly attribute DOMString? authenticatorAttachment;
AuthenticationExtensionsClientOutputs getClientExtensionResults();
[NewObject] static Promise<boolean> isConditionalMediationAvailable();
[Throws] object toJSON();

Просмотреть файл

@ -1,12 +0,0 @@
[createcredential-attachment.https.html]
[navigator.credentials.create() with usb authenticator, attachment as cross-platform]
expected: FAIL
[navigator.credentials.create() with ble authenticator, attachment as cross-platform]
expected: FAIL
[navigator.credentials.create() with nfc authenticator, attachment as cross-platform]
expected: FAIL
[navigator.credentials.create() with internal authenticator, attachment as platform]
expected: FAIL

Просмотреть файл

@ -1,12 +0,0 @@
[getcredential-attachment.https.html]
[navigator.credentials.get() with usb authenticator, attachment as cross-platform]
expected: FAIL
[navigator.credentials.get() with ble authenticator, attachment as cross-platform]
expected: FAIL
[navigator.credentials.get() with nfc authenticator, attachment as cross-platform]
expected: FAIL
[navigator.credentials.get() with internal authenticator, attachment as platform]
expected: FAIL