Skip to content

Commit

Permalink
Extend online key support to SaaS (#675)
Browse files Browse the repository at this point in the history
* chore: add dependencies for Online Key support

Add dependencies to extend Online Key support for other key storage:

- AWS
- Azure
- GCP
- HashCorp Vault

Signed-off-by: Kairo de Araujo <[email protected]>

* feat: add support for multiple online key storage

Add support to use different (supported) key storage to the RSTUF.

These key storage vault/kms are already supported by Worker through the
Python Secure Systems Lib.

This change add the UI/UX flow to allow the user use these keys.

test: fix tests to support new changes

test: add unit test for new extended online key

Signed-off-by: Kairo de Araujo <[email protected]>

* fixup! feat: add support for multiple online key storage

Signed-off-by: Kairo de Araujo <[email protected]>

---------

Signed-off-by: Kairo de Araujo <[email protected]>
  • Loading branch information
kairoaraujo committed Aug 31, 2024
1 parent 893ab87 commit 7f71bab
Show file tree
Hide file tree
Showing 10 changed files with 486 additions and 79 deletions.
4 changes: 4 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ psycopg2 = "*"
tuf = "*"
beaupy = "*"
sigstore = "*"
boto3 = "*"
google-cloud-kms = "*"
hvac = "*"
azure-keyvault-keys = "*"

[dev-packages]
black = "*"
Expand Down
284 changes: 220 additions & 64 deletions Pipfile.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ dependencies = [
"securesystemslib[crypto]",
"tuf",
"sigstore",
"boto3",
"google-cloud-kms",
"hvac",
"azure-keyvault-keys",
]
dynamic = ["version"]

Expand Down
71 changes: 64 additions & 7 deletions repository_service_tuf/cli/admin/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,16 @@
from securesystemslib.hash import digest
from securesystemslib.signer import (
KEY_FOR_TYPE_AND_SCHEME,
AWSSigner,
AzureSigner,
CryptoSigner,
GCPSigner,
Key,
Signature,
SigstoreKey,
SigstoreSigner,
SSlibKey,
VaultSigner,
)
from tuf.api.metadata import (
Metadata,
Expand Down Expand Up @@ -80,14 +84,24 @@
)


class SIGNERS(str, enum.Enum):
@classmethod
def values(self) -> List[str]:
return [e.value for e in self]


# Root signers supported by RSTUF
class ROOT_SIGNERS(str, enum.Enum):
class ROOT_SIGNERS(SIGNERS):
KEY_PEM = "Key PEM File"
SIGSTORE = "Sigstore"

@classmethod
def values(self) -> List[str]:
return [e.value for e in self]

class ONLINE_SIGNERS(SIGNERS):
AWSKMS = "AWS KMS"
GCPKMS = "Google Cloud KMS"
AZKMS = "Azure KMS"
HV = "HashiCorp Vault"
KEY_PEM = "Key PEM File"


@dataclass
Expand Down Expand Up @@ -261,6 +275,47 @@ def _load_key_prompt(
return key


def _load_online_key_prompt(
root: Root, signer_type: str
) -> Tuple[Optional[str], Optional[Key]]:
"""Prompt and return Key, or None on error or if key is already loaded."""
try:
match signer_type:
case ONLINE_SIGNERS.KEY_PEM:
key = _load_key_from_file_prompt()
uri = f"fn:{key.keyid}"

case ONLINE_SIGNERS.AWSKMS:
uri, key = AWSSigner.import_(Prompt.ask("AWS KMS KeyID"))

case ONLINE_SIGNERS.GCPKMS:
uri, key = GCPSigner.import_(Prompt.ask("GCP KeyID"))

case ONLINE_SIGNERS.HV:
uri, key = VaultSigner.import_(
Prompt.ask("HashiCorp Key Name")
)

case ONLINE_SIGNERS.AZKMS:
azure_vault_name = Prompt.ask("Azure Vault Name")
azure_key_name = Prompt.ask("Azure Key Name")
uri, key = AzureSigner.import_(
az_vault_name=azure_vault_name,
az_key_name=azure_key_name,
)

except (OSError, ValueError) as e:
console.print(f"Cannot load key: {e}")
return None, None

# Disallow re-adding a key even if it is for a different role.
if key.keyid in root.keys:
console.print("\nKey already in use.", style="bold red")
return None, None

return uri, key


def _key_name_prompt(root: Root, name: Optional[str] = None) -> str:
"""Prompt for key name until success."""
while True:
Expand Down Expand Up @@ -409,14 +464,16 @@ def _configure_online_key_prompt(root: Root) -> None:
):
return

console.print("\nSelect Online Key type:")
while True:
if new_key := _load_key_prompt(root, signer_type=ROOT_SIGNERS.KEY_PEM):
online_key_signer = _select(ONLINE_SIGNERS.values())
uri, new_key = _load_online_key_prompt(root, online_key_signer)

if new_key:
break

name = _key_name_prompt(root)
new_key.unrecognized_fields[KEY_NAME_FIELD] = name

uri = f"fn:{new_key.keyid}"
new_key.unrecognized_fields[KEY_URI_FIELD] = uri

for role_name in ONLINE_ROLE_NAMES:
Expand Down
20 changes: 19 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,23 +71,39 @@ urllib3==2.2.2; python_version >= '3.8'
virtualenv==20.26.3; python_version >= '3.7'
annotated-types==0.7.0; python_version >= '3.8'
auto-click-auto==0.1.5; python_version >= '3.7' and python_version < '4.0'
azure-core==1.30.2; python_version >= '3.8'
azure-keyvault-keys==4.9.0; python_version >= '3.8'
beaupy==3.9.2; python_full_version >= '3.7.8' and python_full_version < '4.0.0'
betterproto==2.0.0b6; python_version >= '3.7' and python_version < '4.0'
boto3==1.35.8; python_version >= '3.8'
botocore==1.35.8; python_version >= '3.8'
cffi==1.17.0; python_version >= '3.8'
cryptography==43.0.0; python_version >= '3.7'
dnspython==2.6.1; python_version >= '3.8'
dynaconf[yaml]==3.2.6; python_version >= '3.8'
email-validator==2.2.0
emoji==2.12.1; python_version >= '3.7'
greenlet==3.0.3; python_version < '3.13' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))
google-api-core[grpc]==2.19.2; python_version >= '3.7'
google-auth==2.34.0; python_version >= '3.7'
google-cloud-kms==2.24.2; python_version >= '3.7'
googleapis-common-protos[grpc]==1.65.0; python_version >= '3.7'
grpc-google-iam-v1==0.13.1; python_version >= '3.7'
grpcio==1.66.0
grpcio-status==1.66.0
grpclib==0.4.7; python_version >= '3.7'
h2==4.1.0; python_full_version >= '3.6.1'
hpack==4.0.0; python_full_version >= '3.6.1'
hvac==2.3.0; python_version >= '3.8' and python_version < '4.0'
hyperframe==6.0.1; python_full_version >= '3.6.1'
id==1.4.0; python_version >= '3.8'
isodate==0.6.1
jmespath==1.0.1; python_version >= '3.7'
multidict==6.0.5; python_version >= '3.7'
proto-plus==1.24.0; python_version >= '3.7'
protobuf==5.27.4; python_version >= '3.8'
psycopg2==2.9.9; python_version >= '3.7'
pyasn1==0.6.0; python_version >= '3.8'
pyasn1-modules==0.4.0; python_version >= '3.8'
pycparser==2.22; python_version >= '3.8'
pydantic[email]==2.8.2; python_version >= '3.8'
pydantic-core==2.20.1; python_version >= '3.8'
Expand All @@ -99,8 +115,10 @@ python-yakh==0.3.2; python_full_version >= '3.7.8' and python_full_version < '4.
questo==0.3.0; python_full_version >= '3.7.8' and python_full_version < '4.0.0'
rfc8785==0.1.3; python_version >= '3.8'
rich-click==1.8.3; python_version >= '3.7'
rsa==4.9; python_version >= '3.6' and python_version < '4'
ruamel.yaml==0.18.6
ruamel.yaml.clib==0.2.8; python_version < '3.13' and platform_python_implementation == 'CPython'
s3transfer==0.10.2; python_version >= '3.8'
securesystemslib[crypto]==1.1.0; python_version ~= '3.8'
sigstore==3.2.0; python_version >= '3.8'
sigstore-protobuf-specs==0.3.2; python_version >= '3.8'
Expand Down
21 changes: 20 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
-i https://pypi.org/simple
annotated-types==0.7.0; python_version >= '3.8'
auto-click-auto==0.1.5; python_version >= '3.7' and python_version < '4.0'
azure-core==1.30.2; python_version >= '3.8'
azure-keyvault-keys==4.9.0; python_version >= '3.8'
beaupy==3.9.2; python_full_version >= '3.7.8' and python_full_version < '4.0.0'
betterproto==2.0.0b6; python_version >= '3.7' and python_version < '4.0'
boto3==1.35.8; python_version >= '3.8'
botocore==1.35.8; python_version >= '3.8'
cachetools==5.5.0; python_version >= '3.7'
certifi==2024.7.4; python_version >= '3.6'
cffi==1.17.0; python_version >= '3.8'
charset-normalizer==3.3.2; python_full_version >= '3.7.0'
Expand All @@ -12,20 +17,32 @@ dnspython==2.6.1; python_version >= '3.8'
dynaconf[yaml]==3.2.6; python_version >= '3.8'
email-validator==2.2.0
emoji==2.12.1; python_version >= '3.7'
greenlet==3.0.3; python_version < '3.13' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))
google-api-core[grpc]==2.19.2; python_version >= '3.7'
google-auth==2.34.0; python_version >= '3.7'
google-cloud-kms==2.24.2; python_version >= '3.7'
googleapis-common-protos[grpc]==1.65.0; python_version >= '3.7'
grpc-google-iam-v1==0.13.1; python_version >= '3.7'
grpcio==1.66.0
grpcio-status==1.66.0
grpclib==0.4.7; python_version >= '3.7'
h2==4.1.0; python_full_version >= '3.6.1'
hpack==4.0.0; python_full_version >= '3.6.1'
hvac==2.3.0; python_version >= '3.8' and python_version < '4.0'
hyperframe==6.0.1; python_full_version >= '3.6.1'
id==1.4.0; python_version >= '3.8'
idna==3.8; python_version >= '3.6'
isodate==0.6.1
isort==5.13.2; python_full_version >= '3.8.0'
jmespath==1.0.1; python_version >= '3.7'
markdown-it-py==3.0.0; python_version >= '3.8'
mdurl==0.1.2; python_version >= '3.7'
multidict==6.0.5; python_version >= '3.7'
platformdirs==4.2.2; python_version >= '3.8'
proto-plus==1.24.0; python_version >= '3.7'
protobuf==5.27.4; python_version >= '3.8'
psycopg2==2.9.9; python_version >= '3.7'
pyasn1==0.6.0; python_version >= '3.8'
pyasn1-modules==0.4.0; python_version >= '3.8'
pycparser==2.22; python_version >= '3.8'
pydantic[email]==2.8.2; python_version >= '3.8'
pydantic-core==2.20.1; python_version >= '3.8'
Expand All @@ -40,8 +57,10 @@ requests==2.32.3; python_version >= '3.8'
rfc8785==0.1.3; python_version >= '3.8'
rich==13.8.0; python_full_version >= '3.7.0'
rich-click==1.8.3; python_version >= '3.7'
rsa==4.9; python_version >= '3.6' and python_version < '4'
ruamel.yaml==0.18.6
ruamel.yaml.clib==0.2.8; python_version < '3.13' and platform_python_implementation == 'CPython'
s3transfer==0.10.2; python_version >= '3.8'
securesystemslib[crypto]==1.1.0; python_version ~= '3.8'
sigstore==3.2.0; python_version >= '3.8'
sigstore-protobuf-specs==0.3.2; python_version >= '3.8'
Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def key_selection() -> lambda *a: str:
"my rsa key", # select key to remove
"continue", # continue
# selections for input_step4
"Key PEM File", # select Online Key type
"JimiHendrix's Key", # select key to sign
"JanisJoplin's Key", # select key to sign
"continue", # continue
Expand Down Expand Up @@ -143,6 +144,7 @@ def update_key_selection() -> lambda *a: str:
"add", # add key
"Key PEM File", # select key type
"continue", # continue
"Key PEM File", # select Online Key type
# selection for inputs (signing root key)
"JimiHendrix's Key", # select key to sign
"JanisJoplin's Key", # select key to sign
Expand Down
26 changes: 24 additions & 2 deletions tests/unit/cli/admin/test_ceremony.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def test_ceremony_with_dry_run_and_custom_out_pem_and_sigstore_keys(
"remove", # remove key
"my rsa key", # select key to remove
"continue", # continue
"Key PEM File", # select Online Key type
# selections for input_step4
"JanisJoplin's Key", # select key to sign
"[email protected]", # select key to sign
Expand Down Expand Up @@ -303,7 +304,6 @@ def test_ceremony_online_key_one_of_root_keys(
self,
monkeypatch,
ceremony_inputs,
key_selection,
patch_getpass,
patch_utcnow,
):
Expand All @@ -315,8 +315,30 @@ def test_ceremony_online_key_one_of_root_keys(
"Online Key", # Please enter a key name
]

selection_options = iter(
(
# selections for input_step4
"Key PEM File", # select key type
"add", # add key
"Key PEM File", # select key type
"add", # add key
"Key PEM File", # select key type
"remove", # remove key
"my rsa key", # select key to remove
"continue", # continue
# selections for input_step4
"Key PEM File", # select Online Key type
"Key PEM File", # select Online Key type
"JimiHendrix's Key", # select key to sign
"JanisJoplin's Key", # select key to sign
"continue", # continue
)
)
# public keys and signing keys selection options
monkeypatch.setattr(f"{_HELPERS}._select", key_selection)
monkeypatch.setattr(
f"{_HELPERS}._select",
pretend.call_recorder(lambda *a: next(selection_options)),
)

result = invoke_command(
ceremony.ceremony,
Expand Down
Loading

0 comments on commit 7f71bab

Please sign in to comment.