Skip to content

Commit

Permalink
Merge pull request Nitrokey#33 from solokeys/improve-challenge-response
Browse files Browse the repository at this point in the history
Separate make-credential from challenge-response
  • Loading branch information
nickray authored Aug 30, 2019
2 parents b29c5c0 + 68a2328 commit 0ac200b
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 53 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <credential_id> <challenge>`.

## License

Licensed under either of
Expand Down
2 changes: 1 addition & 1 deletion solo/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.14
0.0.15
61 changes: 46 additions & 15 deletions solo/cli/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <credential-id> <challenge>
```
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,
)


Expand Down Expand Up @@ -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)
Expand Down
92 changes: 55 additions & 37 deletions solo/hmac_secret.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 0ac200b

Please sign in to comment.