Skip to content

identity.gpg: generate the sandbox's gpg.conf from the host's - #696

Merged
vyskocilm merged 3 commits into
mainfrom
feat/689-gpg-conf
Oct 23, 2026
Merged

vyskocilm merged 3 commits into
mainfrom
feat/689-gpg-conf

Conversation

@vyskocilm

Copy link
Copy Markdown
Contributor

Refs #689.

Sandbox gpg client ran on compiled-in defaults; host tightening of digest/cipher prefs, weak-digest, compliance, trust, key-ID hiding, compression lost (invariant 7).

Premise correction (measured, gpg 2.5.24)

gpgconf --list-options gpg lists none of the preference options, so no probe of effective values, no "write in full". gpgconf also ignores gpg.conf-<version>, which gpg itself reads. Details in issue comment.

What

  • gpgconf --list-dirs (homedir, sysconfdir) + gpg --version. Reads <sysconfdir>/gpg.conf and the first existing of <homedir>/gpg.conf-<version> … gpg.conf-<major>, gpg.conf, as gpg does (via internal/hostread). Unreadable / FIFO / failed probe → refuse.
  • Each file rebuilt from allowlist policy.GPGConfOptions into a generated read-only file: system one at the host sysconfdir path, user one at ~/.gnupg/gpg.conf. gpg inside applies its own precedence; system meta commands ([force], [ignore], [ignore-all], [user NAME]) carried.
  • Aliases and compliance shortcuts (personal-cipher-prefs, openpgp, no-pgp6, …) carried AS WRITTEN: [force] locks one option-table entry, rewriting changed what it overrides.
  • Carried leak choices: throw-keyids, disable-signer-uid, compression options.
  • assert-signer, trusted-key: hex key IDs only (gpg also reads a file name there).
  • User file always ends with snug's own disable-dirmngr: keyring inside is read-only, a WKD/keyserver lookup can only leak the address.
  • Refuses (exit 77, names file:line): allowlisted value it cannot write back exactly, unknown meta command, option name it cannot read, profile grant at ~/.gnupg/gpg.conf when host user file carries anything, any grant at ~/.gnupg/gpg.conf-*.
  • Dropped options named under -v, keyword only, never value.
  • Docs: docs/src/identity.md "Your gpg.conf", held by TestBookListsExactlyTheCarriedGPGConfOptions.

Two parallel implementations of #689 merged here; the other is kept off-branch.

Red team (three rounds, all fixed)

  • \r \v \f whitespace: CRLF require-compliance dropped silently.
  • personal-cipher-prefs AES128 dropped → AES256 inside; gnupg after compliance openpgp dropped → 3DES/SHA-1 inside.
  • sev:medium: aliases/shortcuts rewritten → system [force] openpgp + user compliance gnupg: host AES256/SHA-256, sandbox 3DES/SHA-1. no-pgp2/6/7/8 dropped.
  • sev:low: host no-auto-key-locate dropped → WKD lookup via dirmngr started inside. throw-keyids dropped (host keyid 0000000000000000, sandbox real ID). compress-algo none dropped (sandbox compressed packet: algo=2). assert-signer signers.txt read ./signers.txt.

Tests

  • Unit: TestGPGConf*, TestProbeGPGConf*, TestStageGPGConf*, golden internal/policy/testdata/gpgconf-*.txt, gpg bwrap/filesystem goldens.
  • Integration: TestIdentityGPGCarriesHostGPGConf (sign digest algo 8, AES128 via alias, host --no-options control), TestIdentityGPGRefusesAnUncarriableGPGConf, TestIdentityGPGHidesWhatTheHostHides (keyid 0000000000000000, no compressed packet, no dirmngr; dirmngr assertion fails with disable-dirmngr removed: dirmngr=1).
  • make gate green. make verify: 5 passed, 4 skipped, 0 failed.
  • make integration locally (SNUG_TEST_NET=1, SNUG_SANDBOX_TIMEOUT=25m): sandbox suite all pass except TestDoctorRunsCleanOnAHostThatCanRunSnug, TestOfflineHasOnlyLoopback — ip_gre loaded, gre0/gretap0/erspan0 in every netns, fails on any branch. 61 engine tests, floor 46. Default 8m sandbox / 4m hostless suite timeouts exceeded on this host (podman 6.1.2, engine tests 10–17s each); no test failed.

🤖 Generated with Claude Code

vyskocilm and others added 3 commits October 23, 2026 15:06
The sandbox gpg client ran on compiled-in defaults, so a host that
tightened digest/cipher preferences, weak-digest, compliance or the trust
model signed, encrypted and verified by stricter rules than gpg inside
(invariant 7).

gpgconf --list-options gpg lists none of the preference options (gpg
2.5.24), so snug reads the files: <sysconfdir>/gpg.conf, and the first of
gpg.conf-<version>..gpg.conf in the host GNUPGHOME, as gpg does. Each is
rewritten from an allowlist of crypto/trust options, aliases and
compliance shortcuts mapped to the carried spelling, system meta commands
carried, into two generated files so gpg inside applies its own
precedence. An allowlisted line snug cannot carry refuses the run.

Refs #689

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…me drops under -v

Two pieces from the parallel #689 implementation (kept on
feat/689-gpg-conf-files):

- A profile grant at ~/.gnupg/gpg.conf was displaced by the generated
  file. Two authors, one path: refuse when the host user file carries
  anything; with nothing carried the stub stays and no user file is
  generated. A grant at ~/.gnupg/gpg.conf-* is refused always: gpg reads
  the versioned name instead of the generated file.
- Options left behind are named under -v, keyword only, never value
  (GENERATED-CONFIG.md §6a). policy.GPGConfSummary.

Refs #689

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…; disable dirmngr

Red team round on the merged #689 branch (gpg 2.5.24):

- F1/F2 sev:medium. Aliases and compliance shortcuts were rewritten to
  the canonical option. gpg's [force] locks one option-table entry, and a
  shortcut is a different entry: system `[force] openpgp` + user
  `compliance gnupg` gave AES256/SHA-256 on the host, 3DES/SHA-1 in the
  sandbox. no-pgp2/6/7/8 (set compliance back to gnupg) were dropped.
  Now every alias and shortcut is carried as written.
- F3 sev:low. Host no-auto-key-locate dropped; gpg's default did a WKD
  lookup through a dirmngr it started inside. The keyring is read-only,
  so no lookup can import: the user file always ends in disable-dirmngr.
- F4/F5 sev:low. throw-keyids (keyid 0000000000000000 on host, real ID
  inside) and compression choices (no compressed packet on host,
  algo=2 inside) dropped. Carried, with disable-signer-uid.
- F6 sev:low. assert-signer and trusted-key also take a FILE name
  (`assert-signer signers.txt` read ./signers.txt). Key IDs only now.

Tests: TestGPGConfKeepsTheHostsSpelling, TestGPGConfCarriesWhatAMessageLeaks,
TestGPGConfKeyIDOptionsRefuseAFileName, TestGPGConfUserFileDisablesDirmngr,
TestIdentityGPGHidesWhatTheHostHides (dirmngr assertion fails with the
line removed: dirmngr=1).

Refs #689

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vyskocilm
vyskocilm merged commit 648950c into main Oct 23, 2026
5 checks passed
@vyskocilm
vyskocilm deleted the feat/689-gpg-conf branch October 23, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant