Skip to content
Permalink
Browse files
[WebAuthn] Add getPublicKey/Algorithm to AuthenticatorAttestationResp…
…onse

https://bugs.webkit.org/show_bug.cgi?id=241878
rdar://90281745

Reviewed by Brent Fulgham.

This patch implements the getPublicKeyAlgorithm and getPublicKey convenience
methods described in https://www.w3.org/TR/webauthn-2/#sctn-public-key-easy

These allow RPs to import keys in a more widely supported format than the
COSE key returned in the attestationObject. getPublicKey() is only implemented
for ES256 keys, which is the first default pubKeyCredParams.

Tested using wpt/webauthn/createcredential-getpublickey.https.html

* Source/WebCore/Modules/webauthn/AuthenticatorAttestationResponse.cpp:
(WebCore::coseKeyForAttestationObject):
(WebCore::AuthenticatorAttestationResponse::getPublicKeyAlgorithm const):
(WebCore::AuthenticatorAttestationResponse::getPublicKey const):
* Source/WebCore/Modules/webauthn/AuthenticatorAttestationResponse.h:
* Source/WebCore/Modules/webauthn/AuthenticatorAttestationResponse.idl:
* Source/WebCore/Modules/webauthn/WebAuthenticationUtils.cpp:
(WebCore::encodeRawPublicKey):
* Source/WebCore/Modules/webauthn/WebAuthenticationUtils.h:
* Source/WebCore/Modules/webauthn/fido/Pin.cpp:
(fido::pin::encodeRawPublicKey): Deleted.
* Source/WebCore/Modules/webauthn/fido/Pin.h:
* LayoutTests/http/wpt/webauthn/public-key-credential-create-success-local.https-expected.txt
* LayoutTests/http/wpt/webauthn/public-key-credential-create-success-local.https.html

Canonical link: https://commits.webkit.org/251844@main
  • Loading branch information
pascoej committed Jun 25, 2022
1 parent 6d162ad commit 9e683e62ce3cd50823af5214c5afa4bf08a13ec6
Showing 9 changed files with 162 additions and 13 deletions.
@@ -6,8 +6,10 @@ CONSOLE MESSAGE: User gesture is not detected. To use the WebAuthn API, call 'na
CONSOLE MESSAGE: User gesture is not detected. To use the WebAuthn API, call 'navigator.credentials.create' or 'navigator.credentials.get' within user activated events.
CONSOLE MESSAGE: User gesture is not detected. To use the WebAuthn API, call 'navigator.credentials.create' or 'navigator.credentials.get' within user activated events.
CONSOLE MESSAGE: User gesture is not detected. To use the WebAuthn API, call 'navigator.credentials.create' or 'navigator.credentials.get' within user activated events.
CONSOLE MESSAGE: User gesture is not detected. To use the WebAuthn API, call 'navigator.credentials.create' or 'navigator.credentials.get' within user activated events.

PASS PublicKeyCredential's [[create]] with minimum options in a mock local authenticator.
PASS PublicKeyCredential's [[create]] with minimum options in a mock local authenticator checking getPublicKey()
PASS PublicKeyCredential's [[create]] with authenticatorSelection { 'platform' } in a mock local authenticator.
PASS PublicKeyCredential's [[create]] with matched exclude credential ids but not transports in a mock local authenticator.
PASS PublicKeyCredential's [[create]] with none attestation in a mock local authenticator.
@@ -83,6 +83,39 @@
});
}, "PublicKeyCredential's [[create]] with minimum options in a mock local authenticator.");

promise_test(async t => {
const privateKeyBase64 = "BKeelnM8D7ykBNa79wacy6FW8UYZ+PG0Cr2Dr4Rzo+qJHExNkPVdzQ1/hjrqg8iY3IzM/NUvJJOzmtuchS6TLqTfzmsXsVdApUn9kx4Lh/cv01hbXt7/Q8kAbubi8Mqmqw==";
const userhandleBase64 = generateUserhandleBase64();
if (window.internals)
internals.setMockWebAuthenticationConfiguration({
local: {
userVerification: "yes",
acceptAttestation: false,
privateKeyBase64: privateKeyBase64,
}
});

const options = {
publicKey: {
rp: {
name: "localhost",
},
user: {
name: userhandleBase64,
id: Base64URL.parse(userhandleBase64),
displayName: "Appleseed",
},
challenge: Base64URL.parse("MTIzNDU2"),
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
}
};

return navigator.credentials.create(options).then(credential => {
assert_equals(bytesToHexString(credential.response.getPublicKey()), "3059301306072a8648ce3d020106082a8648ce3d03010703420004a79e96733c0fbca404d6bbf7069ccba156f14619f8f1b40abd83af8473a3ea891c4c4d90f55dcd0d7f863aea83c898dc8cccfcd52f2493b39adb9c852e932ea4");
assert_equals(credential.response.getPublicKeyAlgorithm(), -7);
});
}, "PublicKeyCredential's [[create]] with minimum options in a mock local authenticator checking getPublicKey()");

