diff --git a/blink/public/common/BUILD.gn b/blink/public/common/BUILD.gn index 13b0ac074e83..9dae69ef1a7f 100644 --- a/blink/public/common/BUILD.gn +++ b/blink/public/common/BUILD.gn @@ -182,6 +182,7 @@ source_set("headers") { "service_worker/service_worker_status_code.h", "service_worker/service_worker_type_converters.h", "service_worker/service_worker_types.h", + "sms/webotp_constants.h", "sms/webotp_service_destroyed_reason.h", "sms/webotp_service_outcome.h", "switches.h", diff --git a/blink/public/common/sms/webotp_constants.h b/blink/public/common/sms/webotp_constants.h new file mode 100644 index 000000000000..6ca275c3df75 --- /dev/null +++ b/blink/public/common/sms/webotp_constants.h @@ -0,0 +1,14 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef THIRD_PARTY_BLINK_PUBLIC_COMMON_SMS_WEBOTP_CONSTANTS_H_ +#define THIRD_PARTY_BLINK_PUBLIC_COMMON_SMS_WEBOTP_CONSTANTS_H_ + +namespace blink { + +static constexpr int kMaxUniqueOriginInAncestorChainForWebOTP = 2; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_PUBLIC_COMMON_SMS_WEBOTP_CONSTANTS_H_ diff --git a/blink/public/mojom/feature_policy/feature_policy_feature.mojom b/blink/public/mojom/feature_policy/feature_policy_feature.mojom index 121d0b608d00..6e481496f9eb 100644 --- a/blink/public/mojom/feature_policy/feature_policy_feature.mojom +++ b/blink/public/mojom/feature_policy/feature_policy_feature.mojom @@ -134,6 +134,9 @@ enum FeaturePolicyFeature { // Controls access to gamepads interface kGamepad = 79, + // Controls use of WebOTP API. + kOTPCredentials = 80, + // Don't change assigned numbers of any item, and don't reuse removed slots. // Add new features at the end of the enum. // Also, run update_feature_policy_enum.py in diff --git a/blink/public/platform/web_runtime_features.h b/blink/public/platform/web_runtime_features.h index a13c6fca843b..3bb8e4d51a0b 100644 --- a/blink/public/platform/web_runtime_features.h +++ b/blink/public/platform/web_runtime_features.h @@ -166,6 +166,7 @@ class WebRuntimeFeatures { BLINK_PLATFORM_EXPORT static void EnableWebGPU(bool); BLINK_PLATFORM_EXPORT static void EnableWebID(bool); BLINK_PLATFORM_EXPORT static void EnableWebNfc(bool); + BLINK_PLATFORM_EXPORT static void EnableWebOTPAssertionFeaturePolicy(bool); BLINK_PLATFORM_EXPORT static void EnableWebShare(bool); BLINK_PLATFORM_EXPORT static void EnableWebUsb(bool); BLINK_PLATFORM_EXPORT static void EnableWebXR(bool); diff --git a/blink/renderer/core/feature_policy/feature_policy_features.json5 b/blink/renderer/core/feature_policy/feature_policy_features.json5 index 5cb677862c63..47b8a5fa9cbb 100644 --- a/blink/renderer/core/feature_policy/feature_policy_features.json5 +++ b/blink/renderer/core/feature_policy/feature_policy_features.json5 @@ -235,6 +235,11 @@ feature_default: "EnableForAll", depends_on: ["FeaturePolicyForSandbox"], }, + { + name: "OTPCredentials", + feature_policy_name: "otp-credentials", + depends_on: ["WebOTPAssertionFeaturePolicy"], + }, { name: "OrientationLock", feature_policy_name: "orientation-lock", diff --git a/blink/renderer/modules/credentialmanager/credentials_container.cc b/blink/renderer/modules/credentialmanager/credentials_container.cc index 6866dd3e4231..401817e9044a 100644 --- a/blink/renderer/modules/credentialmanager/credentials_container.cc +++ b/blink/renderer/modules/credentialmanager/credentials_container.cc @@ -11,6 +11,7 @@ #include "base/numerics/safe_conversions.h" #include "base/rand_util.h" #include "build/build_config.h" +#include "third_party/blink/public/common/sms/webotp_constants.h" #include "third_party/blink/public/common/sms/webotp_service_outcome.h" #include "third_party/blink/public/mojom/credentialmanager/credential_manager.mojom-blink.h" #include "third_party/blink/public/mojom/feature_policy/feature_policy.mojom-blink.h" @@ -111,7 +112,9 @@ enum class RequiredOriginType { // expressed in various ways, e.g.: |allow| iframe attribute and/or // feature-policy header, and may be inherited from parent browsing // contexts. See Feature Policy spec. - kSecureAndPermittedByFeaturePolicy, + kSecureAndPermittedByWebAuthGetAssertionFeaturePolicy, + // Similar to the enum above, checks the "otp-credentials" feature policy. + kSecureAndPermittedByWebOTPAssertionFeaturePolicy, }; bool IsSameOriginWithAncestors(const Frame* frame) { @@ -128,6 +131,37 @@ bool IsSameOriginWithAncestors(const Frame* frame) { return true; } +// An ancestor chain is valid iff there are at most 2 unique origins on the +// chain (current origin included), the unique origins must be consecutive. +// e.g. the following are valid: +// A.com (calls WebOTP API) +// A.com -> A.com (calls WebOTP API) +// A.com -> A.com -> B.com (calls WebOTP API) +// A.com -> B.com -> B.com (calls WebOTP API) +// while the following are invalid: +// A.com -> B.com -> A.com (calls WebOTP API) +// A.com -> B.com -> C.com (calls WebOTP API) +// Note that there is additional requirement on feature permission being granted +// upon crossing origins but that is not verified by this function. +bool IsAncestorChainValidForWebOTP(const Frame* frame) { + const SecurityOrigin* current_origin = + frame->GetSecurityContext()->GetSecurityOrigin(); + int number_of_unique_origin = 1; + + const Frame* parent = frame->Tree().Parent(); + while (parent) { + auto* parent_origin = parent->GetSecurityContext()->GetSecurityOrigin(); + if (!parent_origin->IsSameOriginWith(current_origin)) { + ++number_of_unique_origin; + current_origin = parent_origin; + } + if (number_of_unique_origin > kMaxUniqueOriginInAncestorChainForWebOTP) + return false; + parent = parent->Tree().Parent(); + } + return true; +} + bool CheckSecurityRequirementsBeforeRequest( ScriptPromiseResolver* resolver, RequiredOriginType required_origin_type) { @@ -163,7 +197,8 @@ bool CheckSecurityRequirementsBeforeRequest( } break; - case RequiredOriginType::kSecureAndPermittedByFeaturePolicy: + case RequiredOriginType:: + kSecureAndPermittedByWebAuthGetAssertionFeaturePolicy: // The 'publickey-credentials-get' feature's "default allowlist" is // "self", which means the webauthn feature is allowed by default in // same-origin child browsing contexts. @@ -172,7 +207,7 @@ bool CheckSecurityRequirementsBeforeRequest( resolver->Reject(MakeGarbageCollected( DOMExceptionCode::kNotAllowedError, "The 'publickey-credentials-get' feature is not enabled in this " - "document. Feature Policy may be used to delegate Web " + "document. Permissions Policy may be used to delegate Web " "Authentication capabilities to cross-origin child frames.")); return false; } else { @@ -181,6 +216,22 @@ bool CheckSecurityRequirementsBeforeRequest( WebFeature::kCredentialManagerCrossOriginPublicKeyGetRequest); } break; + + case RequiredOriginType::kSecureAndPermittedByWebOTPAssertionFeaturePolicy: + if (!resolver->GetExecutionContext()->IsFeatureEnabled( + mojom::blink::FeaturePolicyFeature::kOTPCredentials)) { + resolver->Reject(MakeGarbageCollected( + DOMExceptionCode::kNotAllowedError, + "The 'otp-credentials` feature is not enabled in this document.")); + return false; + } + if (!IsAncestorChainValidForWebOTP(resolver->DomWindow()->GetFrame())) { + resolver->Reject(MakeGarbageCollected( + DOMExceptionCode::kNotAllowedError, + "More than two unique origins are detected in the origin chain.")); + return false; + } + break; } return true; @@ -208,10 +259,18 @@ void AssertSecurityRequirementsBeforeResponse( IsSameOriginWithAncestors(resolver->DomWindow()->GetFrame())); break; - case RequiredOriginType::kSecureAndPermittedByFeaturePolicy: + case RequiredOriginType:: + kSecureAndPermittedByWebAuthGetAssertionFeaturePolicy: SECURITY_CHECK(resolver->GetExecutionContext()->IsFeatureEnabled( mojom::blink::FeaturePolicyFeature::kPublicKeyCredentialsGet)); break; + + case RequiredOriginType::kSecureAndPermittedByWebOTPAssertionFeaturePolicy: + SECURITY_CHECK( + resolver->GetExecutionContext()->IsFeatureEnabled( + mojom::blink::FeaturePolicyFeature::kOTPCredentials) && + IsAncestorChainValidForWebOTP(resolver->DomWindow()->GetFrame())); + break; } } @@ -579,7 +638,11 @@ void OnSmsReceive(ScriptPromiseResolver* resolver, mojom::blink::SmsStatus status, const WTF::String& otp) { AssertSecurityRequirementsBeforeResponse( - resolver, RequiredOriginType::kSecureAndSameWithAncestors); + resolver, resolver->GetExecutionContext()->IsFeatureEnabled( + mojom::blink::FeaturePolicyFeature::kOTPCredentials) + ? RequiredOriginType:: + kSecureAndPermittedByWebOTPAssertionFeaturePolicy + : RequiredOriginType::kSecureAndSameWithAncestors); auto& window = *LocalDOMWindow::From(resolver->GetScriptState()); ukm::SourceId source_id = window.UkmSourceID(); ukm::UkmRecorder* recorder = window.UkmRecorder(); @@ -857,13 +920,18 @@ ScriptPromise CredentialsContainer::get( auto* resolver = MakeGarbageCollected(script_state); ScriptPromise promise = resolver->Promise(); + auto required_origin_type = RequiredOriginType::kSecureAndSameWithAncestors; // hasPublicKey() implies that this is a WebAuthn request. - auto required_origin_type = - options->hasPublicKey() && - RuntimeEnabledFeatures:: - WebAuthenticationGetAssertionFeaturePolicyEnabled() - ? RequiredOriginType::kSecureAndPermittedByFeaturePolicy - : RequiredOriginType::kSecureAndSameWithAncestors; + if (options->hasPublicKey() && + RuntimeEnabledFeatures:: + WebAuthenticationGetAssertionFeaturePolicyEnabled()) { + required_origin_type = RequiredOriginType:: + kSecureAndPermittedByWebAuthGetAssertionFeaturePolicy; + } else if (options->hasOtp() && + RuntimeEnabledFeatures::WebOTPAssertionFeaturePolicyEnabled()) { + required_origin_type = + RequiredOriginType::kSecureAndPermittedByWebOTPAssertionFeaturePolicy; + } if (!CheckSecurityRequirementsBeforeRequest(resolver, required_origin_type)) { return promise; } @@ -996,11 +1064,6 @@ ScriptPromise CredentialsContainer::get( WTF::Bind(&AbortOtpRequest, WrapPersistent(script_state))); } - if (!CheckSecurityRequirementsBeforeRequest( - resolver, RequiredOriginType::kSecureAndSameWithAncestors)) { - return promise; - } - auto* webotp_service = CredentialManagerProxy::From(script_state)->WebOTPService(); webotp_service->Receive(WTF::Bind(&OnSmsReceive, WrapPersistent(resolver), diff --git a/blink/renderer/platform/exported/web_runtime_features.cc b/blink/renderer/platform/exported/web_runtime_features.cc index 6a4ba3816ec6..63e191f570b3 100644 --- a/blink/renderer/platform/exported/web_runtime_features.cc +++ b/blink/renderer/platform/exported/web_runtime_features.cc @@ -489,6 +489,10 @@ void WebRuntimeFeatures::EnableWebAuthenticationGetAssertionFeaturePolicy( enable); } +void WebRuntimeFeatures::EnableWebOTPAssertionFeaturePolicy(bool enable) { + RuntimeEnabledFeatures::SetWebOTPAssertionFeaturePolicyEnabled(enable); +} + void WebRuntimeFeatures::EnableLazyInitializeMediaControls(bool enable) { RuntimeEnabledFeatures::SetLazyInitializeMediaControlsEnabled(enable); } diff --git a/blink/renderer/platform/runtime_enabled_features.json5 b/blink/renderer/platform/runtime_enabled_features.json5 index 27390024057f..f35fb4c773cf 100644 --- a/blink/renderer/platform/runtime_enabled_features.json5 +++ b/blink/renderer/platform/runtime_enabled_features.json5 @@ -2184,6 +2184,11 @@ name: "WebOTP", status: {"default": "experimental", "Android": "stable"}, }, + { + name: "WebOTPAssertionFeaturePolicy", + status: "experimental", + depends_on: ["WebOTP"], + }, { name: "WebScheduler", origin_trial_feature_name: "WebScheduler", diff --git a/blink/web_tests/external/wpt/credential-management/otpcredential-iframe.https.html b/blink/web_tests/external/wpt/credential-management/otpcredential-iframe.https.html index 8af17b599612..da3e572b6b57 100644 --- a/blink/web_tests/external/wpt/credential-management/otpcredential-iframe.https.html +++ b/blink/web_tests/external/wpt/credential-management/otpcredential-iframe.https.html @@ -27,6 +27,19 @@ }, "Test OTPCredential enabled in same origin iframes"); +promise_test(async t => { + const messageWatcher = new EventWatcher(t, window, "message"); + var iframe = document.createElement("iframe"); + iframe.src = remoteBaseURL + "support/otpcredential-iframe.html" + iframe.allow = "otp-credentials"; + document.body.appendChild(iframe); + + const message = await messageWatcher.wait_for("message"); + assert_equals(message.data.result, "Pass"); + assert_equals(message.data.code, "ABC123"); + +}, "OTPCredential enabled in cross origin iframes with permissions policy"); + promise_test(async t => { const messageWatcher = new EventWatcher(t, window, "message"); var iframe = document.createElement("iframe"); @@ -37,5 +50,5 @@ assert_equals(message.data.result, "Fail"); assert_equals(message.data.errorType, "NotAllowedError"); -}, "Test OTPCredential disabled in cross origin iframes"); +}, "OTPCredential disabled in cross origin iframes without permissions policy"); diff --git a/blink/web_tests/webexposed/feature-policy-features-expected.txt b/blink/web_tests/webexposed/feature-policy-features-expected.txt index 9c59521c026f..2d95e7238953 100644 --- a/blink/web_tests/webexposed/feature-policy-features-expected.txt +++ b/blink/web_tests/webexposed/feature-policy-features-expected.txt @@ -41,6 +41,7 @@ microphone midi modals orientation-lock +otp-credentials payment picture-in-picture pointer-lock