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
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@
for installs that do not carry the framework source (see `isolated.py`).
"""

FRAMEWORK_FINGERPRINT = "sha256:b45b2c81a7f1bd64"
FRAMEWORK_FINGERPRINT = "sha256:60bec0b8ff160f1e"
37 changes: 34 additions & 3 deletions tools/agent-isolation/gpg-touch-overlay.sh
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,13 @@ agent_socket_rows() {
# a false negative costs a silent block with no window. The subcommand
# may be followed by whitespace, the end of the command, or whatever
# ends a shell word — `git commit;`, `git commit)` and `git commit | tee`
# sign just as much as `git commit -m`.
readonly KEY_SUBCOMMANDS='commit|tag|merge|rebase|revert|cherry-pick|am|push|pull|fetch|clone|ls-remote|remote|submodule'
# sign just as much as `git commit -m`. The plumbing and porcelain that
# reach a remote without being `fetch` or `push` count too: `archive
# --remote`, `fetch-pack` / `send-pack`, `lfs` (git-lfs-authenticate runs
# over ssh), `subtree push`, `svn` (svn+ssh), `maintenance run` (its
# prefetch task fetches) and `request-pull` (it asks the remote for the
# commit).
readonly KEY_SUBCOMMANDS='commit|tag|merge|rebase|revert|cherry-pick|am|push|pull|fetch-pack|fetch|clone|ls-remote|remote|submodule|archive|send-pack|lfs|subtree|svn|maintenance|request-pull'

# The same reach, without git in front of it. An ssh transport asks the
# key for its authentication touch before anything moves, and gpg asks
Expand All @@ -318,6 +323,32 @@ readonly KEY_SUBCOMMANDS='commit|tag|merge|rebase|revert|cherry-pick|am|push|pul
# lead the alternation so the match cannot stop short at `ssh`.
readonly KEY_COMMANDS='ssh-keygen|ssh-add|ssh|scp|sftp|sshfs|rsync|svn|gpg2|gpg'

# gh runs git for these, over ssh when its git_protocol is ssh: `create`
# pushes a branch or repository that is not on GitHub yet. Options
# may stand before the command group or between it and the command
# (`gh -R owner/repo pr checkout 1`, `gh pr -R owner/repo checkout 1`), and
# `co` is gh's own alias for `pr checkout`, both on its own (`gh co 1`)
# and under `pr`.
readonly GH_OPTIONS='([[:space:]]+-[^[:space:];&|]*([[:space:]]+[^-[:space:];&|][^[:space:];&|]*)?)*'
readonly GH_GIT_COMMANDS="gh[\"']?${GH_OPTIONS}[[:space:]]+(repo${GH_OPTIONS}[[:space:]]+(clone|create|fork|sync)|pr${GH_OPTIONS}[[:space:]]+(checkout|co|create)|co)"

# What may stand right before a command word, and right after one. Besides
# the shell separators: a `/`, for a command named by its path
# (`/usr/bin/git push`, `/opt/homebrew/bin/ssh host`), and quotes or a
# backtick, for one handed to a shell as a string (`bash -c "git push"`,
# `sh -c 'ssh host'`). A dot is not among them, which keeps `~/.ssh`
# quiet.
readonly WORD_START="(^|[;&|(\`\"'/]|[[:space:]])"
readonly WORD_END="([[:space:];&|)\`\"']|$)"

# One shell word of a git global option or its value: unquoted characters,
# or a quoted span, which may hold spaces. Without the quoted spans
# `git -c user.name="Jane Doe" commit` and `git --git-dir=/x commit` sign
# with no window, because the option no longer looks like one word. A
# backslash takes the character after it, so `O\'Brien` opens no quote,
# and so it does inside bash's `$'...'`, where `$'O\'Brien'` is one word.
readonly GIT_OPTION_WORD="(\\\\.|\\\$'([^'\\\\]|\\\\.)*'|[^[:space:]\"'\\\\]|\"([^\"\\\\]|\\\\.)*\"|'[^']*')"

# A screen to draw on, and something to draw the window with.
#
# On macOS both questions collapse into one: a logged-in user always has
Expand Down Expand Up @@ -557,7 +588,7 @@ arm() {
cwd="$(printf '%s' "$payload" | jq -r '.cwd // empty' 2>/dev/null)"

printf '%s' "$command_text" |
grep -Eq "(^|[;&|(]|[[:space:]])(git([[:space:]]+-[A-Za-z-]+([[:space:]]+[^[:space:]]+)?)*[[:space:]]+($KEY_SUBCOMMANDS)|($KEY_COMMANDS))([[:space:];&|)]|$)" ||
grep -Eq "${WORD_START}(git[\"']?([[:space:]]+-${GIT_OPTION_WORD}*([[:space:]]+${GIT_OPTION_WORD}+)?)*[[:space:]]+($KEY_SUBCOMMANDS)|$GH_GIT_COMMANDS|($KEY_COMMANDS))${WORD_END}" ||
return 0

# A card that never asks for a touch needs no watcher.
Expand Down
61 changes: 61 additions & 0 deletions tools/agent-isolation/tests/test_gpg_touch_overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ def _arm(command: str) -> str:
"git commit -m 'x'",
"git commit --amend --no-edit",
"git -C /some/repo commit -m 'x'",
# Global options whose value is quoted and holds spaces, or is
# joined with `=`, still put the subcommand in front of the key.
'git -c user.name="Jane Doe" -c user.email="[email protected]" commit -q -am x',
"git -c user.name='Jane Doe' commit -m x",
"git -c core.hooksPath=/dev/null rebase --continue",
"git --git-dir=/some/repo/.git commit -m x",
'GIT_EDITOR=true git -c user.name="Jane Doe" commit --amend --no-edit',
# A backslash-escaped quote opens no quoted span.
"git -c user.name=O\\'Brien commit -m x",
'git -c user.name="Jane \\"JD\\" Doe" commit -m x',
# bash's ANSI-C quoting, where a backslash escapes the quote too.
"git -c user.name=$'O\\'Brien' commit -m x",
"cd /tmp/repo && git commit -m 'x'",
"git tag -s v1.0.0 -m 'release'",
"git rebase --continue",
Expand Down Expand Up @@ -106,6 +118,8 @@ def test_arms_for_commands_that_can_reach_the_key(command: str) -> None:
"git status --short",
"git log --oneline -5",
"git diff HEAD",
'git -c user.name="Jane Doe" log --oneline -1',
"git -c color.ui=always status",
"ls -la",
"echo 'committing to the plan'",
"grep -r commit .",
Expand All @@ -117,6 +131,17 @@ def test_arms_for_commands_that_can_reach_the_key(command: str) -> None:
"sshuttle --dns -r host 0/0",
# Starting an agent is not a request to the key.
"ssh-agent -s",
# A path into an ssh directory, not the command.
"cat /etc/ssh/sshd_config",
# gh talking to the API only, never through git.
"gh pr view 1234",
"gh repo view apache/magpie",
"gh -R apache/magpie pr view 1234",
"gh pr -R apache/magpie list",
"gh pr comment 1234 --body done",
"gh codespace list",
# A script whose name merely starts with git-, not git itself.
"./scripts/git-push-helper.sh origin main",
],
)
def test_stays_quiet_for_commands_that_cannot_sign(command: str) -> None:
Expand Down Expand Up @@ -147,6 +172,42 @@ def test_stays_quiet_for_commands_that_cannot_sign(command: str) -> None:
"ssh host; echo done",
"(ssh host)",
"git fetch && ssh host",
# Named by path rather than found on PATH.
"/usr/bin/git push origin main",
"/opt/homebrew/bin/ssh -T [email protected]",
# A quoted path or name: the quote closes before the arguments.
'"/usr/bin/git" push origin main',
'"git" commit -m x',
"'ssh' host",
'"gh" pr checkout 1234',
'"/opt/homebrew/bin/gh" repo clone apache/magpie',
# Handed to a shell as a string.
'bash -c "git push origin main"',
"sh -c 'ssh host uptime'",
"echo `git ls-remote origin`",
# Remote-talking git that is neither fetch nor push.
"git archive [email protected]:apache/magpie.git HEAD",
"git fetch-pack [email protected]:apache/magpie.git",
"git send-pack [email protected]:apache/magpie.git main",
"git lfs pull",
"git subtree push --prefix=docs origin gh-pages",
"git svn dcommit",
"git maintenance run --task=prefetch",
"git request-pull v1.0 origin",
# gh runs git for these, over ssh under git_protocol=ssh.
"gh repo clone apache/magpie",
"gh repo fork apache/magpie --clone",
"gh repo sync",
"gh pr checkout 1234",
# Options before or inside the command group, and gh's alias.
"gh -R apache/magpie pr checkout 1234",
"gh pr -R apache/magpie checkout 1234",
"gh pr co 1234",
"gh co 1234",
# Pushes the branch first when it is not on GitHub yet.
"gh pr create --fill",
"gh repo create apache/x --source=. --push",
"gh repo clone apache/magpie -- --depth=1",
],
)
def test_arms_for_non_git_commands_that_can_reach_the_key(command: str) -> None:
Expand Down
Loading