Bug 1260318 - Scope U2F Soft Tokens to a single AppID r=qdot,rbarnes

This change includes the FIDO "App ID" as part of the function used to generate
the wrapping key used in the NSS-based U2F soft token, cryptographically binding
the "Key Handle" to the site that Key Handle is intended for.

This is a breaking change with existing registered U2F keys, but since our soft
token is hidden behind a pref, it does not attempt to be backward-compatible.

- Updated for rbarnes' and qdot's reviews comments. Thanks!
- Made more strict in size restrictions, and added a version field
  to help us be this strict.
- Bugfix for an early unprotected buffer use (Thanks again rbarnes!)

MozReview-Commit-ID: Jf6gNPauT4Y

--HG--
extra : rebase_source : 52d10287d10698292e1480e04f580f6f8b4847cb
This commit is contained in:
J.C. Jones 2017-02-01 15:21:04 -07:00
Родитель 09bc17720b
Коммит 8effd5c124
9 изменённых файлов: 181 добавлений и 43 удалений

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

@ -3435,6 +3435,7 @@ ContentParent::RecvNSSU2FTokenIsCompatibleVersion(const nsString& aVersion,
mozilla::ipc::IPCResult
ContentParent::RecvNSSU2FTokenIsRegistered(nsTArray<uint8_t>&& aKeyHandle,
nsTArray<uint8_t>&& aApplication,
bool* aIsValidKeyHandle)
{
MOZ_ASSERT(aIsValidKeyHandle);
@ -3445,6 +3446,7 @@ ContentParent::RecvNSSU2FTokenIsRegistered(nsTArray<uint8_t>&& aKeyHandle,
}
nsresult rv = nssToken->IsRegistered(aKeyHandle.Elements(), aKeyHandle.Length(),
aApplication.Elements(), aApplication.Length(),
aIsValidKeyHandle);
if (NS_FAILED(rv)) {
return IPC_FAIL_NO_REASON(this);

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

@ -803,6 +803,7 @@ private:
bool* aIsCompatible) override;
virtual mozilla::ipc::IPCResult RecvNSSU2FTokenIsRegistered(nsTArray<uint8_t>&& aKeyHandle,
nsTArray<uint8_t>&& aApplication,
bool* aIsValidKeyHandle) override;
virtual mozilla::ipc::IPCResult RecvNSSU2FTokenRegister(nsTArray<uint8_t>&& aApplication,

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

@ -766,9 +766,10 @@ parent:
* Return whether the provided KeyHandle belongs to this Token
*
* |keyHandle| Key Handle to evaluate.
* |application| The FIDO Application data that is associated with this key.
* Returns |True| if the Key Handle is ours.
*/
sync NSSU2FTokenIsRegistered(uint8_t[] keyHandle)
sync NSSU2FTokenIsRegistered(uint8_t[] keyHandle, uint8_t[] application)
returns (bool isValidKeyHandle);
/**

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

@ -211,9 +211,11 @@ U2FPrepTask::Execute()
U2FIsRegisteredTask::U2FIsRegisteredTask(const Authenticator& aAuthenticator,
const LocalRegisteredKey& aRegisteredKey,
const CryptoBuffer& aAppParam,
AbstractThread* aMainThread)
: U2FPrepTask(aAuthenticator, aMainThread)
, mRegisteredKey(aRegisteredKey)
, mAppParam(aAppParam)
{}
U2FIsRegisteredTask::~U2FIsRegisteredTask()
@ -248,6 +250,7 @@ U2FIsRegisteredTask::Run()
bool isRegistered = false;
rv = mAuthenticator->IsRegistered(keyHandle.Elements(), keyHandle.Length(),
mAppParam.Elements(), mAppParam.Length(),
&isRegistered);
if (NS_WARN_IF(NS_FAILED(rv))) {
mPromise.Reject(ErrorCode::OTHER_ERROR, __func__);
@ -378,6 +381,7 @@ U2FSignTask::Run()
bool isRegistered = false;
rv = mAuthenticator->IsRegistered(mKeyHandle.Elements(), mKeyHandle.Length(),
mAppParam.Elements(), mAppParam.Length(),
&isRegistered);
if (NS_WARN_IF(NS_FAILED(rv))) {
mPromise.Reject(ErrorCode::OTHER_ERROR, __func__);
@ -618,13 +622,30 @@ U2FRegisterRunnable::Run()
status->Stop(appIdResult);
}
// Produce the AppParam from the current AppID
nsCString cAppId = NS_ConvertUTF16toUTF8(mAppId);
CryptoBuffer appParam;
if (!appParam.SetLength(SHA256_LENGTH, fallible)) {
return NS_ERROR_OUT_OF_MEMORY;
}
// Note: This could use nsICryptoHash to avoid having to interact with NSS
// directly.
SECStatus srv;
srv = PK11_HashBuf(SEC_OID_SHA256, appParam.Elements(),
reinterpret_cast<const uint8_t*>(cAppId.BeginReading()),
cAppId.Length());
if (srv != SECSuccess) {
return NS_ERROR_FAILURE;
}
// First, we must determine if any of the RegisteredKeys are already
// registered, e.g., in the whitelist.
for (LocalRegisteredKey key : mRegisteredKeys) {
nsTArray<RefPtr<U2FPrepPromise>> prepPromiseList;
for (const Authenticator& token : mAuthenticators) {
RefPtr<U2FIsRegisteredTask> compTask =
new U2FIsRegisteredTask(token, key, mAbstractMainThread);
new U2FIsRegisteredTask(token, key, appParam, mAbstractMainThread);
prepPromiseList.AppendElement(compTask->Execute());
}
@ -668,23 +689,6 @@ U2FRegisterRunnable::Run()
return NS_OK;
}
// Since we're continuing, we hash the AppID into the AppParam
nsCString cAppId = NS_ConvertUTF16toUTF8(mAppId);
CryptoBuffer appParam;
if (!appParam.SetLength(SHA256_LENGTH, fallible)) {
return NS_ERROR_OUT_OF_MEMORY;
}
// Note: This could use nsICryptoHash to avoid having to interact with NSS
// directly.
SECStatus srv;
srv = PK11_HashBuf(SEC_OID_SHA256, appParam.Elements(),
reinterpret_cast<const uint8_t*>(cAppId.BeginReading()),
cAppId.Length());
if (srv != SECSuccess) {
return NS_ERROR_FAILURE;
}
// Now proceed to actually register a new key.
for (LocalRegisterRequest req : mRegisterRequests) {
// Hash the ClientData into the ChallengeParam

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

@ -86,6 +86,7 @@ class U2FIsRegisteredTask final : public U2FPrepTask
public:
U2FIsRegisteredTask(const Authenticator& aAuthenticator,
const LocalRegisteredKey& aRegisteredKey,
const CryptoBuffer& aAppParam,
AbstractThread* aMainThread);
NS_DECL_NSIRUNNABLE
@ -93,6 +94,7 @@ private:
~U2FIsRegisteredTask();
LocalRegisteredKey mRegisteredKey;
CryptoBuffer mAppParam;
};
class U2FTask : public Runnable

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

@ -37,9 +37,11 @@ NSSU2FTokenRemote::IsCompatibleVersion(const nsAString& aVersionString,
NS_IMETHODIMP
NSSU2FTokenRemote::IsRegistered(uint8_t* aKeyHandle, uint32_t aKeyHandleLen,
uint8_t* aAppParam, uint32_t aAppParamLen,
bool* aIsRegistered)
{
NS_ENSURE_ARG_POINTER(aKeyHandle);
NS_ENSURE_ARG_POINTER(aAppParam);
NS_ENSURE_ARG_POINTER(aIsRegistered);
nsTArray<uint8_t> keyHandle;
@ -48,9 +50,15 @@ NSSU2FTokenRemote::IsRegistered(uint8_t* aKeyHandle, uint32_t aKeyHandleLen,
return NS_ERROR_OUT_OF_MEMORY;
}
nsTArray<uint8_t> appParam;
if (!appParam.ReplaceElementsAt(0, appParam.Length(), aAppParam,
aAppParamLen)) {
return NS_ERROR_OUT_OF_MEMORY;
}
ContentChild* cc = ContentChild::GetSingleton();
MOZ_ASSERT(cc);
if (!cc->SendNSSU2FTokenIsRegistered(keyHandle, aIsRegistered)) {
if (!cc->SendNSSU2FTokenIsRegistered(keyHandle, appParam, aIsRegistered)) {
return NS_ERROR_FAILURE;
}
return NS_OK;

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

@ -380,7 +380,8 @@ WebAuthentication::U2FAuthMakeCredential(
return;
}
nsresult rv = aToken->IsRegistered(data, len, &isRegistered);
nsresult rv = aToken->IsRegistered(data, len, aRpIdHash.Elements(),
aRpIdHash.Length(), &isRegistered);
if (NS_WARN_IF(NS_FAILED(rv))) {
aRequest->SetFailure(rv);
return;
@ -558,6 +559,8 @@ WebAuthentication::U2FAuthGetAssertion(const RefPtr<AssertionRequest>& aRequest,
bool isRegistered = false;
nsresult rv = aToken->IsRegistered(allowedCredential.Elements(),
allowedCredential.Length(),
aRpIdHash.Elements(),
aRpIdHash.Length(),
&isRegistered);
// 4.1.2.8.b If any authenticator returns a status indicating that the user

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

@ -23,10 +23,13 @@ interface nsIU2FToken : nsISupports {
* Return whether the provided KeyHandle belongs to this Token
*
* @param keyHandle Key Handle to evaluate.
* @param application The FIDO Application data to associate with the key.
* @return True if the Key Handle is ours.
*/
void isRegistered([array, size_is(keyHandleLen)] in octet keyHandle,
in uint32_t keyHandleLen,
[array, size_is(applicationLen)] in octet application,
in uint32_t applicationLen,
[retval] out boolean result);
/**

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

@ -46,6 +46,8 @@ const uint32_t kParamLen = 32;
const uint32_t kPublicKeyLen = 65;
const uint32_t kWrappedKeyBufLen = 256;
const uint32_t kWrappingKeyByteLen = 128/8;
const uint32_t kSaltByteLen = 64/8;
const uint32_t kVersion1KeyHandleLen = 162;
NS_NAMED_LITERAL_STRING(kEcAlgorithm, WEBCRYPTO_NAMED_CURVE_P256);
const PRTime kOneDay = PRTime(PR_USEC_PER_SEC)
@ -55,6 +57,10 @@ const PRTime kOneDay = PRTime(PR_USEC_PER_SEC)
const PRTime kExpirationSlack = kOneDay; // Pre-date for clock skew
const PRTime kExpirationLife = kOneDay;
enum SoftTokenHandle {
Version1 = 0,
};
static mozilla::LazyLogModule gNSSTokenLog("webauth_u2f");
nsNSSU2FToken::nsNSSU2FToken()
@ -380,17 +386,48 @@ nsNSSU2FToken::Init()
}
// Convert a Private Key object into an opaque key handle, using AES Key Wrap
// and aWrappingKey to convert aPrivKey.
// with the long-lived aPersistentKey mixed with aAppParam to convert aPrivKey.
// The key handle's format is version || saltLen || salt || wrappedPrivateKey
static UniqueSECItem
KeyHandleFromPrivateKey(const UniquePK11SlotInfo& aSlot,
const UniquePK11SymKey& aWrappingKey,
const UniquePK11SymKey& aPersistentKey,
uint8_t* aAppParam, uint32_t aAppParamLen,
const UniqueSECKEYPrivateKey& aPrivKey,
const nsNSSShutDownPreventionLock&)
{
MOZ_ASSERT(aSlot);
MOZ_ASSERT(aWrappingKey);
MOZ_ASSERT(aPersistentKey);
MOZ_ASSERT(aAppParam);
MOZ_ASSERT(aPrivKey);
if (!aSlot || !aWrappingKey || !aPrivKey) {
if (!aSlot || !aPersistentKey || !aPrivKey || !aAppParam) {
return nullptr;
}
// Generate a random salt
uint8_t saltParam[kSaltByteLen];
SECStatus srv = PK11_GenerateRandomOnSlot(aSlot.get(), saltParam,
sizeof(saltParam));
if (srv != SECSuccess) {
MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
("Failed to generate a salt, NSS error #%d", PORT_GetError()));
return nullptr;
}
// Prepare the HKDF (https://tools.ietf.org/html/rfc5869)
CK_NSS_HKDFParams hkdfParams = { true, saltParam, sizeof(saltParam),
true, aAppParam, aAppParamLen };
SECItem kdfParams = { siBuffer, (unsigned char*)&hkdfParams,
sizeof(hkdfParams) };
// Derive a wrapping key from aPersistentKey, the salt, and the aAppParam.
// CKM_AES_KEY_GEN and CKA_WRAP are key type and usage attributes of the
// derived symmetric key and don't matter because we ignore them anyway.
UniquePK11SymKey wrapKey(PK11_Derive(aPersistentKey.get(), CKM_NSS_HKDF_SHA256,
&kdfParams, CKM_AES_KEY_GEN, CKA_WRAP,
kWrappingKeyByteLen));
if (!wrapKey.get()) {
MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
("Failed to derive a wrapping key, NSS error #%d", PORT_GetError()));
return nullptr;
}
@ -398,46 +435,115 @@ KeyHandleFromPrivateKey(const UniquePK11SlotInfo& aSlot,
/* no buffer */ nullptr,
kWrappedKeyBufLen));
if (!wrappedKey) {
MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
("Failed to allocate memory, NSS error #%d", PORT_GetError()));
MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory"));
return nullptr;
}
UniqueSECItem param(PK11_ParamFromIV(CKM_NSS_AES_KEY_WRAP_PAD,
/* default IV */ nullptr ));
SECStatus srv = PK11_WrapPrivKey(aSlot.get(), aWrappingKey.get(),
aPrivKey.get(), CKM_NSS_AES_KEY_WRAP_PAD,
param.get(), wrappedKey.get(),
/* wincx */ nullptr);
srv = PK11_WrapPrivKey(aSlot.get(), wrapKey.get(), aPrivKey.get(),
CKM_NSS_AES_KEY_WRAP_PAD, param.get(), wrappedKey.get(),
/* wincx */ nullptr);
if (srv != SECSuccess) {
MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
("Failed to wrap U2F key, NSS error #%d", PORT_GetError()));
MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
("Failed to wrap U2F key, NSS error #%d", PORT_GetError()));
return nullptr;
}
return wrappedKey;
// Concatenate the salt and the wrapped Private Key together
mozilla::dom::CryptoBuffer keyHandleBuf;
if (!keyHandleBuf.SetCapacity(wrappedKey.get()->len + sizeof(saltParam) + 2,
mozilla::fallible)) {
MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory"));
return nullptr;
}
// It's OK to ignore the return values here because we're writing into
// pre-allocated space
keyHandleBuf.AppendElement(SoftTokenHandle::Version1, mozilla::fallible);
keyHandleBuf.AppendElement(sizeof(saltParam), mozilla::fallible);
keyHandleBuf.AppendElements(saltParam, sizeof(saltParam), mozilla::fallible);
keyHandleBuf.AppendSECItem(wrappedKey.get());
UniqueSECItem keyHandle(SECITEM_AllocItem(/* default arena */ nullptr,
/* no buffer */ nullptr,
keyHandleBuf.Length()));
if (!keyHandle) {
MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory"));
return nullptr;
}
// This can't fail, as it's pre-allocated above, but be careful
if (!keyHandleBuf.ToSECItem(/* default arena */ nullptr, keyHandle.get())) {
MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory"));
return nullptr;
}
return keyHandle;
}
// Convert an opaque key handle aKeyHandle back into a Private Key object, using
// aWrappingKey and the AES Key Wrap algorithm.
// the long-lived aPersistentKey mixed with aAppParam and the AES Key Wrap
// algorithm.
static UniqueSECKEYPrivateKey
PrivateKeyFromKeyHandle(const UniquePK11SlotInfo& aSlot,
const UniquePK11SymKey& aWrappingKey,
const UniquePK11SymKey& aPersistentKey,
uint8_t* aKeyHandle, uint32_t aKeyHandleLen,
uint8_t* aAppParam, uint32_t aAppParamLen,
const nsNSSShutDownPreventionLock&)
{
MOZ_ASSERT(aSlot);
MOZ_ASSERT(aWrappingKey);
MOZ_ASSERT(aPersistentKey);
MOZ_ASSERT(aKeyHandle);
if (!aSlot || !aWrappingKey || !aKeyHandle) {
MOZ_ASSERT(aAppParam);
MOZ_ASSERT(aAppParamLen == SHA256_LENGTH);
if (!aSlot || !aPersistentKey || !aKeyHandle || !aAppParam ||
aAppParamLen != SHA256_LENGTH) {
return nullptr;
}
ScopedAutoSECItem pubKey(kPublicKeyLen);
// As we only support one key format ourselves (right now), fail early if
// we aren't that length
if (aKeyHandleLen != kVersion1KeyHandleLen) {
return nullptr;
}
ScopedAutoSECItem keyHandleItem(aKeyHandleLen);
memcpy(keyHandleItem.data, aKeyHandle, keyHandleItem.len);
if (aKeyHandle[0] != SoftTokenHandle::Version1) {
// Unrecognized version
return nullptr;
}
uint8_t saltLen = aKeyHandle[1];
uint8_t* saltPtr = aKeyHandle + 2;
if (saltLen != kSaltByteLen) {
return nullptr;
}
// Prepare the HKDF (https://tools.ietf.org/html/rfc5869)
CK_NSS_HKDFParams hkdfParams = { true, saltPtr, saltLen,
true, aAppParam, aAppParamLen };
SECItem kdfParams = { siBuffer, (unsigned char*)&hkdfParams,
sizeof(hkdfParams) };
// Derive a wrapping key from aPersistentKey, the salt, and the aAppParam.
// CKM_AES_KEY_GEN and CKA_WRAP are key type and usage attributes of the
// derived symmetric key and don't matter because we ignore them anyway.
UniquePK11SymKey wrapKey(PK11_Derive(aPersistentKey.get(), CKM_NSS_HKDF_SHA256,
&kdfParams, CKM_AES_KEY_GEN, CKA_WRAP,
kWrappingKeyByteLen));
if (!wrapKey.get()) {
MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
("Failed to derive a wrapping key, NSS error #%d", PORT_GetError()));
return nullptr;
}
uint8_t wrappedLen = aKeyHandleLen - saltLen - 2;
uint8_t* wrappedPtr = aKeyHandle + saltLen + 2;
ScopedAutoSECItem wrappedKeyItem(wrappedLen);
memcpy(wrappedKeyItem.data, wrappedPtr, wrappedKeyItem.len);
ScopedAutoSECItem pubKey(kPublicKeyLen);
UniqueSECItem param(PK11_ParamFromIV(CKM_NSS_AES_KEY_WRAP_PAD,
/* default IV */ nullptr ));
@ -446,8 +552,8 @@ PrivateKeyFromKeyHandle(const UniquePK11SlotInfo& aSlot,
int usageCount = 1;
UniqueSECKEYPrivateKey unwrappedKey(
PK11_UnwrapPrivKey(aSlot.get(), aWrappingKey.get(), CKM_NSS_AES_KEY_WRAP_PAD,
param.get(), &keyHandleItem,
PK11_UnwrapPrivKey(aSlot.get(), wrapKey.get(), CKM_NSS_AES_KEY_WRAP_PAD,
param.get(), &wrappedKeyItem,
/* no nickname */ nullptr,
/* discard pubkey */ &pubKey,
/* not permanent */ false,
@ -477,9 +583,11 @@ nsNSSU2FToken::IsCompatibleVersion(const nsAString& aVersion, bool* aResult)
// IsRegistered determines if the provided key handle is usable by this token.
NS_IMETHODIMP
nsNSSU2FToken::IsRegistered(uint8_t* aKeyHandle, uint32_t aKeyHandleLen,
uint8_t* aAppParam, uint32_t aAppParamLen,
bool* aResult)
{
NS_ENSURE_ARG_POINTER(aKeyHandle);
NS_ENSURE_ARG_POINTER(aAppParam);
NS_ENSURE_ARG_POINTER(aResult);
if (!NS_IsMainThread()) {
@ -504,6 +612,8 @@ nsNSSU2FToken::IsRegistered(uint8_t* aKeyHandle, uint32_t aKeyHandleLen,
UniqueSECKEYPrivateKey privKey = PrivateKeyFromKeyHandle(slot, mWrappingKey,
aKeyHandle,
aKeyHandleLen,
aAppParam,
aAppParamLen,
locker);
*aResult = (privKey.get() != nullptr);
return NS_OK;
@ -583,6 +693,8 @@ nsNSSU2FToken::Register(uint8_t* aApplication,
// The key handle will be the result of keywrap(privKey, key=mWrappingKey)
UniqueSECItem keyHandleItem = KeyHandleFromPrivateKey(slot, mWrappingKey,
aApplication,
aApplicationLen,
privKey, locker);
if (!keyHandleItem.get()) {
return NS_ERROR_FAILURE;
@ -695,6 +807,8 @@ nsNSSU2FToken::Sign(uint8_t* aApplication, uint32_t aApplicationLen,
UniqueSECKEYPrivateKey privKey = PrivateKeyFromKeyHandle(slot, mWrappingKey,
aKeyHandle,
aKeyHandleLen,
aApplication,
aApplicationLen,
locker);
if (!privKey.get()) {
MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Couldn't get the priv key!"));