Skip to content
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
11 changes: 9 additions & 2 deletions docs/git-signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ never leaves the vault: git delegates the signing operation to 1Password's own
signer binary, which prompts you to approve it.

This is the alternative to the hardware-token flow in
[yubikey.md](yubikey.md) — `useYubiKey = true` takes precedence and ignores
everything on this page.
[yubikey.md](yubikey.md) — `useYubiKey = true` takes precedence for signing
new commits and tags. Signature verification trusts both sets of public keys.

## Setup

Expand Down Expand Up @@ -48,6 +48,13 @@ turns on `commit.gpgsign` / `tag.gpgsign`, and adds the key to
`~/.config/git/allowed_signers` so your own commits verify locally. A public
key is not a secret, so keeping it in the config is fine.

`allowed_signers` includes the configured `gitSigningKey` and all enrolled
YubiKey public keys regardless of `useYubiKey`, so switching signing backends
does not remove trust in previously signed commits. Keep the YubiKey `.pub`
files in `~/.ssh/` (`id_ed25519_sk_*.pub`, `id_ecdsa_sk_*.pub`, or the legacy
un-suffixed names); chezmoi reads them whenever it renders this file.
The YubiKeys do not need to be plugged in for verification.

Git needs a `key::` prefix to read a literal public key rather than a file
path; the template adds it for you, so paste the key exactly as 1Password
gives it — with or without a trailing comment.
Expand Down
7 changes: 5 additions & 2 deletions docs/yubikey.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,11 @@ private key. If you carry multiple YubiKeys, set `user.signingkey` to the
specific pubkey you want to sign with (or override per-repo via
`git config user.signingkey ~/.ssh/id_ed25519_sk_<other-serial>.pub`).

`allowed_signers` lists **all** per-serial pubkeys so verification works
regardless of which YubiKey signed the commit.
`allowed_signers` lists **all** enrolled per-serial and legacy pubkeys, plus
the configured 1Password `gitSigningKey`, regardless of `useYubiKey`.
Switching signing backends therefore preserves local verification of older
commits. Keep the YubiKey `.pub` files in `~/.ssh/` so chezmoi can continue to
include them; verification does not require a plugged-in YubiKey.

### Add a coworker's key

Expand Down
13 changes: 6 additions & 7 deletions home/dot_config/git/allowed_signers.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
{{- /* man ssh-keygen, "ALLOWED SIGNERS" section. */ -}}
# Managed by chezmoi. Add additional principals via `yk-git-sign-setup --add`.
# Lines starting with '#' are ignored.
{{ if .useYubiKey -}}
{{- /* Inline every per-serial FIDO2 pubkey we find so any plugged-in YubiKey */ -}}
{{- /* verifies. Falls back to the legacy un-suffixed names if present. */ -}}
{{/* Verification keys are independent of the active signing backend. */ -}}
{{- /* Keep every enrolled FIDO2 pubkey, including legacy un-suffixed names. */ -}}
{{- $found := false -}}
{{- range glob (joinPath .chezmoi.homeDir ".ssh" "id_ed25519_sk_*.pub") -}}
{{ $.email }} {{ include . | trim }}
Expand All @@ -28,11 +27,11 @@
{{ .email }} {{ include $legacyEc | trim }}
{{ $found = true -}}
{{- end -}}
{{- if not $found }}
{{- if and .useYubiKey (not $found) }}
# No FIDO2 SSH pubkey found yet. Run `yk-enroll` and re-run `chezmoi apply`.
{{- end }}
{{- else if .gitSigningKey }}
{{- /* 1Password-held signing key (see docs/wsl.md). The public key is enough */ -}}
{{ end }}
{{- if .gitSigningKey }}
{{- /* 1Password-held signing key (see docs/git-signing.md). The public key is enough */ -}}
{{- /* to verify signatures; the private half stays in 1Password. */ -}}
{{ .email }} {{ .gitSigningKey | trim }}
{{- end }}
5 changes: 4 additions & 1 deletion home/dot_config/shell/completions.d/00-homebrew.bash
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ fi

