Skip to content

Commit

Permalink
feat: Sigstore as Root Keys for Signing (#658)
Browse files Browse the repository at this point in the history
* feat: SigStore Signer dependencies

Add the SigStore Signer dependencies to RSTUF CLI

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

* feat: Add SigStore Signing (offline key)

This commit enables the feature of using SigStore (public instance) for
signing the TUF Metadata.

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

* test: add unit tests for sigstore

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

* fixup! feat: Add SigStore Signing (offline key)

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

---------

Signed-off-by: Kairo Araujo <[email protected]>
Signed-off-by: Kairo de Araujo <[email protected]>
  • Loading branch information
kairoaraujo authored Aug 2, 2024
1 parent 2a9b484 commit 528c552
Show file tree
Hide file tree
Showing 11 changed files with 620 additions and 82 deletions.
3 changes: 2 additions & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ securesystemslib = "*"
click = "*"
rich = "*"
auto-click-auto = "*"
PyNaCl = "==1.5.0"
PyNaCl = "*"
requests = "*"
dynaconf = {extras = ["yaml"], version = "*"}
isort = "*"
sqlalchemy = "*"
psycopg2 = "*"
tuf = "*"
beaupy = "*"
sigstore = "*"

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

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion repository_service_tuf/cli/admin/ceremony.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import click
from rich.markdown import Markdown
from tuf.api.metadata import Metadata, Root

# TODO: Should we use the global rstuf console exclusively? We do use it for
# `console.print`, but not with `Confirm/Prompt.ask`. The latter uses a default
Expand All @@ -21,9 +20,11 @@
from repository_service_tuf.cli.admin.helpers import (
BinsRole,
CeremonyPayload,
Metadata,
Metadatas,
Role,
Roles,
Root,
Settings,
_add_root_signatures_prompt,
_configure_online_key_prompt,
Expand Down
113 changes: 102 additions & 11 deletions repository_service_tuf/cli/admin/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#
# SPDX-License-Identifier: MIT

import enum
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple
Expand All @@ -17,7 +18,17 @@
)
from rich.prompt import Confirm, IntPrompt, InvalidResponse, Prompt
from rich.table import Table
from securesystemslib.signer import CryptoSigner, Key, Signature, SSlibKey
from securesystemslib.formats import encode_canonical
from securesystemslib.hash import digest
from securesystemslib.signer import (
KEY_FOR_TYPE_AND_SCHEME,
CryptoSigner,
Key,
Signature,
SigstoreKey,
SigstoreSigner,
SSlibKey,
)
from tuf.api.metadata import (
Metadata,
Root,
Expand Down Expand Up @@ -54,6 +65,30 @@
}
DEFAULT_BINS_NUMBER = 256

# SigStore issuers supported by RSTUF
SIGSTORE_ISSUERS = [
"https://github.com/login/oauth",
"https://login.microsoft.com",
"https://accounts.google.com",
]

# SecureSystemsLib doesn't support SigstoreKey by default.
KEY_FOR_TYPE_AND_SCHEME.update(
{
("sigstore-oidc", "Fulcio"): SigstoreKey,
}
)


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

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


@dataclass
class _OnlineSettings:
Expand Down Expand Up @@ -165,10 +200,54 @@ def _load_key_from_file_prompt() -> SSlibKey:
return key


def _load_key_prompt(root: Root) -> Optional[Key]:
def _new_keyid(key: Key) -> str:
data: bytes = encode_canonical(key.to_dict()).encode()
hasher = digest("sha256")
hasher.update(data)
return hasher.hexdigest()


def _load_key_from_sigstore_prompt() -> Optional[Key]:
console.print(
"\n:warning: SigStore is not supported by all TUF Clients.\n",
justify="left",
style="italic",
)
identity = Prompt.ask("Please enter SigStore identity")
console.print(
"\n:warning: RSTUF only support SigStore public issuers.\n",
justify="left",
style="italic",
)
issuer = _select(SIGSTORE_ISSUERS)

key = SigstoreKey(
keyid="temp",
keytype="sigstore-oidc",
scheme="Fulcio",
keyval={"issuer": issuer, "identity": identity},
unrecognized_fields={KEY_NAME_FIELD: identity},
)

key.keyid = _new_keyid(key)

return key


def _load_key_prompt(
root: Root, signer_type: Optional[str] = None
) -> Optional[Key]:
"""Prompt and return Key, or None on error or if key is already loaded."""
try:
key = _load_key_from_file_prompt()
if not signer_type:
console.print("\nSelect a key type:")
signer_type = _select(ROOT_SIGNERS.values())

match signer_type:
case ROOT_SIGNERS.KEY_PEM:
key = _load_key_from_file_prompt()
case ROOT_SIGNERS.SIGSTORE:
key = _load_key_from_sigstore_prompt()

except (OSError, ValueError) as e:
console.print(f"Cannot load key: {e}")
Expand All @@ -182,10 +261,10 @@ def _load_key_prompt(root: Root) -> Optional[Key]:
return key


def _key_name_prompt(root) -> str:
def _key_name_prompt(root: Root, name: Optional[str] = None) -> str:
"""Prompt for key name until success."""
while True:
name = Prompt.ask("Please enter key name")
name = Prompt.ask("Please enter key name", default=name)
if not name:
console.print("Key name cannot be empty.")
continue
Expand Down Expand Up @@ -245,7 +324,7 @@ def _root_threshold_prompt() -> int:
return _MoreThan1Prompt.ask("Please enter root threshold")


def _select(options: list[str]) -> str:
def _select(options: List[str]) -> str:
return beaupy.select(options=options, cursor=">", cursor_style="cyan")


Expand Down Expand Up @@ -301,7 +380,9 @@ def _configure_root_keys_prompt(root: Root) -> None:
if not new_key:
continue

name = _key_name_prompt(root)
name = _key_name_prompt(
root, new_key.unrecognized_fields.get(KEY_NAME_FIELD)
)
new_key.unrecognized_fields[KEY_NAME_FIELD] = name
root.add_key(new_key, Root.type)
console.print(f"Added root key '{name}'")
Expand Down Expand Up @@ -329,7 +410,7 @@ def _configure_online_key_prompt(root: Root) -> None:
return

while True:
if new_key := _load_key_prompt(root):
if new_key := _load_key_prompt(root, signer_type=ROOT_SIGNERS.KEY_PEM):
break

name = _key_name_prompt(root)
Expand All @@ -352,8 +433,14 @@ def _add_signature_prompt(metadata: Metadata, key: Key) -> Signature:
while True:
name = key.unrecognized_fields.get(KEY_NAME_FIELD, key.keyid)
try:
signer = _load_signer_from_file_prompt(key)
# TODO: Check if the signature is valid for the key?
if key.keytype == "sigstore-oidc":
signer = SigstoreSigner.from_priv_key_uri(
"sigstore:?ambient=false", key
)
# Using Key PEM file
else:
signer = _load_signer_from_file_prompt(key)

signature = metadata.sign(signer, append=True)
break

Expand Down Expand Up @@ -423,7 +510,10 @@ def _print_root(root: Root):

key_table = Table("Role", "ID", "Name", "Signing Scheme", "Public Value")
for key in _get_root_keys(root).values():
public_value = key.keyval["public"] # SSlibKey-specific
if isinstance(key, SigstoreKey):
public_value = f"{key.keyval['identity']}@{key.keyval['issuer']}"
else:
public_value = key.keyval["public"] # SSlibKey-specific
name = key.unrecognized_fields.get(KEY_NAME_FIELD)
key_table.add_row("Root", key.keyid, name, key.scheme, public_value)

Expand Down Expand Up @@ -482,6 +572,7 @@ def _print_keys_for_signing(
f"- '{key.unrecognized_fields.get(KEY_NAME_FIELD, key.keyid)}'"
)
keys.append(key)
console.print()

return keys

Expand Down
3 changes: 2 additions & 1 deletion repository_service_tuf/cli/admin/sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import click
from rich.markdown import Markdown
from tuf.api.metadata import Metadata, Root

# TODO: Should we use the global rstuf console exclusively? We do use it for
# `console.print`, but not with `Confirm/Prompt.ask`. The latter uses a default
Expand All @@ -19,6 +18,8 @@
from repository_service_tuf.cli import console
from repository_service_tuf.cli.admin import metadata
from repository_service_tuf.cli.admin.helpers import (
Metadata,
Root,
SignPayload,
_add_signature_prompt,
_filter_root_verification_results,
Expand Down
23 changes: 22 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,41 @@ types-requests==2.32.0.20240712; python_version >= '3.8'
typing-extensions==4.12.2; python_version >= '3.8'
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'
beaupy==3.8.3.post1; 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'
cffi==1.16.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')))))
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'
hyperframe==6.0.1; python_full_version >= '3.6.1'
id==1.4.0; python_version >= '3.8'
multidict==6.0.5; python_version >= '3.7'
psycopg2==2.9.9; python_version >= '3.7'
pyasn1==0.6.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'
pyjwt==2.8.0; python_version >= '3.7'
pynacl==1.5.0; python_version >= '3.6'
pyopenssl==24.2.1; python_version >= '3.7'
python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
python-yakh==0.3.2; python_full_version >= '3.7.8' and python_full_version < '4.0.0'
questo==0.2.3; 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'
ruamel.yaml==0.18.6
ruamel.yaml.clib==0.2.8; python_version < '3.13' and platform_python_implementation == 'CPython'
securesystemslib[crypto]==1.1.0; python_version ~= '3.8'
sigstore==3.0.0; python_version >= '3.8'
sigstore-protobuf-specs==0.3.2; python_version >= '3.8'
sigstore-rekor-types==0.0.13; python_version >= '3.8'
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
sqlalchemy==2.0.31; python_version >= '3.7'
tuf==5.0.0; python_version >= '3.8'
24 changes: 23 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,29 +1,51 @@
-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'
beaupy==3.8.3.post1; 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'
certifi==2024.7.4; python_version >= '3.6'
cffi==1.16.0; python_version >= '3.8'
charset-normalizer==3.3.2; python_full_version >= '3.7.0'
click==8.1.7; python_version >= '3.7'
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')))))
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'
hyperframe==6.0.1; python_full_version >= '3.6.1'
id==1.4.0; python_version >= '3.8'
idna==3.7; python_version >= '3.5'
isort==5.13.2; python_full_version >= '3.8.0'
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'
psycopg2==2.9.9; python_version >= '3.7'
pyasn1==0.6.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'
pygments==2.18.0; python_version >= '3.8'
pyjwt==2.8.0; python_version >= '3.7'
pynacl==1.5.0; python_version >= '3.6'
pyopenssl==24.2.1; python_version >= '3.7'
python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
python-yakh==0.3.2; python_full_version >= '3.7.8' and python_full_version < '4.0.0'
questo==0.2.3; python_full_version >= '3.7.8' and python_full_version < '4.0.0'
requests==2.32.3; python_version >= '3.8'
rfc8785==0.1.3; python_version >= '3.8'
rich==13.7.1; python_full_version >= '3.7.0'
rich-click==1.8.3; python_version >= '3.7'
ruamel.yaml==0.18.6
ruamel.yaml.clib==0.2.8; python_version < '3.13' and platform_python_implementation == 'CPython'
securesystemslib[crypto]==1.1.0; python_version ~= '3.8'
sigstore==3.0.0; python_version >= '3.8'
sigstore-protobuf-specs==0.3.2; python_version >= '3.8'
sigstore-rekor-types==0.0.13; python_version >= '3.8'
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
sqlalchemy==2.0.31; python_version >= '3.7'
tuf==5.0.0; python_version >= '3.8'
typing-extensions==4.12.2; python_version >= '3.8'
Expand Down
4 changes: 4 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,11 @@ def key_selection() -> lambda *a: str:
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
Expand Down Expand Up @@ -138,6 +141,7 @@ def update_key_selection() -> lambda *a: str:
"remove", # remove key
"JimiHendrix's Key", # select key to remove
"add", # add key
"Key PEM File", # select key type
"continue", # continue
# selection for inputs (signing root key)
"JimiHendrix's Key", # select key to sign
Expand Down
Loading

0 comments on commit 528c552

Please sign in to comment.