Skip to content

Commit

Permalink
Merge pull request #464 from Nitrokey/make-credential
Browse files Browse the repository at this point in the history
fido2 make-credential: Clenaup and add rk and uv options
  • Loading branch information
robin-nitrokey authored Nov 8, 2023
2 parents f03e43e + 9d36565 commit c50a59c
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 45 deletions.
31 changes: 15 additions & 16 deletions pynitrokey/cli/fido2.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,43 +411,42 @@ def feedkernel(count: int, serial: Optional[str]) -> None:
local_print(f"entropy after: 0x{open(entropy_info_file).read().strip()}")


REQUIREMENT_CHOICE = click.Choice(["discouraged", "preferred", "required"])


@click.command()
@click.option(
"-s",
"--serial",
help="Serial number of Nitrokey to use. Prefix with 'device=' to provide device file, e.g. 'device=/dev/hidraw5'.",
)
@click.option(
"--host", help="Relying party's host", default="nitrokeys.dev", show_default=True
)
@click.option("--user", help="User ID", default="they", show_default=True)
@click.option("--pin", help="provide PIN instead of asking the user", default=None)
@click.option(
"--udp", is_flag=True, default=False, help="Communicate over UDP with software key"
"--resident-key",
help="Whether to create a resident key",
type=REQUIREMENT_CHOICE,
default="discouraged",
show_default=True,
)
@click.option(
"--prompt",
help="Prompt for user",
default="Touch your authenticator to generate a credential...",
"--user-verification",
help="Whether to perform user verification (PIN query)",
type=REQUIREMENT_CHOICE,
default="preferred",
show_default=True,
)
def make_credential(
serial: Optional[str], host: str, user: str, udp: bool, prompt: str, pin: str
host: str, user: str, resident_key: str, user_verification: str
) -> None:
"""Generate a credential.
Pass `--prompt ""` to output only the `credential_id` as hex.
"""

if not pin:
pin = AskUser.hidden("Please provide pin: ")

nkfido2.find().make_credential(
host=host,
user_id=user,
serial=serial,
output=True,
udp=udp,
resident_key=resident_key,
user_verification=user_verification,
)


Expand Down
66 changes: 37 additions & 29 deletions pynitrokey/fido2/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,23 @@
from typing import Any, Callable, List, Optional, Tuple, Union

from fido2.client import Fido2Client, UserInteraction
from fido2.cose import ES256, EdDSA
from fido2.ctap import CtapError
from fido2.ctap1 import Ctap1
from fido2.ctap2 import CredentialManagement, Ctap2
from fido2.ctap2.credman import CredentialManagement
from fido2.ctap2.pin import ClientPin
from fido2.hid import CTAPHID, CtapHidDevice, open_device
from fido2.webauthn import (
AuthenticatorSelectionCriteria,
PublicKeyCredentialCreationOptions,
PublicKeyCredentialParameters,
PublicKeyCredentialRpEntity,
PublicKeyCredentialType,
PublicKeyCredentialUserEntity,
ResidentKeyRequirement,
UserVerificationRequirement,
)
from intelhex import IntelHex

import pynitrokey.exceptions
Expand Down Expand Up @@ -240,11 +251,9 @@ def make_credential(
self,
host: str = "nitrokeys.dev",
user_id: str = "they",
serial: Optional[str] = None,
pin: Optional[str] = None,
prompt: str = "Touch your authenticator to generate a credential...",
resident_key: str = "",
user_verification: str = "",
output: bool = True,
udp: bool = False,
fingerprint_only: bool = False,
) -> str:

Expand All @@ -253,30 +262,28 @@ def make_credential(
device's model and firmware.
"""

_user_id = user_id.encode()
assert isinstance(self.client, Fido2Client)
client = self.client
assert self.client is not None

# @todo: rewrite with typing; use fido2.webauthn.PublicKeyCredentialCreationOptions
rp = {"id": host, "name": "Example RP"}
client.host = host # type: ignore
client.origin = f"https://{client.host}" # type: ignore
client.user_id = _user_id # type: ignore
user = {"id": _user_id, "name": "A. User"}
challenge = secrets.token_bytes(32)
# @todo: ... pass PublicKeyCredentialCreationOptions here
attestation_object = client.make_credential(
{
"rp": rp,
"user": user,
"challenge": challenge,
"pubKeyCredParams": [
{"type": "public-key", "alg": -8},
{"type": "public-key", "alg": -7},
],
"extensions": {"hmacCreateSecret": True},
}, # type: ignore
).attestation_object
options = PublicKeyCredentialCreationOptions(
rp=PublicKeyCredentialRpEntity(name="Example RP", id=host),
user=PublicKeyCredentialUserEntity(name="A. User", id=user_id.encode()),
challenge=secrets.token_bytes(32),
pub_key_cred_params=[
PublicKeyCredentialParameters(
type=PublicKeyCredentialType.PUBLIC_KEY, alg=EdDSA.ALGORITHM
),
PublicKeyCredentialParameters(
type=PublicKeyCredentialType.PUBLIC_KEY, alg=ES256.ALGORITHM
),
],
extensions={"hmacCreateSecret": True},
authenticator_selection=AuthenticatorSelectionCriteria(
resident_key=ResidentKeyRequirement(resident_key),
user_verification=UserVerificationRequirement(user_verification),
),
)
self.client.origin = f"https://{host}"
attestation_object = self.client.make_credential(options).attestation_object

if fingerprint_only:
if "x5c" not in attestation_object.att_stmt:
Expand All @@ -287,8 +294,9 @@ def make_credential(
return sha256(data[0]).digest().hex()

credential = attestation_object.auth_data.credential_data
# @todo: this generally will not work, fix this
credential_id = credential.credential_id # type: ignore
if not credential:
raise ValueError("No credential ID available")
credential_id = credential.credential_id
if output:
print(credential_id.hex())

Expand Down

0 comments on commit c50a59c

Please sign in to comment.