Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sigstore for maintainers #78

Merged
merged 4 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 46 additions & 10 deletions playground/signer/playground_sign/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from typing import Generator
import click

from securesystemslib.signer import HSMSigner, Key
from securesystemslib.signer import HSMSigner, Key, SigstoreSigner

from playground_sign._signer_repository import SignerRepository

Expand Down Expand Up @@ -65,13 +65,46 @@ def signing_event(name: str, config: SignerConfig) -> Generator[SignerRepository
git_expect(["checkout", "-"])


def get_signing_key_input(message: str) -> Key:
# TODO use value_proc argument to validate the input
click.prompt(message, default=True, show_default=False)
try:
_, key = HSMSigner.import_()
except Exception as e:
raise click.ClickException(f"Failed to read HW key: {e}")
def get_signing_key_input() -> Key:
click.echo(
"\nConfiguring signing key\n"
" 1. Sigstore (OpenID Connect)\n"
" 2. Yubikey"
)
choice = click.prompt(
bold("Please choose the type of signing key you would like to use"),
type=click.IntRange(1,2),
default=1,
)

if choice == 1:
identity = click.prompt(bold("Please enter your email address"))
jku marked this conversation as resolved.
Show resolved Hide resolved
click.echo(
f" 1. GitHub\n"
f" 2. Google\n"
f" 3. Microsoft"
)
issuer_id = click.prompt(
bold("Please choose the identity issuer"),
type=click.IntRange(1,3),
default=1,
)
if issuer_id == 1:
issuer = "https://github.com/login/oauth"
elif issuer_id == 2:
issuer = "https://accounts.google.com"
else:
issuer = "https://login.microsoftonline.com"
jku marked this conversation as resolved.
Show resolved Hide resolved
try:
_, key = SigstoreSigner.import_(identity, issuer, ambient=False)
jku marked this conversation as resolved.
Show resolved Hide resolved
except Exception as e:
raise click.ClickException(f"Failed to create Sigstore key: {e}")
else:
click.prompt(bold("Please insert your Yubikey and press enter"), default=True, show_default=False)
try:
_, key = HSMSigner.import_()
except Exception as e:
raise click.ClickException(f"Failed to read HW key: {e}")

return key

Expand All @@ -83,7 +116,7 @@ def get_secret_input(secret: str, role: str) -> str:
if not sys.stdin.isatty():
return sys.stdin.readline().rstrip()

return click.prompt(msg, hide_input=True)
return click.prompt(bold(msg), hide_input=True)


def git(cmd: list[str]) -> str:
Expand All @@ -101,4 +134,7 @@ def git_expect(cmd: list[str]) -> str:

def git_echo(cmd: list[str]):
cmd = ["git"] + cmd
subprocess.run(cmd, check=True, text=True)
subprocess.run(cmd, check=True, text=True)

def bold(text: str) -> str:
return click.style(text, bold=True)
18 changes: 15 additions & 3 deletions playground/signer/playground_sign/_signer_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from datetime import datetime, timedelta
from typing import Callable
from securesystemslib.exceptions import UnverifiedSignatureError
from securesystemslib.signer import Signature, Signer
from securesystemslib.signer import Signature, Signer, SigstoreKey, SigstoreSigner, KEY_FOR_TYPE_AND_SCHEME, SIGNER_FOR_URI_SCHEME

from tuf.api.exceptions import UnsignedMetadataError
from tuf.api.metadata import DelegatedRole, Delegations, Key, Metadata, Root, TargetFile, Targets
Expand All @@ -23,6 +23,10 @@

logger = logging.getLogger(__name__)

KEY_FOR_TYPE_AND_SCHEME[("sigstore-oidc", "Fulcio")] = SigstoreKey
SIGNER_FOR_URI_SCHEME[SigstoreSigner.SCHEME] = SigstoreSigner


@unique
class SignerState(Enum):
NO_ACTION = 0,
Expand Down Expand Up @@ -130,6 +134,7 @@ def __init__(self, dir: str, prev_dir: str, user_name: str, secret_func: Callabl
self._prev_dir = prev_dir
self._get_secret = secret_func
self._invites: dict[str, list[str]] = {}
self._signers: dict[str, Signer] = {}

# read signing event state file (invites)
state_file = os.path.join(self._dir, ".signing-event-state")
Expand Down Expand Up @@ -244,15 +249,22 @@ def _sign(self, role: str, md: Metadata, key: Key) -> None:
def secret_handler(secret: str) -> str:
return self._get_secret(secret, role)

signer = Signer.from_priv_key_uri("hsm:", key, secret_handler)
if key.keyid not in self._signers:
# TODO Get key uri from .playground-sign.ini, avoid if-else here
if key.keytype == "sigstore-oidc":
self._signers[key.keyid] = Signer.from_priv_key_uri("sigstore:?ambient=false", key, secret_handler)
else:
self._signers[key.keyid] = Signer.from_priv_key_uri("hsm:", key, secret_handler)

signer = self._signers[key.keyid]

while True:
try:
md.sign(signer, True)
break
except UnsignedMetadataError:
print(f"Failed to sign {role} with {self.user_name} key. Try again?")


def _write(self, role: str, md: Metadata) -> None:
filename = self._get_filename(role)

Expand Down
39 changes: 22 additions & 17 deletions playground/signer/playground_sign/delegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from securesystemslib.signer import GCPSigner, SigstoreKey, SSlibKey, KEY_FOR_TYPE_AND_SCHEME

from playground_sign._common import (
bold,
get_signing_key_input,
git_expect,
git_echo,
Expand Down Expand Up @@ -38,10 +39,12 @@ def _get_offline_input(
config = copy.deepcopy(config)
click.echo(f"\nConfiguring role {role}")
while True:
choice = click.prompt(
click.echo(
f" 1. Configure signers: [{', '.join(config.signers)}], requiring {config.threshold} signatures\n"
f" 2. Configure expiry: Role expires in {config.expiry_period} days, re-signing starts {config.signing_period} days before expiry\n"
"Please choose an option or press enter to continue",
f" 2. Configure expiry: Role expires in {config.expiry_period} days, re-signing starts {config.signing_period} days before expiry"
)
choice = click.prompt(
bold("Please choose an option or press enter to continue"),
type=click.IntRange(0, 2),
default=0,
show_default=False,
Expand All @@ -51,7 +54,7 @@ def _get_offline_input(
if choice == 1:
# TODO use value_proc argument to validate the input
response = click.prompt(
f"Please enter list of {role} signers",
bold(f"Please enter list of {role} signers"),
default=", ".join(config.signers)
)
config.signers.clear()
Expand All @@ -66,18 +69,18 @@ def _get_offline_input(
else:
# TODO use value_proc argument to validate threshold is [1-len(new_signers)]
config.threshold = click.prompt(
f"Please enter {role} threshold",
bold(f"Please enter {role} threshold"),
type=int,
default=config.threshold
)
elif choice == 2:
config.expiry_period = click.prompt(
f"Please enter {role} expiry period in days",
bold(f"Please enter {role} expiry period in days"),
type=int,
default=config.expiry_period,
)
config.signing_period = click.prompt(
f"Please enter {role} signing period in days",
bold(f"Please enter {role} signing period in days"),
type=int,
default=config.signing_period,
)
Expand Down Expand Up @@ -114,11 +117,13 @@ def _get_online_input(
click.echo(f"\nConfiguring online roles")
while True:
keyuri = config.keys[0].unrecognized_fields["x-playground-online-uri"]
choice = click.prompt(
click.echo(
f" 1. Configure online key: {keyuri}\n"
f" 2. Configure timestamp: Expires in {config.timestamp_expiry} days\n"
f" 3. Configure snapshot: Expires in {config.snapshot_expiry} days\n"
"Please choose an option or press enter to continue",
f" 3. Configure snapshot: Expires in {config.snapshot_expiry} days"
)
choice = click.prompt(
bold("Please choose an option or press enter to continue"),
type=click.IntRange(0, 3),
default=0,
show_default=False,
Expand All @@ -128,7 +133,7 @@ def _get_online_input(
if choice == 1:
# TODO use value_proc argument to validate the input
key_id = click.prompt(
"Press enter to use Sigstore, or enter a Google Cloud KMS key id",
bold("Press enter to use Sigstore, or enter a Google Cloud KMS key id"),
default=""
)
if key_id == "LOCAL_TESTING_KEY":
Expand All @@ -148,13 +153,13 @@ def _get_online_input(
raise click.ClickException(f"Failed to read Google Cloud KMS key: {e}")
if choice == 2:
config.timestamp_expiry = click.prompt(
f"Please enter timestamp expiry in days",
bold(f"Please enter timestamp expiry in days"),
type=int,
default=config.timestamp_expiry,
)
if choice == 3:
config.snapshot_expiry = click.prompt(
f"Please enter snapshot expiry in days",
bold(f"Please enter snapshot expiry in days"),
type=int,
default=config.snapshot_expiry,
)
Expand All @@ -175,7 +180,7 @@ def _init_repository(repo: SignerRepository, user_config: SignerConfig) -> bool:

key = None
if repo.user_name in root_config.signers or repo.user_name in targets_config.signers:
key = get_signing_key_input("Insert your HW key and press enter")
key = get_signing_key_input()

repo.set_role_config("root", root_config, key)
repo.set_role_config("targets", targets_config, key)
Expand Down Expand Up @@ -208,7 +213,7 @@ def _update_offline_role(repo: SignerRepository, role: str) -> bool:

key = None
if repo.user_name in new_config.signers:
key = get_signing_key_input("Insert your HW key and press enter")
key = get_signing_key_input()

repo.set_role_config(role, new_config, key)
return True
Expand All @@ -232,7 +237,7 @@ def delegate(verbose: int, push: bool, event_name: str, role: str | None):
changed = _init_repository(repo, user_config)
else:
if role is None:
role = click.prompt("Enter name of role to modify")
role = click.prompt(bold("Enter name of role to modify"))

if role in ["timestamp", "snapshot"]:
changed = _update_online_roles(repo, user_config)
Expand All @@ -248,7 +253,7 @@ def delegate(verbose: int, push: bool, event_name: str, role: str | None):
git_expect(["commit", "-m", msg, "--", "metadata"])
if push:
msg = f"Press enter to push changes to {user_config.push_remote}/{event_name}"
click.prompt(msg, default=True, show_default=False)
click.prompt(bold(msg), default=True, show_default=False)
git_echo(["push", "--progress", user_config.push_remote, f"HEAD:refs/heads/{event_name}"])
else:
# TODO: deal with existing branch?
Expand Down
7 changes: 4 additions & 3 deletions playground/signer/playground_sign/sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os

from playground_sign._common import (
bold,
get_signing_key_input,
git_expect,
git_echo,
Expand Down Expand Up @@ -35,7 +36,7 @@ def sign(verbose: int, push: bool, event_name: str):
changed = False
elif repo.state == SignerState.INVITED:
click.echo(f"You have been invited to become a signer for role(s) {repo.invites}.")
key = get_signing_key_input("To accept the invitation, please insert your HW key and press enter")
key = get_signing_key_input()
for rolename in repo.invites.copy():
# Modify the delegation
role_config = repo.get_role_config(rolename)
Expand Down Expand Up @@ -64,7 +65,7 @@ def sign(verbose: int, push: bool, event_name: str):
for rolename, states in repo.target_changes.items():
for target_state in states.values():
click.echo(f" {target_state.target.path} ({target_state.state.name})")
click.prompt("Press enter to approve these changes", default=True, show_default=False)
click.prompt(bold("Press enter to approve these changes"), default=True, show_default=False)

repo.update_targets()

Expand All @@ -88,7 +89,7 @@ def sign(verbose: int, push: bool, event_name: str):
git_expect(["commit", "-m", f"Signed by {user_config.user_name}"])
if push:
msg = f"Press enter to push signature(s) to {user_config.push_remote}/{event_name}"
click.prompt(msg, default=True, show_default=False)
click.prompt(bold(msg), default=True, show_default=False)
git_echo(["push", "--progress", user_config.push_remote, f"HEAD:refs/heads/{event_name}"])
else:
# TODO: maybe deal with existing branch?
Expand Down
4 changes: 4 additions & 0 deletions playground/tests/e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ signer_init()
"1" # Configure online roles? [1: configure key]
"LOCAL_TESTING_KEY" # Enter key id
"" # Configure online roles? [enter to continue]
"2" # Choose signing key [2: yubikey]
"" # Insert HW key and press enter
"0000" # sign root
"0000" # sign root
Expand Down Expand Up @@ -147,6 +148,7 @@ signer_init_shorter_snapshot_expiry()
"3" # Configure online roles? [3: configure snapshot]
"10" # Enter expiry [10 days]
"" # Configure online roles? [enter to continue]
"2" # Choose signing key [2: yubikey]
"" # Insert HW key and press enter
"0000" # sign root
"0000" # sign root
Expand Down Expand Up @@ -184,6 +186,7 @@ signer_init_multiuser()
"1" # Configure online roles? [1: configure key]
"LOCAL_TESTING_KEY" # Enter key id
"" # Configure online roles? [enter to continue]
"2" # Choose signing key [2: yubikey]
"" # Insert HW key and press enter
"0000" # sign targets
"" # press enter to push
Expand All @@ -207,6 +210,7 @@ signer_accept_invite()
export SOFTHSM2_CONF="$SIGNER_DIR/softhsm2.conf"

INPUT=(
"2" # Choose signing key [2: yubikey]
"" # Insert HW and press enter
"0000" # sign targets
"0000" # sign targets
Expand Down