From 68a2328afae1e442b466c1b5c3d80ff858a2bcfe Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Fri, 30 Aug 2019 15:09:56 +0200 Subject: [PATCH] Separate make-credential from challenge-response --- CHANGELOG.md | 8 ++++ README.md | 23 ++++++++++++ solo/VERSION | 2 +- solo/cli/key.py | 61 ++++++++++++++++++++++-------- solo/hmac_secret.py | 92 +++++++++++++++++++++++++++------------------ 5 files changed, 133 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2c08ad5..ca7624cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.0.15] - 2019-08-30 +### Added +- `solo.hmac_secret.make_credential` method +- separate `solo key make-credential` CLI target +### Changed +- remove credential generation from `solo.hmac_secret.simple_secret` +- demand `credential_id` in `solo key challenge-response` + ## [0.0.14] - 2019-08-30 ### Added - challenge-response via `hmac-secret` diff --git a/README.md b/README.md index f7efd4fd..fa5bb909 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,29 @@ Comprehensive documentation coming, for now these are the main components - `solo.dfu`: connect to Solo Hacker in dfu mode (disabled on Solo Secure keys) - `solo.cli`: implementation of the `solo` command line interface +## Challenge-Response + +By abuse of the `hmac-secret` extension, we can generate static challenge responses, +which are scoped to a credential. A use case might be e.g. unlocking a LUKS-encrypted drive. + +**DANGER** The generated reponses depend on both the key and the credential. +There is no way to extract or backup from the physical key, so if you intend to use the +"response" as a static password, make sure to store it somewhere separately, e.g. on paper. + +**DANGER** Also, if you generate a new credential with the same `(host, user_id)` pair, it will likely +overwrite the old credential, and you lose the capability to generate the original responses +too. + +**DANGER** This functionality has not been sufficiently debugged, please generate GitHub issues +if you detect anything. + +There are two steps: + +1. Generate a credential. This can be done with `solo key make-credential`, storing the + (hex-encoded) generated `credential_id` for the next step. +2. Pick a challenge, and generate the associated response. This can be done with + `solo key challenge-response `. + ## License Licensed under either of diff --git a/solo/VERSION b/solo/VERSION index 9789c4cc..ceddfb28 100644 --- a/solo/VERSION +++ b/solo/VERSION @@ -1 +1 @@ -0.0.14 +0.0.15 diff --git a/solo/cli/key.py b/solo/cli/key.py index 6386ecd1..3001e775 100644 --- a/solo/cli/key.py +++ b/solo/cli/key.py @@ -109,36 +109,66 @@ def feedkernel(count, serial): @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.option( + "--host", help="Relying party's host", default="solokeys.dev", show_default=True +) +@click.option("--user", help="User ID", default="they", show_default=True) +@click.option( + "--prompt", + help="Prompt for user", + default="Touch your authenticator to generate a credential...", + show_default=True, +) +def make_credential(serial, host, user, prompt): + """Generate a credential. + + Pass `--prompt ""` to output only the `credential_id` as hex. + """ + + import solo.hmac_secret + + solo.hmac_secret.make_credential( + host=host, user_id=user, serial=serial, output=True, prompt=prompt + ) + + +@click.command() +@click.option("-s", "--serial", help="Serial number of Solo use") +@click.option("--host", help="Relying party's host", default="solokeys.dev") +@click.option("--user", help="User ID", default="they") +@click.option( + "--prompt", + help="Prompt for user", + default="Touch your authenticator to generate a reponse...", + show_default=True, +) +@click.argument("credential-id") @click.argument("challenge") -def challenge_response(serial, credential_id, relying_party, user_id, challenge): +def challenge_response(serial, host, user, prompt, credential_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 - ``` + This means that we first need to setup a credential_id; this depends on the + specific authenticator used. To do this, use `solo key make-credential`. If so desired, user and relying party can be changed from the defaults. + + The prompt can be suppressed using `--prompt ""`. """ import solo.hmac_secret - solo.hmac_secret.response = solo.hmac_secret.simple_secret( + solo.hmac_secret.simple_secret( + credential_id, challenge, - credential_id=credential_id, - relying_party=relying_party, - user_id=user_id, + host=host, + user_id=user, serial=serial, + prompt=prompt, + output=True, ) @@ -344,6 +374,7 @@ def wink(serial, udp): rng.add_command(hexbytes) rng.add_command(raw) rng.add_command(feedkernel) +key.add_command(make_credential) key.add_command(challenge_response) key.add_command(reset) key.add_command(update) diff --git a/solo/hmac_secret.py b/solo/hmac_secret.py index c9a73a8e..71ef1197 100644 --- a/solo/hmac_secret.py +++ b/solo/hmac_secret.py @@ -10,69 +10,87 @@ import binascii import hashlib +import secrets from fido2.extensions import HmacSecretExtension import solo.client +def make_credential( + host="solokeys.dev", + user_id="they", + serial=None, + pin=None, + prompt="Touch your authenticator to generate a credential...", + output=True, +): + user_id = user_id.encode() + client = solo.client.find(solo_serial=serial).client + + rp = {"id": host, "name": "Example RP"} + client.host = host + client.origin = f"https://{client.host}" + client.user_id = user_id + user = {"id": user_id, "name": "A. User"} + challenge = secrets.token_hex(32) + + if prompt: + print(prompt) + + hmac_ext = HmacSecretExtension(client.ctap2) + 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 + if output: + print(credential_id.hex()) + + return credential_id + + def simple_secret( + credential_id, secret_input, - credential_id=None, - relying_party="example.org", + host="solokeys.dev", user_id="they", serial=None, pin=None, + prompt="Touch your authenticator to generate a reponse...", + output=True, ): 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) + # rp = {"id": host, "name": "Example RP"} + client.host = host + client.origin = f"https://{client.host}" + client.user_id = user_id + # user = {"id": user_id, "name": "A. User"} + 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: + challenge = secrets.token_hex(32) h = hashlib.sha256() h.update(secret_input.encode()) salt = h.digest() - # print(f"salt = {salt.hex()}") - print("Touch your authenticator to generate the response...") + if prompt: + print(prompt) + assertions, client_data = client.get_assertion( - relying_party, - challenge, - allow_list, - extensions=hmac_ext.get_dict(salt), - pin=pin, + host, 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()) + response = hmac_ext.results_for(assertion.auth_data)[0] + if output: + print(response.hex()) + + return response