Skip to content

Commit

Permalink
Merge pull request Nitrokey#32 from solokeys/challenge-response
Browse files Browse the repository at this point in the history
Add challenge-response via hmac-secret
  • Loading branch information
nickray authored Aug 29, 2019
2 parents e2a81a2 + 3ab2f80 commit b29c5c0
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
2 changes: 1 addition & 1 deletion solo/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.13
0.0.14
36 changes: 36 additions & 0 deletions solo/cli/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <credential-id> <challenge>
```
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(
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion solo/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
78 changes: 78 additions & 0 deletions solo/hmac_secret.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
#
# Copyright 2019 SoloKeys Developers
#
# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
# http://opensource.org/licenses/MIT>, 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())

0 comments on commit b29c5c0

Please sign in to comment.