Adding hmac-secret tests.

Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
This commit is contained in:
Pol Henarejos
2022-10-03 16:10:36 +02:00
parent 40b5f70761
commit 577edbb62f
3 changed files with 284 additions and 12 deletions

View File

@@ -6,6 +6,7 @@ from fido2.server import Fido2Server
from fido2.ctap import CtapError from fido2.ctap import CtapError
from fido2.webauthn import CollectedClientData, AttestedCredentialData from fido2.webauthn import CollectedClientData, AttestedCredentialData
from getpass import getpass from getpass import getpass
from utils import *
import sys import sys
import pytest import pytest
import os import os
@@ -237,14 +238,27 @@ class Device():
type=CollectedClientData.TYPE.CREATE, origin=self.__origin, challenge=os.urandom(32) type=CollectedClientData.TYPE.CREATE, origin=self.__origin, challenge=os.urandom(32)
) )
rp_id = rp_id if rp_id is not Ellipsis else self.__rp['id'] rp_id = rp_id if rp_id is not Ellipsis else self.__rp['id']
result = self.__client._backend.do_get_assertion( try:
client_data=client_data, result = self.__client._backend.do_get_assertion(
rp_id=rp_id, client_data=client_data,
allow_list=allow_list, rp_id=rp_id,
extensions=extensions, allow_list=allow_list,
user_verification=user_verification, extensions=extensions,
event=event user_verification=user_verification,
) event=event
)
except ClientError as e:
if (e.code == ClientError.ERR.CONFIGURATION_UNSUPPORTED):
client_pin = ClientPin(self.__client._backend.ctap2)
client_pin.set_pin(DEFAULT_PIN)
result = self.__client._backend.do_get_assertion(
client_data=client_data,
rp_id=rp_id,
allow_list=allow_list,
extensions=extensions,
user_verification=user_verification,
event=event
)
return {'res':result,'req':{'client_data':client_data, return {'res':result,'req':{'client_data':client_data,
'rp_id':rp_id}} 'rp_id':rp_id}}
@@ -272,10 +286,10 @@ def GARes(device, MCRes, *args):
res = device.doGA(allow_list=[ res = device.doGA(allow_list=[
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} {"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
], *args) ], *args)
credential_data = AttestedCredentialData(MCRes['res'].attestation_object.auth_data.credential_data)
assertions = res['res'].get_assertions() assertions = res['res'].get_assertions()
for a in assertions: for a in assertions:
a.verify(res['req']['client_data'].hash, credential_data.public_key) verify(MCRes['res'].attestation_object, a, res['req']['client_data'].hash)
return res return res
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
@@ -287,6 +301,6 @@ def GARes_DC(device, MCRes_DC, *args):
res = device.GA(allow_list=[ res = device.GA(allow_list=[
{"id": MCRes_DC['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} {"id": MCRes_DC['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
], *args) ], *args)
credential_data = AttestedCredentialData(MCRes_DC['res'].attestation_object.auth_data.credential_data) verify(MCRes_DC['res'].attestation_object, res['res'], res['req']['client_data_hash'])
res['res'].verify(res['req']['client_data_hash'], credential_data.public_key)
return res return res

View File

@@ -0,0 +1,199 @@
import pytest
from fido2.ctap import CtapError
from fido2.ctap2.extensions import HmacSecretExtension
from fido2.utils import hmac_sha256
from fido2.ctap2.pin import PinProtocolV2
from fido2.webauthn import UserVerificationRequirement
from utils import *
salt1 = b"\xa5" * 32
salt2 = b"\x96" * 32
salt3 = b"\x03" * 32
salt4 = b"\x5a" * 16
salt5 = b"\x96" * 64
@pytest.fixture(scope="class")
def MCHmacSecret(resetdevice):
res = resetdevice.doMC(extensions={"hmacCreateSecret": True},rk=True)
return res['res'].attestation_object
@pytest.fixture(scope="class")
def hmac(resetdevice):
return HmacSecretExtension(resetdevice.client()._backend.ctap2, pin_protocol=PinProtocolV2())
def test_hmac_secret_make_credential(MCHmacSecret):
assert MCHmacSecret.auth_data.extensions
assert "hmac-secret" in MCHmacSecret.auth_data.extensions
assert MCHmacSecret.auth_data.extensions["hmac-secret"] == True
def test_hmac_secret_info(info):
assert "hmac-secret" in info.extensions
def test_fake_extension(device):
device.doMC(extensions={"tetris": True})
@pytest.mark.parametrize("salts", [(salt1,), (salt1, salt2)])
def test_hmac_secret_entropy(device, MCHmacSecret, hmac, salts
):
hout = {'salt1':salts[0]}
if (len(salts) > 1):
hout['salt2'] = salts[1]
auth = device.doGA(extensions={"hmacGetSecret": hout})['res'].get_response(0)
ext = auth.extension_results
assert ext
assert "hmacGetSecret" in ext
assert len(auth.authenticator_data.extensions['hmac-secret']) == len(salts) * 32 + 16
#print(shannon_entropy(auth.authenticator_data.extensions['hmac-secret']))
if len(salts) == 1:
assert shannon_entropy(auth.authenticator_data.extensions['hmac-secret']) > 4.6
assert shannon_entropy(ext["hmacGetSecret"]['output1']) > 4.6
if len(salts) == 2:
assert shannon_entropy(auth.authenticator_data.extensions['hmac-secret']) > 5.4
assert shannon_entropy(ext["hmacGetSecret"]['output1']) > 4.6
assert shannon_entropy(ext["hmacGetSecret"]['output2']) > 4.6
def get_output(device, MCHmacSecret, hmac, salts):
hout = {'salt1':salts[0]}
if (len(salts) > 1):
hout['salt2'] = salts[1]
auth = device.doGA(extensions={"hmacGetSecret": hout})['res'].get_response(0)
ext = auth.extension_results
assert ext
assert "hmacGetSecret" in ext
assert len(auth.authenticator_data.extensions['hmac-secret']) == len(salts) * 32 + 16
if len(salts) == 2:
return ext["hmacGetSecret"]['output1'], ext["hmacGetSecret"]['output2']
else:
return ext["hmacGetSecret"]['output1']
def test_hmac_secret_sanity(device, MCHmacSecret, hmac):
output1 = get_output(device, MCHmacSecret, hmac, (salt1,))
output12 = get_output(
device, MCHmacSecret, hmac, (salt1, salt2)
)
output21 = get_output(
device, MCHmacSecret, hmac, (salt2, salt1)
)
assert output12[0] == output1
assert output21[1] == output1
assert output21[0] == output12[1]
assert output12[0] != output12[1]
def test_missing_keyAgreement(device, hmac):
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}})
with pytest.raises(CtapError):
device.GA(extensions={"hmac-secret": {2: hout[2], 3: hout[3]}})
def test_missing_saltAuth(device, hmac):
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}})
with pytest.raises(CtapError) as e:
device.GA(extensions={"hmac-secret": {1: hout[1], 2: hout[2]}})
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
def test_missing_saltEnc(device, hmac):
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}})
with pytest.raises(CtapError) as e:
device.GA(extensions={"hmac-secret": {1: hout[1], 3: hout[3]}})
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
def test_bad_auth(device, hmac, MCHmacSecret):
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}})
bad_auth = list(hout[3][:])
bad_auth[len(bad_auth) // 2] = bad_auth[len(bad_auth) // 2] ^ 1
bad_auth = bytes(bad_auth)
with pytest.raises(CtapError) as e:
device.GA(extensions={"hmac-secret": {1: hout[1], 2: hout[2], 3: bad_auth, 4: 2}})
assert e.value.code == CtapError.ERR.EXTENSION_FIRST
@pytest.mark.parametrize("salts", [(salt4,), (salt4, salt5)])
def test_invalid_salt_length(device, hmac, salts):
with pytest.raises(ValueError) as e:
if (len(salts) == 2):
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0],"salt2":salts[1]}})
else:
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0]}})
device.doGA(extensions={"hmacGetSecret": hout})
@pytest.mark.parametrize("salts", [(salt1,), (salt1, salt2)])
def test_get_next_assertion_has_extension(
device, hmac, salts
):
""" Check that get_next_assertion properly returns extension information for multiple accounts. """
if (len(salts) == 2):
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0],"salt2":salts[1]}})
else:
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0]}})
accounts = 3
regs = []
auths = []
rp = {"id": f"example_salts_{len(salts)}.org", "name": "ExampleRP_2"}
fixed_users = [generate_random_user() for _ in range(accounts)]
for i in range(accounts):
res = device.doMC(extensions={"hmacCreateSecret": True},
rk=True,
rp=rp,
user=fixed_users[i])['res'].attestation_object
regs.append(res)
hout = {'salt1':salts[0]}
if (len(salts) > 1):
hout['salt2'] = salts[1]
ga = device.doGA(extensions={"hmacGetSecret": hout}, rp_id=rp['id'])
auths = ga['res'].get_assertions()
for x in auths:
assert x.auth_data.flags & (1 << 7) # has extension
ext = x.auth_data.extensions
assert ext
assert "hmac-secret" in ext
assert isinstance(ext["hmac-secret"], bytes)
assert len(ext["hmac-secret"]) == len(salts) * 32 + 16
key = hmac.process_get_output(x)
def test_hmac_secret_different_with_uv(device, MCHmacSecret, hmac):
salts = [salt1]
if (len(salts) == 2):
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0],"salt2":salts[1]}})
else:
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0]}})
auth_no_uv = device.GA(extensions={"hmac-secret": hout})['res']
assert (auth_no_uv.auth_data.flags & (1 << 2)) == 0
ext_no_uv = auth_no_uv.auth_data.extensions
assert ext_no_uv
assert "hmac-secret" in ext_no_uv
assert isinstance(ext_no_uv["hmac-secret"], bytes)
assert len(ext_no_uv["hmac-secret"]) == len(salts) * 32 + 16
# Now get same auth with UV
hout = {'salt1':salts[0]}
if (len(salts) > 1):
hout['salt2'] = salts[1]
auth_uv = device.doGA(extensions={"hmacGetSecret": hout}, user_verification=UserVerificationRequirement.REQUIRED)['res'].get_response(0)
assert auth_uv.authenticator_data.flags & (1 << 2)
ext_uv = auth_uv.extension_results
assert ext_uv
assert "hmacGetSecret" in ext_uv
assert len(ext_uv["hmacGetSecret"]) == len(salts)
# Now see if the hmac-secrets are different
assert ext_no_uv["hmac-secret"][:32] != ext_uv["hmacGetSecret"]['output1']

