168 lines
5.7 KiB
Python
168 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright 2022 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# Lint as: python3
|
|
"""Script to benchmark CTAP commands using Dilithium Hybrid signatures."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
import datetime
|
|
from subprocess import DEVNULL, STDOUT, check_call
|
|
import sys
|
|
from time import sleep
|
|
from typing import Any
|
|
import uuid
|
|
|
|
import colorama
|
|
from tqdm.auto import tqdm
|
|
|
|
from fido2 import ctap
|
|
from fido2.webauthn import PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, PublicKeyCredentialParameters
|
|
from fido2 import hid
|
|
from tools.configure import fatal, info, get_opensk_devices
|
|
|
|
ES256_ALGORITHM = PublicKeyCredentialParameters("public-key", -7)
|
|
HYBRID_ALGORITHM = PublicKeyCredentialParameters("public-key", -65537)
|
|
|
|
|
|
def error(message: str):
|
|
tqdm.write(message)
|
|
|
|
|
|
def check_info(authenticator: Any):
|
|
"""Checks if the assumed upgrade info matches the authenticator's."""
|
|
try:
|
|
info("Reading info...")
|
|
if HYBRID_ALGORITHM not in authenticator.info.algorithms:
|
|
fatal("The device does not support hybrid signatures.")
|
|
except ctap.CtapError as ex:
|
|
error(f"Failed to read OpenSK info (error: {ex}")
|
|
|
|
|
|
def f_args(*params):
|
|
"""Constructs a dict from a list of arguments for sending a CBOR command.
|
|
None elements will be omitted.
|
|
:param params: Arguments, in order, to add to the command.
|
|
:return: The input parameters as a dict.
|
|
"""
|
|
return dict((i, v) for i, v in enumerate(params, 1) if v is not None)
|
|
|
|
|
|
def compute_stats(elapsed):
|
|
n = len(elapsed)
|
|
mean = sum(elapsed) / n
|
|
variance = sum((x - mean)**2 for x in elapsed) / n
|
|
std_dev = variance**0.5
|
|
return (mean, std_dev)
|
|
|
|
|
|
def get_authenticator():
|
|
devices = None
|
|
while not devices:
|
|
try:
|
|
devices = get_opensk_devices(False)
|
|
except Exception as e: # pylint: disable=broad-except
|
|
error(str(e))
|
|
check_call(["nrfjprog", "--reset", "--family", "NRF52"],
|
|
stdout=DEVNULL,
|
|
stderr=STDOUT)
|
|
sleep(0.1)
|
|
return devices[0]
|
|
|
|
|
|
def main(args):
|
|
colorama.init()
|
|
|
|
authenticator = get_authenticator()
|
|
# If the device supports it, wink to show which device we use.
|
|
if authenticator.device.capabilities & hid.CAPABILITY.WINK:
|
|
authenticator.device.wink()
|
|
aaguid = uuid.UUID(bytes=authenticator.get_info().aaguid)
|
|
check_info(authenticator)
|
|
info(f"Testing OpenSK device AAGUID {aaguid} ({authenticator.device}).")
|
|
|
|
make_durations = []
|
|
get_durations = []
|
|
|
|
for _ in tqdm(range(args.runs), file=sys.stdout):
|
|
authenticator = get_authenticator()
|
|
try:
|
|
start = datetime.datetime.now()
|
|
result = authenticator.make_credential(
|
|
client_data_hash=bytes(32),
|
|
rp=PublicKeyCredentialRpEntity(id="example.com", name="Example"),
|
|
user=PublicKeyCredentialUserEntity(id=b"diana", name="Diana"),
|
|
key_params=[HYBRID_ALGORITHM],
|
|
)
|
|
end = datetime.datetime.now()
|
|
make_delta = (end - start).total_seconds() * 1000.0
|
|
make_durations.append(make_delta)
|
|
|
|
credential_data = result.auth_data.credential_data
|
|
credential_id_length = 256 * credential_data[16] + credential_data[17]
|
|
credential_id = credential_data[18:18 + credential_id_length]
|
|
allow_list = [{"type": "public-key", "id": credential_id}]
|
|
|
|
start = datetime.datetime.now()
|
|
_ = authenticator.get_assertion(
|
|
rp_id="example.com",
|
|
client_data_hash=bytes(32),
|
|
allow_list=allow_list,
|
|
)
|
|
end = datetime.datetime.now()
|
|
get_delta = (end - start).total_seconds() * 1000.0
|
|
get_durations.append(get_delta)
|
|
|
|
with open("make_durations.txt", "a", encoding="utf-8") as file_make:
|
|
file_make.write(str(make_delta) + ",\n")
|
|
with open("get_durations.txt", "a", encoding="utf-8") as file_get:
|
|
file_get.write(str(get_delta) + ",\n")
|
|
|
|
except ctap.CtapError as ex:
|
|
message = "Failed to make a hybrid signature with OpenSK"
|
|
if ex.code.value == ctap.CtapError.ERR.INVALID_COMMAND:
|
|
error(f"{message} (unsupported command).")
|
|
elif ex.code.value == ctap.CtapError.ERR.INVALID_PARAMETER:
|
|
error(f"{message} (invalid parameter, maybe a wrong byte array size?).")
|
|
elif ex.code.value == 0xF2: # VENDOR_INTERNAL_ERROR
|
|
error(f"{message} (internal conditions not met).")
|
|
elif ex.code.value == 0xF3: # VENDOR_HARDWARE_FAILURE
|
|
error(f"{message} (internal hardware error).")
|
|
else:
|
|
error(f"{message} (unexpected error: {ex})")
|
|
except Exception as e: # pylint: disable=broad-except
|
|
error(str(e))
|
|
|
|
info(f"Successful operations: {len(make_durations)} and {len(get_durations)}")
|
|
info("\nMake Credential benchmark:")
|
|
(mean, std_dev) = compute_stats(make_durations)
|
|
info(f"Average: {mean} ms/iter (standard deviation: {std_dev} ms/iter)")
|
|
info("\nGet Assertion benchmark:")
|
|
(mean, std_dev) = compute_stats(get_durations)
|
|
info(f"Average: {mean} ms/iter (standard deviation: {std_dev} ms/iter)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"--runs",
|
|
type=int,
|
|
default=1000,
|
|
help=("How many iterations to use."),
|
|
)
|
|
main(parser.parse_args())
|