Commit 977c0047 authored by Martin Kreichgauer's avatar Martin Kreichgauer Committed by Commit Bot

fido: implement IsUVPAA for Windows

This wires the IsUserVerifyingPlatformAuthenticator API call up to its
implementation in the native Windows API, where available.

Bug: 898718
Change-Id: I40307ff39f8dc8e02197debc48041aad62d253ac
Reviewed-on: https://chromium-review.googlesource.com/c/1351900
Commit-Queue: Adam Langley <agl@chromium.org>
Reviewed-by: default avatarAdam Langley <agl@chromium.org>
Cr-Commit-Position: refs/heads/master@{#611948}
parent 99ffaaeb
...@@ -61,6 +61,10 @@ ...@@ -61,6 +61,10 @@
#include "device/fido/mac/authenticator.h" #include "device/fido/mac/authenticator.h"
#endif #endif
#if defined(OS_WIN)
#include "device/fido/win/authenticator.h"
#endif
namespace content { namespace content {
namespace client_data { namespace client_data {
...@@ -232,7 +236,8 @@ bool IsAppIdAllowedForOrigin(const GURL& appid, const url::Origin& origin) { ...@@ -232,7 +236,8 @@ bool IsAppIdAllowedForOrigin(const GURL& appid, const url::Origin& origin) {
device::CtapMakeCredentialRequest CreateCtapMakeCredentialRequest( device::CtapMakeCredentialRequest CreateCtapMakeCredentialRequest(
const std::string& client_data_json, const std::string& client_data_json,
const blink::mojom::PublicKeyCredentialCreationOptionsPtr& options, const blink::mojom::PublicKeyCredentialCreationOptionsPtr& options,
bool is_individual_attestation) { bool is_individual_attestation,
bool is_incognito) {
auto credential_params = mojo::ConvertTo< auto credential_params = mojo::ConvertTo<
std::vector<device::PublicKeyCredentialParams::CredentialInfo>>( std::vector<device::PublicKeyCredentialParams::CredentialInfo>>(
options->public_key_parameters); options->public_key_parameters);
...@@ -251,6 +256,7 @@ device::CtapMakeCredentialRequest CreateCtapMakeCredentialRequest( ...@@ -251,6 +256,7 @@ device::CtapMakeCredentialRequest CreateCtapMakeCredentialRequest(
make_credential_param.SetExcludeList(std::move(exclude_list)); make_credential_param.SetExcludeList(std::move(exclude_list));
make_credential_param.SetIsIndividualAttestation(is_individual_attestation); make_credential_param.SetIsIndividualAttestation(is_individual_attestation);
make_credential_param.SetHmacSecret(options->hmac_create_secret); make_credential_param.SetHmacSecret(options->hmac_create_secret);
make_credential_param.set_is_incognito_mode(is_incognito);
return make_credential_param; return make_credential_param;
} }
...@@ -258,7 +264,8 @@ device::CtapGetAssertionRequest CreateCtapGetAssertionRequest( ...@@ -258,7 +264,8 @@ device::CtapGetAssertionRequest CreateCtapGetAssertionRequest(
const std::string& client_data_json, const std::string& client_data_json,
const blink::mojom::PublicKeyCredentialRequestOptionsPtr& options, const blink::mojom::PublicKeyCredentialRequestOptionsPtr& options,
base::Optional<base::span<const uint8_t, device::kRpIdHashLength>> base::Optional<base::span<const uint8_t, device::kRpIdHashLength>>
alternative_application_parameter) { alternative_application_parameter,
bool is_incognito) {
device::CtapGetAssertionRequest request_parameter(options->relying_party_id, device::CtapGetAssertionRequest request_parameter(options->relying_party_id,
client_data_json); client_data_json);
...@@ -281,6 +288,7 @@ device::CtapGetAssertionRequest CreateCtapGetAssertionRequest( ...@@ -281,6 +288,7 @@ device::CtapGetAssertionRequest CreateCtapGetAssertionRequest(
mojo::ConvertTo<std::vector<device::CableDiscoveryData>>( mojo::ConvertTo<std::vector<device::CableDiscoveryData>>(
options->cable_authentication_data)); options->cable_authentication_data));
} }
request_parameter.set_is_incognito_mode(is_incognito);
return request_parameter; return request_parameter;
} }
...@@ -482,12 +490,11 @@ base::flat_set<device::FidoTransportProtocol> GetTransportsEnabledByFlags() { ...@@ -482,12 +490,11 @@ base::flat_set<device::FidoTransportProtocol> GetTransportsEnabledByFlags() {
transports.insert(device::FidoTransportProtocol::kBluetoothLowEnergy); transports.insert(device::FidoTransportProtocol::kBluetoothLowEnergy);
} }
if (
#if defined(OS_WIN) #if defined(OS_WIN)
if (base::FeatureList::IsEnabled(features::kWebAuthCable) && base::FeatureList::IsEnabled(features::kWebAuthCableWin) &&
base::FeatureList::IsEnabled(features::kWebAuthCableWin)) { #endif
#else base::FeatureList::IsEnabled(features::kWebAuthCable)) {
if (base::FeatureList::IsEnabled(features::kWebAuthCable)) {
#endif // defined(OS_WIN)
transports.insert( transports.insert(
device::FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy); device::FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy);
} }
...@@ -669,7 +676,8 @@ void AuthenticatorImpl::MakeCredential( ...@@ -669,7 +676,8 @@ void AuthenticatorImpl::MakeCredential(
: device::AuthenticatorSelectionCriteria(); : device::AuthenticatorSelectionCriteria();
auto ctap_request = CreateCtapMakeCredentialRequest( auto ctap_request = CreateCtapMakeCredentialRequest(
client_data_json_, options, individual_attestation); client_data_json_, options, individual_attestation,
browser_context()->IsOffTheRecord());
ctap_request.set_is_u2f_only(OriginIsCryptoTokenExtension(caller_origin_)); ctap_request.set_is_u2f_only(OriginIsCryptoTokenExtension(caller_origin_));
request_ = std::make_unique<device::MakeCredentialRequestHandler>( request_ = std::make_unique<device::MakeCredentialRequestHandler>(
...@@ -778,9 +786,9 @@ void AuthenticatorImpl::GetAssertion( ...@@ -778,9 +786,9 @@ void AuthenticatorImpl::GetAssertion(
if (!connector_) if (!connector_)
connector_ = ServiceManagerConnection::GetForProcess()->GetConnector(); connector_ = ServiceManagerConnection::GetForProcess()->GetConnector();
auto ctap_request = auto ctap_request = CreateCtapGetAssertionRequest(
CreateCtapGetAssertionRequest(client_data_json_, std::move(options), client_data_json_, std::move(options), alternative_application_parameter_,
alternative_application_parameter_); browser_context()->IsOffTheRecord());
auto opt_platform_authenticator_info = auto opt_platform_authenticator_info =
CreatePlatformAuthenticatorIfAvailableAndCheckIfCredentialExists( CreatePlatformAuthenticatorIfAvailableAndCheckIfCredentialExists(
ctap_request); ctap_request);
...@@ -815,18 +823,25 @@ void AuthenticatorImpl::IsUserVerifyingPlatformAuthenticatorAvailable( ...@@ -815,18 +823,25 @@ void AuthenticatorImpl::IsUserVerifyingPlatformAuthenticatorAvailable(
} }
bool AuthenticatorImpl::IsUserVerifyingPlatformAuthenticatorAvailableImpl() { bool AuthenticatorImpl::IsUserVerifyingPlatformAuthenticatorAvailableImpl() {
// N.B. request_delegate_ may be nullptr at this point.
// All platform authenticators are disabled in incognito mode.
// TODO(martinkr): Revisit incognito handling (crbug/908622).
if (browser_context()->IsOffTheRecord())
return false;
#if defined(OS_MACOSX) #if defined(OS_MACOSX)
// Touch ID is disabled, regardless of hardware support, if the embedder // Touch ID is disabled, regardless of hardware support, if the embedder
// doesn't support it or if this is an Incognito session. N.B. // doesn't support it.
// request_delegate_ may be nullptr at this point.
if (!GetContentClient() if (!GetContentClient()
->browser() ->browser()
->IsWebAuthenticationTouchIdAuthenticatorSupported() || ->IsWebAuthenticationTouchIdAuthenticatorSupported())
browser_context()->IsOffTheRecord()) {
return false; return false;
}
return device::fido::mac::TouchIdAuthenticator::IsAvailable(); return device::fido::mac::TouchIdAuthenticator::IsAvailable();
#elif defined(OS_WIN)
return base::FeatureList::IsEnabled(device::kWebAuthUseNativeWinApi) &&
device::WinWebAuthnApiAuthenticator::
IsUserVerifyingPlatformAuthenticatorAvailable();
#else #else
return false; return false;
#endif #endif
......
...@@ -48,6 +48,10 @@ ...@@ -48,6 +48,10 @@
#include "device/fido/mac/scoped_touch_id_test_environment.h" #include "device/fido/mac/scoped_touch_id_test_environment.h"
#endif #endif
#if defined(OS_WIN)
#include "device/fido/win/fake_webauthn_api.h"
#endif
namespace content { namespace content {
using ::testing::_; using ::testing::_;
...@@ -1947,9 +1951,42 @@ TEST_F(AuthenticatorContentBrowserClientTest, IsUVPAATrueIfTouchIdAvailable) { ...@@ -1947,9 +1951,42 @@ TEST_F(AuthenticatorContentBrowserClientTest, IsUVPAATrueIfTouchIdAvailable) {
} }
#endif // defined(OS_MACOSX) #endif // defined(OS_MACOSX)
#if !defined(OS_MACOSX) #if defined(OS_WIN)
TEST_F(AuthenticatorContentBrowserClientTest, WinIsUVPAA) {
for (const bool enable_feature_flag : {false, true}) {
SCOPED_TRACE(enable_feature_flag ? "enable_feature_flag"
: "!enable_feature_flag");
for (const bool enable_win_webauthn_api : {false, true}) {
SCOPED_TRACE(enable_win_webauthn_api ? "enable_win_webauthn_api"
: "!enable_win_webauthn_api");
for (const bool is_uvpaa : {false, true}) {
SCOPED_TRACE(is_uvpaa ? "is_uvpaa" : "!is_uvpaa");
base::test::ScopedFeatureList scoped_feature_list;
if (enable_feature_flag)
scoped_feature_list.InitAndEnableFeature(
device::kWebAuthUseNativeWinApi);
device::ScopedFakeWinWebAuthnApi fake_api;
fake_api.set_available(enable_win_webauthn_api);
fake_api.set_is_uvpaa(is_uvpaa);
AuthenticatorPtr authenticator = ConnectToAuthenticator();
TestIsUvpaaCallback cb;
authenticator->IsUserVerifyingPlatformAuthenticatorAvailable(
cb.callback());
cb.WaitForCallback();
EXPECT_EQ(enable_feature_flag && enable_win_webauthn_api && is_uvpaa,
cb.value());
}
}
}
}
#endif // defined(OS_WIN)
#if !defined(OS_MACOSX) && !defined(OS_WIN)
TEST_F(AuthenticatorContentBrowserClientTest, IsUVPAAFalse) { TEST_F(AuthenticatorContentBrowserClientTest, IsUVPAAFalse) {
// No platform authenticator on non-macOS platforms. // There are no platform authenticators other than Windows Hello and macOS
// Touch ID.
NavigateAndCommit(GURL(kTestOrigin1)); NavigateAndCommit(GURL(kTestOrigin1));
AuthenticatorPtr authenticator = ConnectToAuthenticator(); AuthenticatorPtr authenticator = ConnectToAuthenticator();
...@@ -1958,7 +1995,7 @@ TEST_F(AuthenticatorContentBrowserClientTest, IsUVPAAFalse) { ...@@ -1958,7 +1995,7 @@ TEST_F(AuthenticatorContentBrowserClientTest, IsUVPAAFalse) {
cb.WaitForCallback(); cb.WaitForCallback();
EXPECT_FALSE(cb.value()); EXPECT_FALSE(cb.value());
} }
#endif // !defined(OS_MACOSX) #endif // !defined(OS_MACOSX) && !defined(OS_WIN)
TEST_F(AuthenticatorContentBrowserClientTest, TEST_F(AuthenticatorContentBrowserClientTest,
CryptotokenBypassesAttestationConsentPrompt) { CryptotokenBypassesAttestationConsentPrompt) {
......
...@@ -88,6 +88,11 @@ class COMPONENT_EXPORT(DEVICE_FIDO) CtapGetAssertionRequest { ...@@ -88,6 +88,11 @@ class COMPONENT_EXPORT(DEVICE_FIDO) CtapGetAssertionRequest {
return alternative_application_parameter_; return alternative_application_parameter_;
} }
bool is_incognito_mode() const { return is_incognito_mode_; }
void set_is_incognito_mode(bool is_incognito_mode) {
is_incognito_mode_ = is_incognito_mode;
}
private: private:
std::string rp_id_; std::string rp_id_;
std::string client_data_json_; std::string client_data_json_;
...@@ -102,6 +107,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) CtapGetAssertionRequest { ...@@ -102,6 +107,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) CtapGetAssertionRequest {
base::Optional<std::vector<CableDiscoveryData>> cable_extension_; base::Optional<std::vector<CableDiscoveryData>> cable_extension_;
base::Optional<std::array<uint8_t, kRpIdHashLength>> base::Optional<std::array<uint8_t, kRpIdHashLength>>
alternative_application_parameter_; alternative_application_parameter_;
bool is_incognito_mode_ = false;
}; };
} // namespace device } // namespace device
......
...@@ -88,6 +88,11 @@ class COMPONENT_EXPORT(DEVICE_FIDO) CtapMakeCredentialRequest { ...@@ -88,6 +88,11 @@ class COMPONENT_EXPORT(DEVICE_FIDO) CtapMakeCredentialRequest {
void set_is_u2f_only(bool is_u2f_only) { is_u2f_only_ = is_u2f_only; } void set_is_u2f_only(bool is_u2f_only) { is_u2f_only_ = is_u2f_only; }
bool is_u2f_only() { return is_u2f_only_; } bool is_u2f_only() { return is_u2f_only_; }
bool is_incognito_mode() const { return is_incognito_mode_; }
void set_is_incognito_mode(bool is_incognito_mode) {
is_incognito_mode_ = is_incognito_mode;
}
private: private:
std::string client_data_json_; std::string client_data_json_;
std::array<uint8_t, kClientDataHashLength> client_data_hash_; std::array<uint8_t, kClientDataHashLength> client_data_hash_;
...@@ -107,6 +112,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) CtapMakeCredentialRequest { ...@@ -107,6 +112,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) CtapMakeCredentialRequest {
// If true, instruct the request handler only to dispatch this request via // If true, instruct the request handler only to dispatch this request via
// U2F. // U2F.
bool is_u2f_only_ = false; bool is_u2f_only_ = false;
bool is_incognito_mode_ = false;
base::Optional<std::vector<PublicKeyCredentialDescriptor>> exclude_list_; base::Optional<std::vector<PublicKeyCredentialDescriptor>> exclude_list_;
base::Optional<std::vector<uint8_t>> pin_auth_; base::Optional<std::vector<uint8_t>> pin_auth_;
......
...@@ -43,6 +43,17 @@ base::string16 OptionalGURLToUTF16(const base::Optional<GURL>& in) { ...@@ -43,6 +43,17 @@ base::string16 OptionalGURLToUTF16(const base::Optional<GURL>& in) {
const char WinWebAuthnApiAuthenticator::kAuthenticatorId[] = const char WinWebAuthnApiAuthenticator::kAuthenticatorId[] =
"WinWebAuthnApiAuthenticator"; "WinWebAuthnApiAuthenticator";
// static
bool WinWebAuthnApiAuthenticator::
IsUserVerifyingPlatformAuthenticatorAvailable() {
BOOL result;
return WinWebAuthnApi::GetDefault()->IsAvailable() &&
WinWebAuthnApi::GetDefault()
->IsUserVerifyingPlatformAuthenticatorAvailable(&result) ==
S_OK &&
result == TRUE;
}
WinWebAuthnApiAuthenticator::WinWebAuthnApiAuthenticator( WinWebAuthnApiAuthenticator::WinWebAuthnApiAuthenticator(
WinWebAuthnApi* win_api, WinWebAuthnApi* win_api,
HWND current_window) HWND current_window)
...@@ -185,15 +196,26 @@ void WinWebAuthnApiAuthenticator::MakeCredentialBlocking( ...@@ -185,15 +196,26 @@ void WinWebAuthnApiAuthenticator::MakeCredentialBlocking(
_WEBAUTHN_CREDENTIAL_LIST exclude_credential_list{exclude_list.size(), _WEBAUTHN_CREDENTIAL_LIST exclude_credential_list{exclude_list.size(),
&exclude_list_ptr}; &exclude_list_ptr};
uint32_t authenticator_attachment;
if (request.is_u2f_only()) {
authenticator_attachment =
WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM_U2F_V2;
} else if (request.is_incognito_mode()) {
// Disable all platform authenticators in incognito mode. We are going to
// revisit this in crbug/908622.
authenticator_attachment = WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM;
} else {
authenticator_attachment =
ToWinAuthenticatorAttachment(request.authenticator_attachment());
}
WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS make_credential_options{ WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS make_credential_options{
WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_3, WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_3,
kWinWebAuthnTimeoutMilliseconds, kWinWebAuthnTimeoutMilliseconds,
WEBAUTHN_CREDENTIALS{ WEBAUTHN_CREDENTIALS{
0, nullptr}, // Ignored because pExcludeCredentialList is set. 0, nullptr}, // Ignored because pExcludeCredentialList is set.
WEBAUTHN_EXTENSIONS{extensions.size(), extensions.data()}, WEBAUTHN_EXTENSIONS{extensions.size(), extensions.data()},
request.is_u2f_only() authenticator_attachment,
? WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM_U2F_V2
: ToWinAuthenticatorAttachment(request.authenticator_attachment()),
request.resident_key_required(), request.resident_key_required(),
ToWinUserVerificationRequirement(request.user_verification()), ToWinUserVerificationRequirement(request.user_verification()),
WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT, WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT,
...@@ -301,19 +323,31 @@ void WinWebAuthnApiAuthenticator::GetAssertionBlocking( ...@@ -301,19 +323,31 @@ void WinWebAuthnApiAuthenticator::GetAssertionBlocking(
_WEBAUTHN_CREDENTIAL_LIST allow_credential_list{allow_list.size(), _WEBAUTHN_CREDENTIAL_LIST allow_credential_list{allow_list.size(),
&allow_list_ptr}; &allow_list_ptr};
uint32_t authenticator_attachment;
if (opt_app_id16) {
authenticator_attachment =
WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM_U2F_V2;
} else if (request.is_incognito_mode()) {
// Disable all platform authenticators in incognito mode. We are going to
// revisit this in crbug/908622.
authenticator_attachment = WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM;
} else {
authenticator_attachment = WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY;
}
WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS get_assertion_options{ WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS get_assertion_options{
WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_4, WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_4,
kWinWebAuthnTimeoutMilliseconds, kWinWebAuthnTimeoutMilliseconds,
WEBAUTHN_CREDENTIALS{ WEBAUTHN_CREDENTIALS{
0, nullptr}, // Ignored because pAllowCredentialList is set. 0, nullptr}, // Ignored because pAllowCredentialList is set.
WEBAUTHN_EXTENSIONS{0, nullptr}, WEBAUTHN_EXTENSIONS{0, nullptr},
// Note that attachment is effectively restricted via |allow_list|. authenticator_attachment,
WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY,
ToWinUserVerificationRequirement(request.user_verification()), ToWinUserVerificationRequirement(request.user_verification()),
0, // flags 0, // flags
opt_app_id16 ? opt_app_id16->c_str() : nullptr, // pwszU2fAppId opt_app_id16 ? opt_app_id16->c_str() : nullptr, // pwszU2fAppId
opt_app_id16 ? &kUseAppIdTrue : nullptr, // pbU2fAppId opt_app_id16 ? &kUseAppIdTrue : nullptr, // pbU2fAppId
&cancellation_id_, &allow_credential_list, &cancellation_id_,
&allow_credential_list,
}; };
// |assertion| must not not outlive |win_api_|. // |assertion| must not not outlive |win_api_|.
......
...@@ -29,6 +29,8 @@ class COMPONENT_EXPORT(DEVICE_FIDO) WinWebAuthnApiAuthenticator ...@@ -29,6 +29,8 @@ class COMPONENT_EXPORT(DEVICE_FIDO) WinWebAuthnApiAuthenticator
// The return value of |GetId|. // The return value of |GetId|.
static const char kAuthenticatorId[]; static const char kAuthenticatorId[];
static bool IsUserVerifyingPlatformAuthenticatorAvailable();
WinWebAuthnApiAuthenticator(WinWebAuthnApi* win_api, HWND current_window); WinWebAuthnApiAuthenticator(WinWebAuthnApi* win_api, HWND current_window);
~WinWebAuthnApiAuthenticator() override; ~WinWebAuthnApiAuthenticator() override;
......
...@@ -17,9 +17,9 @@ bool FakeWinWebAuthnApi::IsAvailable() const { ...@@ -17,9 +17,9 @@ bool FakeWinWebAuthnApi::IsAvailable() const {
HRESULT FakeWinWebAuthnApi::IsUserVerifyingPlatformAuthenticatorAvailable( HRESULT FakeWinWebAuthnApi::IsUserVerifyingPlatformAuthenticatorAvailable(
BOOL* result) { BOOL* result) {
*result = false;
DCHECK(is_available_); DCHECK(is_available_);
return E_NOTIMPL; *result = is_uvpaa_;
return S_OK;
} }
HRESULT FakeWinWebAuthnApi::AuthenticatorMakeCredential( HRESULT FakeWinWebAuthnApi::AuthenticatorMakeCredential(
......
...@@ -16,6 +16,9 @@ class FakeWinWebAuthnApi : public WinWebAuthnApi { ...@@ -16,6 +16,9 @@ class FakeWinWebAuthnApi : public WinWebAuthnApi {
// Inject the return value for WinWebAuthnApi::IsAvailable(). // Inject the return value for WinWebAuthnApi::IsAvailable().
void set_available(bool available) { is_available_ = available; } void set_available(bool available) { is_available_ = available; }
// Inject the return value for
// WinWebAuthnApi::IsUserverifyingPlatformAuthenticatorAvailable().
void set_is_uvpaa(bool is_uvpaa) { is_uvpaa_ = is_uvpaa; }
// WinWebAuthnApi: // WinWebAuthnApi:
bool IsAvailable() const override; bool IsAvailable() const override;
...@@ -42,6 +45,7 @@ class FakeWinWebAuthnApi : public WinWebAuthnApi { ...@@ -42,6 +45,7 @@ class FakeWinWebAuthnApi : public WinWebAuthnApi {
private: private:
bool is_available_ = true; bool is_available_ = true;
bool is_uvpaa_ = false;
}; };
// ScopedFakeWinWebAuthnApi overrides the value returned // ScopedFakeWinWebAuthnApi overrides the value returned
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment