Skip to content

Commit

Permalink
SD: Add "replace-kvn" to key import/generate
Browse files Browse the repository at this point in the history
  • Loading branch information
dainnilsson committed Jun 3, 2024
1 parent cb88d32 commit 1b87964
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 18 deletions.
25 changes: 25 additions & 0 deletions tests/device/cli/test_securitydomain.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,31 @@ def preconditions(ykman_cli):
ykman_cli("sd", "reset", "-f")


def test_replace_kvn(ykman_cli):
key = "01" * 16
keys = f"{key}:{key}:{key}"

# Replace default SCP03 keyset
ykman_cli("--scp-sd", "1", "0", "sd", "keys", "import", "scp03", "2", keys)

# Generate new SCP11a key
ykman_cli("--scp", keys, "sd", "keys", "generate", "scp11a", "3", "-")

for i in range(3, 8):
ykman_cli(
"--scp",
keys,
"sd",
"keys",
"generate",
"scp11a",
str(i + 1),
"-r",
str(i),
"-",
)


def test_scp11a(ykman_cli):
with pytest.raises(ValueError):
with open_file("scp/oce.pfx") as f:
Expand Down
24 changes: 14 additions & 10 deletions tests/device/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ def _device(pytestconfig):
serial = None
else:
pytest.skip("No serial specified for device tests")

version_str = pytestconfig.getoption("use_version")
if version_str:
version = Version.from_string(version_str)

# Monkey patch all parsing of Version to use the supplied value
# N.B. There are some instances where ideally we would replace the version,
# but we don't really care
def get_version(cls, data):
return version

Version.from_bytes = classmethod(get_version)
Version.from_string = classmethod(get_version)

reader = pytestconfig.getoption("reader")
if reader:
readers = list_devices(reader)
Expand All @@ -38,16 +52,6 @@ def _device(pytestconfig):
dev, info = devices[0]
if info.serial != serial:
pytest.exit("Device serial does not match: %d != %r" % (serial, info.serial))
version = pytestconfig.getoption("use_version")
if version:
info.version = Version.from_string(version)

# Monkey patch all parsing of Version to use the supplied value
def get_version(cls, data):
return info.version

Version.from_bytes = classmethod(get_version)
Version.from_string = classmethod(get_version)

return dev, info

Expand Down
42 changes: 34 additions & 8 deletions ykman/_cli/securitydomain.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,16 @@ def info(ctx):
data: List[Any] = []
cas = sd.get_supported_ca_identifiers()
for ref in sd.get_key_information().keys():
if ref.kid < 0x10: # SCP03
data.append(f"{ref}")
if ref.kid == 1: # SCP03
data.append(
{
f"SCP03 (KID=0x01-0x03, KVN=0x{ref.kvn:02X})": [
"Default key set" if ref.kvn == 0xFF else "Imported key set"
]
}
)
elif ref.kid in (2, 3): # SCP03 always in full key sets
continue
else: # SCP11
inner: Dict[str, Any] = {}
if ref in cas:
Expand All @@ -116,9 +124,13 @@ def info(ctx):
]
except ApduError:
pass
data.append({ref: inner})
try:
name = ScpKid(ref.kid).name
except ValueError:
name = "SCP11 OCE CA"
data.append({f"{name} (KID=0x{ref.kid:02X}, KVN=0x{ref.kvn:02X})": inner})

click.echo("\n".join(pretty_print(data)))
click.echo("\n".join(pretty_print({"SCP keys": data})))


