From 3ab2f803b20c017b23fc7e88b7a1b52fd70f78a1 Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Fri, 30 Aug 2019 01:44:45 +0200 Subject: [PATCH] Add challenge-response via hmac-secret --- CHANGELOG.md | 4 +++ solo/VERSION | 2 +- solo/cli/key.py | 36 +++++++++++++++++++++ solo/client.py | 3 +- solo/hmac_secret.py | 78 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 solo/hmac_secret.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 595f5d77..a2c08ad5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.0.14] - 2019-08-30 +### Added +- challenge-response via `hmac-secret` + ## [0.0.13] - 2019-08-19 ### Changed - implement passing PIN to `solo key verify` diff --git a/solo/VERSION b/solo/VERSION index 43b29618..9789c4cc 100644 --- a/solo/VERSION +++ b/solo/VERSION @@ -1 +1 @@ -0.0.13 +0.0.14 diff --git a/solo/cli/key.py b/solo/cli/key.py index 6409fb54..6386ecd1 100644 --- a/solo/cli/key.py +++ b/solo/cli/key.py @@ -107,6 +107,41 @@ def feedkernel(count, serial): print(f"Entropy after: 0x{open(entropy_info_file).read().strip()}") +@click.command() +@click.option("-s", "--serial", help="Serial number of Solo use") +@click.option("--credential-id", help="Pre-registered credential ID (hex)") +@click.option("--relying-party", help="Relying party", default="example.org") +@click.option("--user-id", help="User ID", default="userid") +@click.argument("challenge") +def challenge_response(serial, credential_id, relying_party, user_id, challenge): + """Uses `hmac-secret` to implement a challenge-response mechanism. + + We abuse hmac-secret, which gives us `HMAC(K, hash(challenge))`, where `K` + is a secret tied to the `credential_id`. We hash the challenge first, since + a 32 byte value is expected (in original usage, it's a salt). + + This means that we first need to setup a credential_id (this depends on the + specific authenticator used). Once this is done, we can directly get the + challenge response via + + ``` + solo key challenge-response --credential-id + ``` + + If so desired, user and relying party can be changed from the defaults. + """ + + import solo.hmac_secret + + solo.hmac_secret.response = solo.hmac_secret.simple_secret( + challenge, + credential_id=credential_id, + relying_party=relying_party, + user_id=user_id, + serial=serial, + ) + + @click.command() @click.option("-s", "--serial", help="Serial number of Solo use") @click.option( @@ -309,6 +344,7 @@ def wink(serial, udp): rng.add_command(hexbytes) rng.add_command(raw) rng.add_command(feedkernel) +key.add_command(challenge_response) key.add_command(reset) key.add_command(update) key.add_command(probe) diff --git a/solo/client.py b/solo/client.py index eec34c40..caf1741a 100644 --- a/solo/client.py +++ b/solo/client.py @@ -68,6 +68,7 @@ class SoloClient: def __init__(self,): self.origin = "https://example.org" self.host = "example.org" + self.user_id = b"they" self.exchange = self.exchange_hid self.do_reboot = True @@ -210,7 +211,7 @@ def reset(self,): def make_credential(self, pin=None): rp = {"id": self.host, "name": "example site"} - user = {"id": b"abcdef", "name": "example user"} + user = {"id": self.user_id, "name": "example user"} challenge = "Y2hhbGxlbmdl" attest, data = self.client.make_credential( rp, user, challenge, exclude_list=[], pin=pin diff --git a/solo/hmac_secret.py b/solo/hmac_secret.py new file mode 100644 index 00000000..c9a73a8e --- /dev/null +++ b/solo/hmac_secret.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2019 SoloKeys Developers +# +# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +# copied, modified, or distributed except according to those terms. + + +import binascii +import hashlib + +from fido2.extensions import HmacSecretExtension + +import solo.client + + +def simple_secret( + secret_input, + credential_id=None, + relying_party="example.org", + user_id="they", + serial=None, + pin=None, +): + user_id = user_id.encode() + + client = solo.client.find(solo_serial=serial).client + hmac_ext = HmacSecretExtension(client.ctap2) + + if credential_id is None: + rp = {"id": relying_party, "name": "Example RP"} + client.rp = relying_party + client.origin = f"https://{client.rp}" + client.user_id = user_id + user = {"id": user_id, "name": "A. User"} + # challenge = "Y2hhbGxlbmdl" + challenge = "123" + + print("Touch your authenticator to generate a credential...") + attestation_object, client_data = client.make_credential( + rp, user, challenge, extensions=hmac_ext.create_dict(), pin=pin + ) + credential = attestation_object.auth_data.credential_data + credential_id = credential.credential_id + + # Show credential_id for convenience + print(f"credential ID (hex-encoded):") + print(credential_id.hex()) + else: + credential_id = binascii.a2b_hex(credential_id) + + allow_list = [{"type": "public-key", "id": credential_id}] + + # challenge = 'Q0hBTExFTkdF' # Use a new challenge for each call. + challenge = "abc" + + # Generate a salt for HmacSecret: + + h = hashlib.sha256() + h.update(secret_input.encode()) + salt = h.digest() + # print(f"salt = {salt.hex()}") + + print("Touch your authenticator to generate the response...") + assertions, client_data = client.get_assertion( + relying_party, + challenge, + allow_list, + extensions=hmac_ext.get_dict(salt), + pin=pin, + ) + + assertion = assertions[0] # Only one cred in allowList, only one response. + secret = hmac_ext.results_for(assertion.auth_data)[0] + print("hmac-secret (hex-encoded):") + print(secret.hex())