59
tests/utils.py Normal file
View File

@@ -0,0 +1,59 @@
from fido2.webauthn import AttestedCredentialData
import random
import string
import secrets
import math
def verify(MC, GA, client_data_hash):
credential_data = AttestedCredentialData(MC.auth_data.credential_data)
GA.verify(client_data_hash, credential_data.public_key)
def generate_random_user():
# https://www.w3.org/TR/webauthn/#user-handle
user_id_length = random.randint(1, 64)
user_id = secrets.token_bytes(user_id_length)
# https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity
name = "User name"
icon = "https://www.w3.org/TR/webauthn/"
display_name = "Displayed " + name
return {"id": user_id, "name": name, "icon": icon, "displayName": display_name}
counter = 1
def generate_user_maximum():
"""
Generate RK with the maximum lengths of the fields, according to the minimal requirements of the FIDO2 spec
"""
global counter
# https://www.w3.org/TR/webauthn/#user-handle
user_id_length = 64
user_id = secrets.token_bytes(user_id_length)
# https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity
name = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(64))
name = f"{counter}: {name}"
icon = "https://www.w3.org/TR/webauthn/" + "A" * 128
display_name = "Displayed " + name
name = name[:64]
display_name = display_name[:64]
icon = icon[:128]
counter += 1
return {"id": user_id, "name": name, "icon": icon, "displayName": display_name}
def shannon_entropy(data):
s = 0.0
total = len(data)
for x in range(0, 256):
freq = data.count(x)
p = freq / total
if p > 0:
s -= p * math.log2(p)
return s