# Load Homebrew's bash completions from bash_completion.d directory
# These are static completion files provided by Homebrew packages
if command -v brew &>/dev/null; then
# Some (e.g. ykman) require Bash 4.4's `complete -o nosort`.
# Keep shellenv above active on older Bash; load only our fallback completions.
if ((BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4))) &&
command -v brew &>/dev/null; then
HOMEBREW_PREFIX="$(brew --prefix)"
if [ -d "${HOMEBREW_PREFIX}/etc/bash_completion.d" ]; then
for completion_file in "${HOMEBREW_PREFIX}/etc/bash_completion.d"/*; do
Expand Down
6 changes: 5 additions & 1 deletion home/dot_config/shell/completions.d/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ This directory contains shell completions and tool initializations for **Bash**
### Homebrew Integration (Priority)

- **00-homebrew.bash** / **00-homebrew.zsh** - Homebrew environment and completion setup (loaded first)
- **Bash**: Sources completions from `$(brew --prefix)/etc/bash_completion.d/`
- **Bash 4.4+**: Sources completions from `$(brew --prefix)/etc/bash_completion.d/`
- **Older Bash (including macOS Bash 3.2)**: Initializes Homebrew's environment
but skips this third-party completion directory because some files require
newer features such as `complete -o nosort`. The dotfiles' own completion
initializers still run. Use a newer Bash for the full Homebrew completion set.
- **Zsh**: Adds `$(brew --prefix)/share/zsh/site-functions` to `FPATH`
- See [Homebrew Shell Completion docs](https://docs.brew.sh/Shell-Completion) for details

Expand Down
4 changes: 2 additions & 2 deletions home/dot_config/shell/config.bash
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ if [[ "${TERM_PROGRAM}" != "vscode" ]]; then
projects_path="${HOME}/projects"

# Check if current path contains 'projects' (case-insensitive)
# Using bash parameter expansion to convert to lowercase for comparison
if [[ ! "${current_path,,}" =~ "projects" ]]; then
# Character classes also work with macOS's built-in Bash 3.2.
if [[ "${current_path}" != *[Pp][Rr][Oo][Jj][Ee][Cc][Tt][Ss]* ]]; then
# Not in projects directory, change to it if it exists
if [[ -d "${projects_path}" ]]; then
cd "${projects_path}" || true
Expand Down
1 change: 1 addition & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ Invoke-Pester -Path ./tests/powershell -Tag "Pipeline"
- `test-entra-id-parsing.bats` - Tests Entra ID user parsing
- `test-fish-config.bats` - Tests Fish shell configuration loading
- `test-git-config-windows.bats` - Tests Git configuration on Windows
- `git-allowed-signers.bats` - Verifies public keys remain trusted across signing modes
- `test-shell-startup-logic.bats` - Tests shell initialization logic
- `verify-dotfiles.bats` - Verifies applied dotfiles exist

Expand Down
90 changes: 90 additions & 0 deletions tests/bash/git-allowed-signers.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bats

setup() {
command -v chezmoi >/dev/null 2>&1 || skip "Chezmoi not installed"
REPO_ROOT="$(cd "${BATS_TEST_DIRNAME}/../.." && pwd)"
TEST_HOME="$BATS_TEST_TMPDIR/home"
TEST_CONFIG="$BATS_TEST_TMPDIR/chezmoi.yaml"
OP_KEY="ssh-ed25519 AAAA1password 1Password"
mkdir -p "$TEST_HOME/.ssh"
printf 'data: {}\n' >"$TEST_CONFIG"
# Resolve shims before changing HOME so mise does not re-bootstrap in fixtures.
CHEZMOI_BIN="$(chezmoi --config "$TEST_CONFIG" execute-template '{{ .chezmoi.executable }}')"
}

render_signers() {
local use_yubikey="$1" signing_key="${2-$OP_KEY}"
cat >"$TEST_CONFIG" <<EOF
data:
email: "test@example.com"
useYubiKey: $use_yubikey
gitSigningKey: "$signing_key"
EOF
HOME="$TEST_HOME" "$CHEZMOI_BIN" --config "$TEST_CONFIG" --destination "$TEST_HOME" execute-template \
<"$REPO_ROOT/home/dot_config/git/allowed_signers.tmpl"
}

signer_entries() {
printf '%s\n' "$output" | awk 'NF && $1 !~ /^#/'
}

@test "allowed-signers: all enrolled and legacy YubiKey keys survive switching signing modes" {
local ed1="sk-ssh-ed25519@openssh.com AAAAfirst first"
local ed2="sk-ssh-ed25519@openssh.com AAAAsecond second"
local ec="sk-ecdsa-sha2-nistp256@openssh.com AAAAecdsa ecdsa"
local legacy_ed="sk-ssh-ed25519@openssh.com AAAAlegacyEd legacy-ed"
local legacy_ec="sk-ecdsa-sha2-nistp256@openssh.com AAAAlegacyEc legacy-ec"
printf '%s\n' "$ed1" >"$TEST_HOME/.ssh/id_ed25519_sk_11.pub"
printf '%s\n' "$ed2" >"$TEST_HOME/.ssh/id_ed25519_sk_22.pub"
printf '%s\n' "$ec" >"$TEST_HOME/.ssh/id_ecdsa_sk_33.pub"
printf '%s\n' "$legacy_ed" >"$TEST_HOME/.ssh/id_ed25519_sk.pub"
printf '%s\n' "$legacy_ec" >"$TEST_HOME/.ssh/id_ecdsa_sk.pub"

local expected mode
expected="$(printf 'test@example.com %s\n' "$ed1" "$ed2" "$ec" "$legacy_ed" "$legacy_ec" "$OP_KEY")"
for mode in true false true; do
run render_signers "$mode"
[ "$status" -eq 0 ]
[ "$(signer_entries)" = "$expected" ]
[[ "$output" != *"No FIDO2 SSH pubkey"* ]]
done
}

@test "allowed-signers: YubiKey keys remain when no 1Password key is configured" {
local key="sk-ssh-ed25519@openssh.com AAAAretained retained" mode
printf '%s\n' "$key" >"$TEST_HOME/.ssh/id_ed25519_sk_11.pub"

for mode in false true; do
run render_signers "$mode" ""
[ "$status" -eq 0 ]
[ "$(signer_entries)" = "test@example.com $key" ]
done
}

@test "allowed-signers: 1Password key remains trusted in either signing mode without YubiKey files" {
local mode
for mode in false true; do
run render_signers "$mode"
[ "$status" -eq 0 ]
[ "$(signer_entries)" = "test@example.com $OP_KEY" ]
done
}

@test "allowed-signers: missing YubiKey guidance is shown only when YubiKey signing is selected" {
run render_signers true
[ "$status" -eq 0 ]
[[ "$output" == *"No FIDO2 SSH pubkey found yet"* ]]

run render_signers false
[ "$status" -eq 0 ]
[[ "$output" != *"No FIDO2 SSH pubkey"* ]]
}

@test "allowed-signers: no keys produces no malformed signer entries" {
local mode
for mode in false true; do
run render_signers "$mode" ""
[ "$status" -eq 0 ]
[ -z "$(signer_entries)" ]
done
}
69 changes: 62 additions & 7 deletions tests/bash/test-shell-startup-logic.bats
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,71 @@ setup() {
[ "$status" -eq 0 ]
}

@test "test-shell-startup-logic: bash config uses case-insensitive check" {
local config_file="$REPO_ROOT/home/dot_config/shell/config.bash"
run_bash_startup() {
run env -u BASH_ENV HOME="$BATS_TEST_TMPDIR/home" TERM_PROGRAM="${2:-}" \
/bin/bash --noprofile --norc -c '
cd -- "$1" || exit
source "$REPO_ROOT/home/dot_config/shell/config.bash"
pwd
' bash "$1"
}

if [ ! -f "$config_file" ]; then
skip "Bash config not found"
fi
@test "test-shell-startup-logic: bash preserves mixed-case projects paths without startup errors" {
local start="$BATS_TEST_TMPDIR/home/PrOjEcTs/client"
mkdir -p "$start" "$BATS_TEST_TMPDIR/home/projects"

# Should use case-insensitive comparison (either parameter expansion or regex)
run bash -c "grep -q ',,\|[Pp][Rr][Oo][Jj][Ee][Cc][Tt][Ss]' '$config_file'"
run_bash_startup "$start"
[ "$status" -eq 0 ]
[ "$output" = "$start" ]
}

@test "test-shell-startup-logic: bash enters projects from an unrelated directory" {
mkdir -p "$BATS_TEST_TMPDIR/home/work" "$BATS_TEST_TMPDIR/home/projects"

run_bash_startup "$BATS_TEST_TMPDIR/home/work"
[ "$status" -eq 0 ]
[ "$output" = "$BATS_TEST_TMPDIR/home/projects" ]
}

@test "test-shell-startup-logic: bash preserves the VS Code working directory" {
local start="$BATS_TEST_TMPDIR/home/work"
mkdir -p "$start" "$BATS_TEST_TMPDIR/home/projects"

run_bash_startup "$start" vscode
[ "$status" -eq 0 ]
[ "$output" = "$start" ]
}

@test "test-shell-startup-logic: bash preserves the working directory when projects is absent" {
local start="$BATS_TEST_TMPDIR/home/work"
mkdir -p "$start"

run_bash_startup "$start"
[ "$status" -eq 0 ]
[ "$output" = "$start" ]
}

@test "test-shell-startup-logic: Homebrew completions load only on Bash supporting nosort" {
export TEST_BREW_PREFIX="$BATS_TEST_TMPDIR/brew"
local completions="$TEST_BREW_PREFIX/etc/bash_completion.d"
mkdir -p "$completions"
cat >"$completions/example" <<'EOF'
complete -o nosort -W example example
printf 'completion loaded\n'
EOF

run env -u BASH_ENV /bin/bash --noprofile --norc -c '
brew() { printf "%s\n" "$TEST_BREW_PREFIX"; }
source "$REPO_ROOT/home/dot_config/shell/completions.d/00-homebrew.bash" || exit
if (( BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4) )); then
complete -p example >/dev/null || exit
else
if complete -p example >/dev/null 2>&1; then exit 1; fi
printf "older Bash: completion skipped\n"
fi
'
[ "$status" -eq 0 ]
[[ "$output" = "completion loaded" || "$output" = "older Bash: completion skipped" ]]
}

@test "test-shell-startup-logic: zsh config contains VS Code check" {
Expand Down
Loading