@securitydomain.command()
Expand Down Expand Up @@ -202,7 +214,14 @@ def convert(self, value, param, ctx):
@click.pass_context
@click_key_argument
@click.argument("public-key-output", type=click.File("wb"), metavar="PUBLIC-KEY")
def generate_key(ctx, key, public_key_output):
@click.option(
"-r",
"--replace-kvn",
type=HexIntParamType(),
default=0,
help="replace an existing key of the same type (same KID)",
)
def generate_key(ctx, key, public_key_output, replace_kvn):
"""
Generate an asymmetric key pair.
Expand All @@ -222,7 +241,7 @@ def generate_key(ctx, key, public_key_output):
session = ctx.obj["session"]

try:
public_key = session.generate_ec_key(key)
public_key = session.generate_ec_key(key, replace_kvn=replace_kvn)
except ApduError as e:
if e.sw == SW.NO_SPACE:
raise CliFail("No space left for SCP keys")
Expand All @@ -246,7 +265,14 @@ def generate_key(ctx, key, public_key_output):
@click_key_argument
@click.argument("input", metavar="INPUT")
@click.option("-p", "--password", help="password used to decrypt the file (if needed)")
def import_key(ctx, key, input, password):
@click.option(
"-r",
"--replace-kvn",
type=HexIntParamType(),
default=0,
help="replace an existing key of the same type (same KID)",
)
def import_key(ctx, key, input, password, replace_kvn):
"""
Import a key or certificate.
Expand Down Expand Up @@ -309,7 +335,7 @@ def import_key(ctx, key, input, password):
raise CliFail(f"Invalid value for KID={key.kid:x}")

try:
session.put_key(key, target)
session.put_key(key, target, replace_kvn)
except ApduError as e:
if e.sw == SW.NO_SPACE:
raise CliFail("No space left for SCP keys")
Expand Down
7 changes: 7 additions & 0 deletions yubikit/oath.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Version,
Tlv,
BadResponseError,
NotSupportedError,
)
from .core.smartcard import AID, SmartCardConnection, SmartCardProtocol, ScpKeyParams

Expand Down Expand Up @@ -273,6 +274,8 @@ def __init__(
)

if scp_key_params:
if (5, 0, 0) <= self._version < (5, 6, 3):
raise NotSupportedError("SCP for OATH requires YubiKey 5.6.3 or later")
self.protocol.init_scp(scp_key_params)
self._scp_params = scp_key_params

Expand Down Expand Up @@ -424,6 +427,7 @@ def rename_credential(
:param name: The new name of the credential.
:param issuer: The credential issuer.
"""
logger.debug(f"Renaming credential '{credential_id!r}' to '{issuer}:{name}'")
require_version(self.version, (5, 3, 1))
_, _, period = _parse_cred_id(credential_id, OATH_TYPE.TOTP)
new_id = _format_cred_id(issuer, name, OATH_TYPE.TOTP, period)
Expand All @@ -435,6 +439,7 @@ def rename_credential(

def list_credentials(self) -> List[Credential]:
"""List OATH credentials."""
logger.debug("Listing OATH credentials...")
creds = []
for tlv in Tlv.parse_list(self.protocol.send_apdu(0, INS_LIST, 0, 0)):
data = Tlv.unpack(TAG_NAME_LIST, tlv)
Expand All @@ -454,6 +459,7 @@ def calculate(self, credential_id: bytes, challenge: bytes) -> bytes:
:param credential_id: The id of the credential.
:param challenge: The challenge.
"""
logger.debug(f"Calculating response for credential: {credential_id!r}")
resp = Tlv.unpack(
TAG_RESPONSE,
self.protocol.send_apdu(
Expand All @@ -471,6 +477,7 @@ def delete_credential(self, credential_id: bytes) -> None:
:param credential_id: The id of the credential.
"""
logger.debug(f"Deleting crededential: {credential_id!r}")
self.protocol.send_apdu(0, INS_DELETE, 0, 0, Tlv(TAG_NAME, credential_id))
logger.info("Credential deleted")

Expand Down
2 changes: 2 additions & 0 deletions yubikit/securitydomain.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
AID,
SmartCardConnection,
SmartCardProtocol,
ApduFormat,
ApduError,
SW,
ScpProcessor,
Expand Down Expand Up @@ -100,6 +101,7 @@ class SecurityDomainSession:
def __init__(self, connection: SmartCardConnection):
self.protocol = SmartCardProtocol(connection)
self.protocol.select(AID.SECURE_DOMAIN)
self.protocol.apdu_format = ApduFormat.EXTENDED
logger.debug("SecurityDomain session initialized")

def authenticate(self, key_params: ScpKeyParams) -> None:
Expand Down

0 comments on commit 1b87964

Please sign in to comment.