Files
OpenSK/benchmarks.py
2022-10-20 23:19:15 +02:00

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())