1
0
Fork 0
forked from infra/keys

CI and scripts and readme and such

This commit is contained in:
kleines Filmröllchen 2025-01-28 11:02:35 +01:00
parent 476cb92216
commit 1bfc9528f5
Signed by: filmroellchen
SSH key fingerprint: SHA256:NarU6J/XgCfEae4rbei0YIdN2pYaYDccarK6R53dnc8
6 changed files with 239 additions and 2 deletions

View file

@ -0,0 +1,29 @@
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]
jobs:
build-latex:
name: Verify SSH keys
runs-on: alpine
container:
image: alpine:latest
defaults:
run:
shell: ash -eo pipefail {0}
steps:
- name: Install dependencies
run: |
apk add --no-cache nodejs git openssh python3 py3-pip
- name: Create virtualenv and install Python dependencies
run:
python -m venv venv
. venv/bin/activate
pip install --upgrade GitPython requests
- name: Checkout repository
uses: actions/checkout@v4
- name: Run verification script
run: . venv/bin/activate && python verify.py

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.env

View file

@ -1,4 +1,4 @@
Copyright (c) 2025 filmroellchen
Copyright (c) 2025 Chaostreff Backnang
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

View file

@ -1,3 +1,66 @@
# ssh-keys
SSH Key verification (Experimental Tests)
## Verified cryptographic keys for use with CTBK infrastructure
This repo holds all SSH keys and wireguard keys for sysadmins of the Chaostreff Backnang infrastructure. It is a simple database of all valid cryptographic keys, with verification (i.e. signatures) that they have been supplied by their owner.
The workflow roughly goes like this, with detailed explanations further below:
- Users add their SSH key(s) to their account on git.ctbk.de and verify them. This can establish an additional, optional link between an email address and the keys. Therefore, access to a git.ctbk.de and IdP account verifies the accuracy of the SSH key(s).
- Users commit their keys to this repository, in a user-specific directory, making sure to sign their commits with the same SSH key(s) they added to their account. Therefore, the account SSH key(s) verify their own public keyfiles and, by extension, all other files in the same directory.
The chain of trust and verification is therefore: IdP account -> Forgejo account with specific username & email address -> SSH key added and verified in Forgejo -> Git commit in this repo, made with Forgejo account name and email address, signed with account SSH key -> Files in the commit, containing the username, email address, SSH key(s), and more.
## Using this repository
Make sure to follow this guide carefully, otherwise your commits may not be verified correctly!
### Forgejo setup
Add your SSH key(s) to your Forgejo account:
- Click on your user profile picture.
- Click “Settings”.
- Click “SSH / GPG keys”
- Click “Add key”
- Add your key and give it a comment so you remember its purpose. Currently, all Forgejo SSH keys are general-purpose, but in case this ever changes, make sure the key can be used for commit singing.
- The new key should appear as unverified. Click “Verify” next to the key you just created. Forgejo will provide you with a random string to sign with your SSH key and a command line which can be used to do this. Paste the SSH signature and your key should now be verified.
Repeat this for at least every key you plan to use for signing, *not necessarily* every key you want to commit to the repository.
Make sure every email address you want to use with Git (see below) has been added to your profile. This is possible under the “Account” tab in the settings.
### Git setup
Any of the git config changes may be executed with `--global`, at your option.
Change your Git email address and user name to any email address known to Forgejo, and the Forgejo user name (not display name), respectively:
```shell
git config user.name forgejo-user-name
git config user.email user@example.net
```
Add your SSH key to Git as a signing key, and enable signing:
```shell
# must be an absolute path
git config user.signingkey path/to/your/key
git config commit.gpgsign true
git config gpg.format ssh
```
If you configured everything correctly, your commits should appear with a green padlock in Forgejos interface:
![A verified commit by kleines Filmröllchen as seen in Forgejos interface. Next to the commit hash a green padlock is shown, as well as their profile picture.](verified-commit.png)
### Changing (and adding) your verified data
Make all changes as necessary. (File structure TBD.) Make sure to only modify a subdirectory named *exactly* like your Forgejo user name. When youre done, create a commit, ensure the commit has been signed properly, and open a pull request.
The CI will run on your PR and verify the changes have been signed by the correct user. **As an administrator of this repo, never merge a change that doesnt pass CI.**
Pull requests are merged by Git merge, which preserves the signing status. The CI should check out on main as well; otherwise, some users key data in Forgejo may have changed. These users must be notified ASAP so they remove their outdated keys and add new keys if they want to keep their verified data. Worst case, the files can be removed, removing them from the verification.

BIN
verified-commit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

144
verify.py Executable file
View file

@ -0,0 +1,144 @@
#!/usr/bin/env python
# Verify all commits in a Git repository.
# This is a script for GitHub/Forgejo Actions and does not work properly without the necessary environment variables set.
# Required dependencies: GitPython
import logging
import git
from os import environ as env, chdir
from pathlib import Path
from typing import Generator
from subprocess import call
from tempfile import NamedTemporaryFile
import requests
log = logging.getLogger(__name__)
is_ci = "CI" in env
repo_name = env["GITHUB_REPOSITORY"]
server = env["GITHUB_SERVER_URL"]
action_ref = env["GITHUB_REF"]
# token = env["GITHUB_TOKEN"]
def collect_user_dirs() -> Generator[Path]:
return (
dir
for dir in Path(".").glob("*")
if dir.is_dir() and not dir.name.startswith(".")
)
def last_commit_for(dir: Path, ref: git.Reference):
"""Returns the Git commit signature for the last commit on this path."""
last_commit_hash = str(ref.repo.git.rev_list("--max-count=1", action_ref, dir))
return ref.repo.commit(last_commit_hash)
def keylist_to_principals(keyfile_text: str, email: str) -> str:
return "\n".join(
f"{email} {public_key}" for public_key in keyfile_text.splitlines()
)
def get_forgejo_keys(username: str) -> str:
response = requests.get(f"{server}/{username}.keys")
if not response.ok:
log.error(f"Server error: {response.status_code} {response.reason}")
raise Exception(f"Couldnt retrieve keylist for user {username}.")
return response.text
def verify_dir(dir: Path, ref: git.Reference):
username = dir.name
keyfile = dir / "keys"
if not keyfile.exists():
raise Exception("Missing keyfile")
commit = last_commit_for(dir, ref)
log.debug(f"Found last commit: {commit.name_rev}")
if commit.author.name != username:
raise Exception(
f"Commit author {commit.author.name} is not the owner of this directory."
)
signature = commit.gpgsig
if signature is None or len(signature) == 0:
raise Exception(
f"Last commit {commit.name_rev} was not signed (committer {commit.author.name})."
)
email = commit.author.email
if email is None:
raise Exception(
f"Commit {commit.name_rev} does not have an email address attached (committer {commit.author.name})."
)
log.info(
f"Last commit {commit.name_rev} was signed by the correct user {commit.author}"
)
local_keys = keyfile.read_text()
remote_keys = get_forgejo_keys(username)
log.debug(f"Got remote keys:\n{remote_keys}")
with NamedTemporaryFile(mode="+w") as temp_keyfile:
# First, configure git to use our custom-made principals file
with ref.repo.config_writer("repository") as config:
config.set_value("gpg.ssh", "allowedSignersFile", temp_keyfile.name)
temp_keyfile_contents = keylist_to_principals(remote_keys, email)
log.debug(f"temp keyfile:\n{temp_keyfile_contents}")
temp_keyfile.write(temp_keyfile_contents)
temp_keyfile.flush()
# Check whether one of the user keys signed this commit.
# throws an exception automatically if verification fails, nothing else to do
ref.repo.git.verify_commit("--raw", commit.hexsha)
# BTW: For ssh-keygen -Y verify, the namespace is really just “git”. No, this is not documented anywhere.
# Oh I absolutely went digging for this in the git source code, though it wasnt hard to find.
# See https://git.kernel.org/pub/scm/git/git.git/tree/gpg-interface.c?id=5f8f7081f7761acdf83d0a4c6819fe3d724f01d7#n559
# Verify that the key is also in the local keylist.
temp_keyfile_contents = keylist_to_principals(local_keys, email)
temp_keyfile.write(temp_keyfile_contents)
temp_keyfile.flush()
ref.repo.git.verify_commit("--raw", commit.hexsha)
def current_ref(repo: git.Repo) -> git.Reference:
for ref in repo.references:
if ref.name == action_ref or ref.abspath == action_ref:
return ref
raise Exception(f"No ref named {action_ref} found")
def main():
logging.basicConfig(
style="{",
format="[{levelname:8}] {message}",
level=logging.INFO if is_ci else logging.DEBUG,
)
chdir(env.get("GITHUB_WORKSPACE", default="."))
if is_ci:
log.info("Verification script running in CI.")
log.debug(f"{server} / {repo_name}")
log.debug(f"Repo dir: {Path.cwd().resolve()}")
repo = git.Repo(Path.cwd())
ref = current_ref(repo)
dirs = collect_user_dirs()
errors = 0
successes = 0
for dir in dirs:
try:
log.info(f"------ Verifying {dir.name} ...")
verify_dir(dir, ref)
successes += 1
except Exception as e:
log.error(f"Verification failure for user {dir.name}: {e}")
errors += 1
if errors > 0:
log.error("At least one user failed verification. See log for details.")
exit(errors)
else:
log.info(f"All users passed verification. Checked {successes} directories.")
if __name__ == "__main__":
main()