Skip to content

Commit

Permalink
Allow HSM operators (Yubkey authed) to create/delete each other's keys
Browse files Browse the repository at this point in the history
  • Loading branch information
elonen committed Jul 30, 2024
1 parent ea6a1f1 commit 1b2e11a
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 20 deletions.
29 changes: 19 additions & 10 deletions hsm-conf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,17 @@ user_keys:
- sign-eddsa # X.509 signing in Ed25519
- sign-ecdsa # X.509 signing in EC
- encrypt-cbc # General AES symmetric data encryption
- decrypt-cbc # (--||--)
- decrypt-cbc
- encrypt-ecb # (non-chained AES, not recommended for general use)
- decrypt-ecb # (--||--)
- get-pseudo-random # For generating random salts
- change-authentication-key # For changing this key's credentials
- sign-attestation-certificate # For proving some other key is protected by an HSM
- exportable-under-wrap # Allows replication of the key to another HSM for backup/HA
- decrypt-ecb
- get-pseudo-random # Generating random numbrs
- sign-attestation-certificate # Prove some other key is protected by an HSM
- exportable-under-wrap # Allow backing up of this key
- get-opaque # For getting certificates stored in the HSM
- export-wrapped # For backup
- import-wrapped # For backup restore (in case service keys are DoSed by `change-authentication-key`)
delegated_capabilities: [] # (No delegated capabilities, as this cannot create new keys to delegate to)
# TODO: Need to split this key into separate keys for different purposes? Would it give any real security benefit?
- change-authentication-key # Change this key's credentials
- delete-authentication-key # Delete this or any other auth key
- put-authentication-key # Create new auth keys (allow operators to re-create keys for each other)
delegated_capabilities: ['same'] # (copy from 'capabilities' above)


# Service keys are for automated use by services, probably less well authenticated than user keys.
Expand All @@ -131,6 +130,16 @@ service_keys:
capabilities: ['get-log-entries', 'exportable-under-wrap']
delegated_capabilities: []

# For backup / restore only. This is necessary because HSM operator user keys above are not
# members of the 'x509' domain, to disallow them from using root CA keys directly.
# This user, however, can only export and import keys, not use them.
-
label: backup-restore
id: 0x0009
domains: ['all']
capabilities: ['export-wrapped', 'import-wrapped', 'exportable-under-wrap']
delegated_capabilities: []

# Service key for NAC (Network Access Control) for
# signing 802.1X EAP-TLS certificates.
-
Expand Down
28 changes: 25 additions & 3 deletions hsm_secrets/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import click.shell_completion
from pydantic import BaseModel, ConfigDict, HttpUrl, Field, StringConstraints
from typing_extensions import Annotated
from typing import Any, Callable, List, Literal, NewType, Optional, Sequence, Union
from typing import Any, Callable, Iterable, List, Literal, NewType, Optional, Sequence, Union, cast
from yubihsm.defs import CAPABILITY, ALGORITHM # type: ignore [import]
import click
import yaml # type: ignore [import]
Expand All @@ -17,6 +17,7 @@
class NoExtraBaseModel(BaseModel):
model_config = ConfigDict(extra="forbid")

AnyCapability = Union['AsymmetricCapabilityName', 'SymmetricCapabilityName', 'WrapCapabilityName', 'HmacCapabilityName', 'AuthKeyCapabilityName', 'AuthKeyDelegatedCapabilityName', 'WrapDelegateCapabilityName']