promise_test(async t => {
const privateKeyBase64 = await generatePrivateKeyBase64();
const credentialID = await calculateCredentialID(privateKeyBase64);
@@ -30,10 +30,44 @@

#include "AuthenticatorResponseData.h"
#include "CBORReader.h"
#include "CryptoAlgorithmECDH.h"
#include "CryptoKeyEC.h"
#include "WebAuthenticationUtils.h"

namespace WebCore {

static std::optional<cbor::CBORValue> coseKeyForAttestationObject(Ref<ArrayBuffer> attObj)
{
auto decodedResponse = cbor::CBORReader::read(convertArrayBufferToVector(attObj.ptr()));
if (!decodedResponse || !decodedResponse->isMap()) {
ASSERT_NOT_REACHED();
return std::nullopt;
}
const auto& attObjMap = decodedResponse->getMap();
auto it = attObjMap.find(cbor::CBORValue("authData"));
if (it == attObjMap.end() || !it->second.isByteString()) {
ASSERT_NOT_REACHED();
return std::nullopt;
}
auto authData = it->second.getByteString();
const size_t credentialIdLengthOffset = rpIdHashLength + flagsLength + signCounterLength + aaguidLength;
if (authData.size() < credentialIdLengthOffset + credentialIdLengthLength)
return std::nullopt;

const size_t credentialIdLength = (static_cast<size_t>(authData[credentialIdLengthOffset]) << 8) | static_cast<size_t>(authData[credentialIdLengthOffset + 1]);
const size_t cosePublicKeyOffset = credentialIdLengthOffset + credentialIdLengthLength + credentialIdLength;
if (authData.size() <= cosePublicKeyOffset)
return std::nullopt;

const size_t cosePublicKeyLength = authData.size() - cosePublicKeyOffset;
Vector<uint8_t> cosePublicKey;
cosePublicKey.reserveInitialCapacity(cosePublicKeyLength);
auto beginIt = authData.begin() + cosePublicKeyOffset;
cosePublicKey.appendRange(beginIt, beginIt + cosePublicKeyLength);

return cbor::CBORReader::read(cosePublicKey);
}

Ref<AuthenticatorAttestationResponse> AuthenticatorAttestationResponse::create(Ref<ArrayBuffer>&& rawId, Ref<ArrayBuffer>&& attestationObject, AuthenticatorAttachment attachment, Vector<AuthenticatorTransport>&& transports)
{
return adoptRef(*new AuthenticatorAttestationResponse(WTFMove(rawId), WTFMove(attestationObject), attachment, WTFMove(transports)));
@@ -77,6 +111,79 @@ RefPtr<ArrayBuffer> AuthenticatorAttestationResponse::getAuthenticatorData() con
return ArrayBuffer::tryCreate(authData.data(), authData.size());
}

int64_t AuthenticatorAttestationResponse::getPublicKeyAlgorithm() const
{
auto key = coseKeyForAttestationObject(m_attestationObject);
if (!key || !key->isMap())
return 0;
auto& keyMap = key->getMap();

auto it = keyMap.find(cbor::CBORValue(COSE::alg));
if (it == keyMap.end() || !it->second.isInteger()) {
ASSERT_NOT_REACHED();
return 0;
}
return it->second.getInteger();
}

RefPtr<ArrayBuffer> AuthenticatorAttestationResponse::getPublicKey() const
{
auto key = coseKeyForAttestationObject(m_attestationObject);
if (!key || !key->isMap())
return nullptr;
auto& keyMap = key->getMap();

auto it = keyMap.find(cbor::CBORValue(COSE::alg));
if (it == keyMap.end() || !it->second.isInteger()) {
ASSERT_NOT_REACHED();
return nullptr;
}
auto alg = it->second.getInteger();

it = keyMap.find(cbor::CBORValue(COSE::kty));
if (it == keyMap.end() || !it->second.isInteger()) {
ASSERT_NOT_REACHED();
return nullptr;
}
auto kty = it->second.getInteger();

std::optional<int64_t> crv;
it = keyMap.find(cbor::CBORValue(COSE::crv));
if (it != keyMap.end() && it->second.isInteger())
crv = it->second.getInteger();

switch (alg) {
case COSE::ES256: {
if (kty != COSE::EC2 || crv != COSE::P_256)
return nullptr;

auto it = keyMap.find(cbor::CBORValue(COSE::x));
if (it == keyMap.end() || !it->second.isByteString()) {
ASSERT_NOT_REACHED();
return nullptr;
}
auto x = it->second.getByteString();

it = keyMap.find(cbor::CBORValue(COSE::y));
if (it == keyMap.end() || !it->second.isByteString()) {
ASSERT_NOT_REACHED();
return nullptr;
}
auto y = it->second.getByteString();

auto peerKey = CryptoKeyEC::importRaw(CryptoAlgorithmIdentifier::ECDH, "P-256"_s, encodeRawPublicKey(x, y), true, CryptoKeyUsageDeriveBits);
if (!peerKey)
return nullptr;
auto keySpki = peerKey->exportSpki().releaseReturnValue();
return ArrayBuffer::tryCreate(keySpki.data(), keySpki.size());
}
default:
break;
}

return nullptr;
}

} // namespace WebCore

#endif // ENABLE(WEB_AUTHN)
@@ -42,6 +42,8 @@ class AuthenticatorAttestationResponse : public AuthenticatorResponse {
ArrayBuffer* attestationObject() const { return m_attestationObject.ptr(); }
const Vector<AuthenticatorTransport>& getTransports() const { return m_transports; }
RefPtr<ArrayBuffer> getAuthenticatorData() const;
RefPtr<ArrayBuffer> getPublicKey() const;
int64_t getPublicKeyAlgorithm() const;

private:
AuthenticatorAttestationResponse(Ref<ArrayBuffer>&&, Ref<ArrayBuffer>&&, AuthenticatorAttachment, Vector<AuthenticatorTransport>&&);
@@ -23,6 +23,8 @@
* THE POSSIBILITY OF SUCH DAMAGE.
*/

typedef long COSEAlgorithmIdentifier;

[
Conditional=WEB_AUTHN,
EnabledBySetting=WebAuthenticationEnabled,
@@ -32,4 +34,6 @@
[SameObject] readonly attribute ArrayBuffer attestationObject;
sequence<AuthenticatorTransport> getTransports();
ArrayBuffer getAuthenticatorData();
ArrayBuffer? getPublicKey();
COSEAlgorithmIdentifier getPublicKeyAlgorithm();
};
@@ -191,6 +191,16 @@ Vector<uint8_t> buildClientDataJsonHash(const ArrayBuffer& clientDataJson)
return crypto->computeHash();
}

Vector<uint8_t> encodeRawPublicKey(const Vector<uint8_t>& x, const Vector<uint8_t>& y)
{
Vector<uint8_t> rawKey;
rawKey.reserveInitialCapacity(1 + x.size() + y.size());
rawKey.uncheckedAppend(0x04);
rawKey.appendVector(x);
rawKey.appendVector(y);
return rawKey;
}

} // namespace WebCore

#endif // ENABLE(WEB_AUTHN)
@@ -63,6 +63,9 @@ WEBCORE_EXPORT Ref<ArrayBuffer> buildClientDataJson(ClientDataType /*type*/, con
WEBCORE_EXPORT Vector<uint8_t> buildClientDataJsonHash(const ArrayBuffer& clientDataJson);

WEBCORE_EXPORT cbor::CBORValue::MapValue buildUserEntityMap(const Vector<uint8_t>& userId, const String& name, const String& displayName);

// encodeRawPublicKey takes X & Y and returns them as a 0x04 || X || Y byte array.
WEBCORE_EXPORT Vector<uint8_t> encodeRawPublicKey(const Vector<uint8_t>& X, const Vector<uint8_t>& Y);
} // namespace WebCore

#endif // ENABLE(WEB_AUTHN)
@@ -43,6 +43,7 @@
#include "CryptoKeyHMAC.h"
#include "DeviceResponseConverter.h"
#include "WebAuthenticationConstants.h"
#include "WebAuthenticationUtils.h"
#include <pal/crypto/CryptoDigest.h>

namespace fido {
@@ -70,16 +71,6 @@ static Vector<uint8_t> makePinAuth(const CryptoKeyHMAC& key, const Vector<uint8_
return pinAuth;
}

Vector<uint8_t> encodeRawPublicKey(const Vector<uint8_t>& x, const Vector<uint8_t>& y)
{
Vector<uint8_t> rawKey;
rawKey.reserveInitialCapacity(1 + x.size() + y.size());
rawKey.uncheckedAppend(0x04);
rawKey.appendVector(x);
rawKey.appendVector(y);
return rawKey;
}

std::optional<CString> validateAndConvertToUTF8(const String& pin)
{
if (!hasAtLeastFourCodepoints(pin))
@@ -84,9 +84,6 @@ constexpr int64_t kProtocolVersion = 1;
// encodeCOSEPublicKey takes a raw ECDH256 public key and returns it as a COSE structure.
WEBCORE_EXPORT cbor::CBORValue::MapValue encodeCOSEPublicKey(const Vector<uint8_t>& key);

// encodeRawPublicKey takes X & Y and returns them as a 0x04 || X || Y byte array.
WEBCORE_EXPORT Vector<uint8_t> encodeRawPublicKey(const Vector<uint8_t>& X, const Vector<uint8_t>& Y);

// validateAndConvertToUTF8 convert the input to a UTF8 CString if it is a syntactically valid PIN.
WEBCORE_EXPORT std::optional<CString> validateAndConvertToUTF8(const String& pin);

0 comments on commit 9e683e6

Please sign in to comment.