Repository navigation
fix(guard/guardcore): fail closed when the policy signature cannot be trusted - #171
Merged
codeyevsky merged 2 commits intoOct 27, 2026
Merged
Conversation
verifyHomePolicy() only rejected a home policy.json when VerifyDocument reported signed && !valid. Every other outcome of the trust check fell through to "no verification required" and loaded the file: - a signature that could not be decoded (verr != nil) was ignored; - a machine key that existed but could not be loaded (kerr != nil) skipped the check entirely; - a stripped signature (signed == false) was treated as a legacy file even on a machine that had already signed policies; - a corrupt file was skipped, so the loader fell back to the project-local policy, which carries no Security fields at all. Each of those let someone who can write policy.json - or merely strip its signature - have an untrusted policy enforced, with the guard's own checks disabled through the missing Security block. The loader now applies one accepting rule on a keyed machine: a signature that verifies against this machine's key. Anything else returns ErrPolicyTampered, which guard/main.go already turns into a deny plus an audit record. Loading no longer creates the signing key. verifyHomePolicy used to call LoadOrCreateKeyPair on every load, so a single run keyed the install; an upgrade that still had a pre-signature, unsigned policy.json then rejected that same policy on its second run. With no key pair present an unsigned file is treated as pre-signature and loads, while a signed one is refused because there is nothing here to check it against. Writes still create the key pair through localstore, which is where signing belongs. Signed-off-by: Turgay Nalbant <182372382+tnalbant@users.noreply.github.com>
tnalbant
force-pushed
the
fix/policy-signature-fail-closed-104
branch
from
October 27, 2026 18:14
8361e41 to
656ecfb
Compare
codeyevsky
approved these changes
Oct 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
LoadLocalPolicyFileonly rejected the homepolicy.jsonwhenVerifyDocumentreturnedsigned && !valid. Every other outcome of the trust check fell through to "no verificationrequired" and the file was loaded and applied:
verr != nil)ErrPolicyTamperedkerr != nil)ErrPolicyTamperedErrPolicyTamperedErrPolicyTamperedsecurityblock)ErrPolicyTamperedguard/main.goalready turns a loader error into a deny plus an audit record, so these paths nowfail closed instead of quietly enforcing an untrusted policy with the guard's own checks disabled.
Reading a policy no longer creates the signing key
This one is subtle, and
sg-parity-check.shcaught it.verifyHomePolicycalledcrypto.LoadOrCreateKeyPairon every load, so merely running the guard keyed the install. A machinethat still had a pre-signature, unsigned
policy.jsonwould then be "keyed" on its second run andreject its own policy - an upgrade that bricks the install, and an unsigned fixture that stops
loading after the first
expectin the parity script.The loader now only loads. With no key pair present, an unsigned file is a pre-signature policy and
still loads, while a signed one is refused because there is nothing here to check it against. With a
key pair present, the strict rule above applies. Writes still create the key pair through
localstore, which is where signing belongs.Verification
TestLoadLocalPolicyFileDoesNotCreateASigningKey- loads an unsigned policy twice and asserts nokey pair appeared and both loads succeeded. This is the parity-script regression.
TestLoadLocalPolicyFileOwnMalformedSignatureIsRejected- failed before (err=<nil>: accepted),passes now.
TestLoadLocalPolicyFileOwnUnsignedAfterTheMachineHasASigningKeyIsRejected- failed before(
err=<nil>), passes now.TestLoadLocalPolicyFileCorruptOwnFileIsRejectedNotSkipped- rewritten from...SkipsACorruptOwnFileAndFallsBackToProjectLocal. Intentional behaviour change: a corrupt trustanchor must not silently downgrade the guard to a policy with no
securityblock.packages/guard: 8/8 packages pass;packages/cryptopasses;go vetclean;go test -raceclean on
guardcoreandcrypto.scripts/sg-parity-check.sh: passed=10 failed=0 (on the first revision of this branch:8 passed, 2 failed - the two "should be allowed" cases were denied).
scripts/sg-layers-check.sh: passed=25 wrong=0.scripts/sg-box-check.sh: not runnable here (bwrap is not installed); untouched by this branch.Refs #104
Residual risk
Deleting
~/.solongate/cryptoas well as the signature still reads as "no signing identity yet", andan unsigned file loads. Writing there is already enough to disable the guard, so this closes the
file-only attack rather than the "own the whole state directory" one.
AI assistance
Implemented with AI assistance (Hermes agent) at my direction, including the test and script runs
above against both revisions. I reviewed the change and the evidence before submitting it.