class HSMConfig(NoExtraBaseModel):
general: 'General'
Expand Down Expand Up @@ -58,8 +59,8 @@ def find_def(self, id_or_label: Union[int, str], enforce_type: Optional[type] =
def domain_bitfield_to_nums(bitfield: int) -> set['HSMDomainNum']:
return {i+1 for i in range(16) if bitfield & (1 << i)}

@staticmethod
def capability_from_names(names: set[Union['AsymmetricCapabilityName', 'SymmetricCapabilityName', 'WrapCapabilityName', 'HmacCapabilityName', 'AuthKeyCapabilityName', 'AuthKeyDelegatedCapabilityName', 'WrapDelegateCapabilityName']]) -> CAPABILITY:
@classmethod
def capability_from_names(cls, names: Iterable[AnyCapability]) -> CAPABILITY:
capability = CAPABILITY.NONE
for name in names:
if name == "none":
Expand All @@ -73,6 +74,26 @@ def capability_from_names(names: set[Union['AsymmetricCapabilityName', 'Symmetri
raise ValueError(f"Unknown capability name: {name}")
return capability

@classmethod
def delegated_capability_from_names(
cls,
names: Iterable[Union['AuthKeyDelegatedCapabilityName', 'WrapDelegateCapabilityName']],
non_delegated_caps: Optional[Iterable[AnyCapability]] = None
) -> CAPABILITY:
"""
Convert a set of delegated capability names to a bitfield.
If 'same' is included in the names, the non-delegated capabilities must be passed in as well,
and the resulting bitfield will include both the 'names' and 'non_delegated_caps' sets.
"""
names_set = {n for n in names}
without_same: list[AnyCapability] = [cast(AnyCapability, n) for n in (names_set - {"same"})]
res = cls.capability_from_names(without_same)
if "same" in names:
assert non_delegated_caps is not None, "Cannot use 'same' in delegated capabilities without passing in non-delegated capabilities."
res |= cls.capability_from_names(non_delegated_caps)
return res


@staticmethod
def capability_to_names(capability: CAPABILITY) -> set:
names = set()
Expand Down Expand Up @@ -198,6 +219,7 @@ class HSMHmacKey(HSMObjBase):
"rewrap-from-otp-aead-key", "rewrap-to-otp-aead-key", "set-option", "sign-attestation-certificate", "sign-ecdsa",
"sign-eddsa", "sign-hmac", "sign-pkcs", "sign-pss", "sign-ssh-certificate", "unwrap-data", "verify-hmac", "wrap-data",
"decrypt-ecb", "encrypt-ecb", "decrypt-cbc", "encrypt-cbc",
"same"
]
class HSMAuthKey(HSMObjBase):
capabilities: set[AuthKeyCapabilityName]
Expand Down
20 changes: 13 additions & 7 deletions hsm_secrets/yubihsm.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,10 @@ def put_wrap_key(self, keydef: HSMWrapKey, secret: bytes) -> ObjectInfo:
label=keydef.label,
algorithm=self.conf.algorithm_from_name(keydef.algorithm),
domains=self.conf.get_domain_bitfield(keydef.domains),
capabilities=self.conf.capability_from_names(set(keydef.capabilities)),
delegated_capabilities=self.conf.capability_from_names(set(keydef.delegated_capabilities)),
capabilities=self.conf.capability_from_names(keydef.capabilities),
delegated_capabilities=self.conf.delegated_capability_from_names(
keydef.delegated_capabilities,
non_delegated_caps=keydef.capabilities),
key=secret)
return res.get_info()

Expand Down Expand Up @@ -372,7 +374,9 @@ def auth_key_put_derived(self, keydef: HSMAuthKey, password: str) -> ObjectInfo:
label=keydef.label,
domains=self.conf.get_domain_bitfield(keydef.domains),
capabilities=self.conf.capability_from_names(keydef.capabilities),
delegated_capabilities=self.conf.capability_from_names(keydef.delegated_capabilities),
delegated_capabilities=self.conf.delegated_capability_from_names(
keydef.delegated_capabilities,
non_delegated_caps=keydef.capabilities),
password=password).get_info()

def auth_key_put(self, keydef: HSMAuthKey, key_enc: bytes, key_mac: bytes) -> ObjectInfo:
Expand All @@ -384,7 +388,9 @@ def auth_key_put(self, keydef: HSMAuthKey, key_enc: bytes, key_mac: bytes) -> Ob
label=keydef.label,
domains=self.conf.get_domain_bitfield(keydef.domains),
capabilities=self.conf.capability_from_names(keydef.capabilities),
delegated_capabilities=self.conf.capability_from_names(keydef.delegated_capabilities),
delegated_capabilities=self.conf.delegated_capability_from_names(
keydef.delegated_capabilities,
non_delegated_caps=keydef.capabilities),
key_enc=key_enc,
key_mac=key_mac).get_info()

Expand All @@ -396,7 +402,7 @@ def sym_key_generate(self, keydef: HSMSymmetricKey) -> ObjectInfo:
object_id=keydef.id,
label=keydef.label,
domains=self.conf.get_domain_bitfield(keydef.domains),
capabilities=self.conf.capability_from_names(set(keydef.capabilities)),
capabilities=self.conf.capability_from_names(keydef.capabilities),
algorithm=self.conf.algorithm_from_name(keydef.algorithm)).get_info()

def asym_key_generate(self, keydef: HSMAsymmetricKey) -> ObjectInfo:
Expand All @@ -407,7 +413,7 @@ def asym_key_generate(self, keydef: HSMAsymmetricKey) -> ObjectInfo:
object_id=keydef.id,
label=keydef.label,
domains=self.conf.get_domain_bitfield(keydef.domains),
capabilities=self.conf.capability_from_names(set(keydef.capabilities)),
capabilities=self.conf.capability_from_names(keydef.capabilities),
algorithm=self.conf.algorithm_from_name(keydef.algorithm)).get_info()

def hmac_key_generate(self, keydef: HSMHmacKey) -> ObjectInfo:
Expand All @@ -418,7 +424,7 @@ def hmac_key_generate(self, keydef: HSMHmacKey) -> ObjectInfo:
object_id=keydef.id,
label=keydef.label,
domains=self.conf.get_domain_bitfield(keydef.domains),
capabilities=self.conf.capability_from_names(set(keydef.capabilities)),
capabilities=self.conf.capability_from_names(keydef.capabilities),
algorithm=self.conf.algorithm_from_name(keydef.algorithm)).get_info()

def get_pseudo_random(self, length: int) -> bytes:
Expand Down

0 comments on commit 1b2e11a

Please sign in to comment.