From e0d3b5e0daa577c7d7e00f5c31464eb6b402e8b5 Mon Sep 17 00:00:00 2001 From: stevenwdv Date: Fri, 15 Oct 2021 21:31:11 +0200 Subject: [PATCH 1/9] Add support for choosing algorithm and displaying public key to make_credential (v0.0.30 version) --- solo/cli/key.py | 20 +++++++++++++++++--- solo/hmac_secret.py | 23 +++++++++++++++++------ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/solo/cli/key.py b/solo/cli/key.py index 299c86d..3b838cd 100644 --- a/solo/cli/key.py +++ b/solo/cli/key.py @@ -19,6 +19,7 @@ from fido2.client import ClientError as Fido2ClientError from fido2.ctap1 import ApduError from fido2.ctap2 import CredentialManagement +import fido2.cose import solo import solo.fido2 @@ -134,21 +135,28 @@ def feedkernel(count, serial): default="Touch your authenticator to generate a credential...", show_default=True, ) -def make_credential(serial, host, user, udp, prompt, pin): +@click.option("--alg", default="EdDSA,ES256", help="Algorithm(s) for key, separated by ',', in order of preference") +@click.option("--no-pubkey", is_flag=True, default=False, help="Do not display public key") +def make_credential(serial, host, user, udp, prompt, pin, alg, no_pubkey): """Generate a credential. - Pass `--prompt ""` to output only the `credential_id` as hex. + Pass `--prompt "" --no-pubkey` to output only the `credential_id` as hex. """ import solo.hmac_secret + algs = [fido2.cose.CoseKey.for_name(a).ALGORITHM for a in alg.split(",")] + if None in algs: + print("Error: Unknown algorithm(s): ", [a for a, aid in zip(alg.split(","), algs) if aid is None]) + return 1 + # check for PIN if not pin: pin = getpass.getpass("PIN (leave empty for no PIN): ") if not pin: pin = None - solo.hmac_secret.make_credential( + cred_id, pk = solo.hmac_secret.make_credential( host=host, user_id=user, serial=serial, @@ -156,8 +164,14 @@ def make_credential(serial, host, user, udp, prompt, pin): prompt=prompt, udp=udp, pin=pin, + algs=algs ) + pk_bytes = pk[-2] + + if not no_pubkey: + print(f"Public key ({type(pk).__name__}) (HEX): {pk_bytes.hex()}") + @click.command() @click.option("-s", "--serial", help="Serial number of Solo use") diff --git a/solo/hmac_secret.py b/solo/hmac_secret.py index 773340a..a50ee40 100644 --- a/solo/hmac_secret.py +++ b/solo/hmac_secret.py @@ -14,6 +14,13 @@ import hashlib import secrets +import fido2.cose +from fido2.webauthn import ( + PublicKeyCredentialCreationOptions, + PublicKeyCredentialParameters, PublicKeyCredentialDescriptor, + PublicKeyCredentialType, PublicKeyCredentialRpEntity, +) + import solo.client @@ -25,15 +32,19 @@ def make_credential( prompt="Touch your authenticator to generate a credential...", output=True, udp=False, + algs=None ): + if algs is None: + algs = [fido2.cose.EdDSA.ALGORITHM, fido2.cose.ES256.ALGORITHM] + user_id = user_id.encode() client = solo.client.find(solo_serial=serial, udp=udp).get_current_fido_client() - rp = {"id": host, "name": "Example RP"} + rp = PublicKeyCredentialRpEntity(host, "Example RP") client.host = host client.origin = f"https://{client.host}" client.user_id = user_id - user = {"id": user_id, "name": "A. User"} + user = fido2.webauthn.PublicKeyCredentialUserEntity(user_id, "A. User") challenge = secrets.token_bytes(32) if prompt: @@ -45,8 +56,8 @@ def make_credential( "user": user, "challenge": challenge, "pubKeyCredParams": [ - {"type": "public-key", "alg": -8}, - {"type": "public-key", "alg": -7}, + PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, alg) + for alg in algs ], "extensions": {"hmacCreateSecret": True}, }, @@ -58,7 +69,7 @@ def make_credential( if output: print(credential_id.hex()) - return credential_id + return credential_id, credential.public_key def simple_secret( @@ -83,7 +94,7 @@ def simple_secret( # user = {"id": user_id, "name": "A. User"} credential_id = binascii.a2b_hex(credential_id) - allow_list = [{"type": "public-key", "id": credential_id}] + allow_list = [PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, credential_id)] challenge = secrets.token_bytes(32) From dacd2281fc64ee3de7c16c13aff98a97ef90834a Mon Sep 17 00:00:00 2001 From: stevenwdv Date: Fri, 15 Oct 2021 21:40:24 +0200 Subject: [PATCH 2/9] Add support for Minisign to sign-file/sign-hash and make-credential (largely from branch minisign(-v0.0.27)), via a custom CTAP command, see stevenwdv/solo:sign-hash. (v0.0.30 version) Note that sign-hash now uses HEX credentials just like make-credential, and it will respect the credential algorithm --- solo/cli/key.py | 166 +++++++++++++++++++++++++++++++++++++++---- solo/devices/base.py | 23 +++--- 2 files changed, 167 insertions(+), 22 deletions(-) diff --git a/solo/cli/key.py b/solo/cli/key.py index 3b838cd..f591f61 100644 --- a/solo/cli/key.py +++ b/solo/cli/key.py @@ -11,12 +11,14 @@ import getpass import hashlib import os +import pathlib import sys import time import click from cryptography.hazmat.primitives import hashes from fido2.client import ClientError as Fido2ClientError +from fido2.ctap import CtapError from fido2.ctap1 import ApduError from fido2.ctap2 import CredentialManagement import fido2.cose @@ -137,7 +139,13 @@ def feedkernel(count, serial): ) @click.option("--alg", default="EdDSA,ES256", help="Algorithm(s) for key, separated by ',', in order of preference") @click.option("--no-pubkey", is_flag=True, default=False, help="Do not display public key") -def make_credential(serial, host, user, udp, prompt, pin, alg, no_pubkey): +@click.option("--minisign", is_flag=True, default=False, help="Display public key in Minisign-compatible format") +@click.option("--key-file", default=None, help="File to store public key (use with --minisign)") +@click.option("--key-id", default=None, help="Key ID to write to key file (8 bytes as HEX) (use with --key-file)" + " [default: ]") +@click.option("--untrusted-comment", default=None, + help="Untrusted comment to write to public key file (use with --key-file) [default: ]") +def make_credential(serial, host, user, udp, prompt, pin, alg, no_pubkey, minisign, key_file, key_id, untrusted_comment): """Generate a credential. Pass `--prompt "" --no-pubkey` to output only the `credential_id` as hex. @@ -169,9 +177,45 @@ def make_credential(serial, host, user, udp, prompt, pin, alg, no_pubkey): pk_bytes = pk[-2] - if not no_pubkey: + if minisign: + if pk.ALGORITHM != fido2.cose.EdDSA.ALGORITHM: + print(f"Error: Minisign only supports EdDSA keys but this credential was created using {type(pk).__name__}") + return 1 + + if key_id is not None: + key_id_hex = key_id + key_id = int(key_id, 16).to_bytes(8, "little") + else: + key_id = hashlib.blake2b(cred_id).digest()[:8] + # key_id is interpreted as little endian integer and then converted to hex (omitting leading zeros) + key_id_hex = f"{int.from_bytes(key_id, 'little'):X}" + + minisign_pk = base64.b64encode(b"Ed" + key_id + pk_bytes) + if not no_pubkey: + print(f"Public key ({type(pk).__name__}) {key_id_hex} (Minisign Base64): {minisign_pk.decode()}") + + elif not no_pubkey: print(f"Public key ({type(pk).__name__}) (HEX): {pk_bytes.hex()}") + if key_file is not None: + if minisign: + if untrusted_comment is not None: + untrusted_comment_bytes = untrusted_comment.encode() + else: + untrusted_comment_bytes = b"minisign solokey public key " + key_id_hex.encode() + + with open(key_file, "wb") as f: + f.write(b"untrusted comment: ") + f.write(untrusted_comment_bytes) + f.write(b"\n") + f.write(minisign_pk) + f.write(b"\n") + + print(f"Minisign public key written to {key_file}") + + else: + print("Writing key file is only supported for minisign keys") + @click.command() @click.option("-s", "--serial", help="Serial number of Solo use") @@ -608,27 +652,123 @@ def cred_rm(pin, credential_id, serial, udp): @click.command() @click.option("--pin", help="PIN for to access key") @click.option("-s", "--serial", help="Serial number of Solo to use") +@click.option( + "--udp", is_flag=True, default=False, help="Communicate over UDP with software key" +) +@click.option( + "--prompt", + help="Prompt for user", + default="Touch your authenticator to generate a response...", + show_default=True, +) +@click.option("--minisign", is_flag=True, default=False, help="Use Minisign-compatible signatures (pre-hashed)") +@click.option("--sig-file", default=None, help="Destination file for signature" + " (.(mini)sig if empty)") +@click.option("--trusted-comment", default=None, + help="Trusted comment included in global signature (combine with --minisign)" + " [default: