CI and scripts and readme and such
This commit is contained in:
parent
476cb92216
commit
1bfc9528f5
6 changed files with 239 additions and 2 deletions
29
.forgejo/workflows/verify.yml
Normal file
29
.forgejo/workflows/verify.yml
Normal 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
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.env
|
2
LICENSE
2
LICENSE
|
@ -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:
|
||||
|
||||
|
|
65
README.md
65
README.md
|
@ -1,3 +1,66 @@
|
|||
# ssh-keys
|
||||
|
||||
SSH Key verification (Experimental Tests)
|
||||
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 Forgejo’s interface:
|
||||
|
||||

|
||||
|
||||
### 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 you’re 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 doesn’t 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 user’s 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
BIN
verified-commit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
144
verify.py
Executable file
144
verify.py
Executable 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"Couldn’t 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 wasn’t 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()
|
Loading…
Add table
Reference in a new issue