Skip to content

Commit bf78886

Browse files
committed
fix(plugins): skip TUF update on --allow-unsigned
1 parent 887cfae commit bf78886

3 files changed

Lines changed: 75 additions & 3 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Fixed
2+
3+
- `ggshield plugin install --allow-unsigned` and `ggshield plugin update --allow-unsigned` now verify plugin signatures using the embedded / cached sigstore trust root instead of refreshing it over the network, so plugins can still be installed when the sigstore TUF endpoints are unreachable.

ggshield/core/plugin/signature.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
88
Verification modes:
99
- STRICT: block unsigned or invalid plugins
10-
- WARN: log a warning but allow loading
10+
- WARN: log a warning but allow loading. Verification still runs against the
11+
embedded / cached sigstore trust root (offline=True) so the TUF metadata
12+
refresh, which can fail in restricted networks, is skipped.
1113
- DISABLED: skip verification entirely
1214
"""
1315

@@ -131,10 +133,13 @@ def verify_wheel_signature(
131133
logger.warning("%s", msg)
132134
return SignatureInfo(status=SignatureStatus.MISSING, message=msg)
133135

134-
# Verify bundle
136+
# Verify bundle. In WARN mode (--allow-unsigned) we use the embedded /
137+
# cached sigstore trust root via offline=True so that the TUF metadata
138+
# refresh -- which can fail in restricted networks -- is skipped.
135139
bundle = Bundle.from_json(bundle_path.read_bytes())
136140
wheel_bytes = wheel_path.read_bytes()
137-
verifier = Verifier.production()
141+
offline = mode == SignatureVerificationMode.WARN
142+
verifier = Verifier.production(offline=offline)
138143

139144
for trusted in trusted_identities:
140145
policy = AllOf(

tests/unit/core/plugin/test_signature.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,70 @@ def test_warn_mode_returns_missing(self, tmp_path: Path) -> None:
9292
assert result.status == SignatureStatus.MISSING
9393

9494

95+
class TestSignatureVerificationModeWarn:
96+
"""WARN mode verifies offline so the sigstore TUF refresh is skipped."""
97+
98+
def test_warn_mode_uses_offline_verifier(self, tmp_path: Path) -> None:
99+
wheel = tmp_path / "plugin-1.0.0.whl"
100+
wheel.write_bytes(b"fake wheel content")
101+
bundle = tmp_path / "plugin-1.0.0.whl.sigstore"
102+
bundle.write_bytes(b'{"fake": "bundle"}')
103+
104+
mock_verifier_cls = MagicMock()
105+
mock_bundle_cls = MagicMock()
106+
mock_all_of_cls = MagicMock()
107+
mock_oidc_issuer_cls = MagicMock()
108+
mock_gh_repo_cls = MagicMock()
109+
110+
mock_verifier_cls.production.return_value.verify_artifact.return_value = None
111+
mock_bundle_cls.from_json.return_value = MagicMock()
112+
113+
with (
114+
patch("ggshield.core.plugin.signature.Verifier", mock_verifier_cls),
115+
patch("ggshield.core.plugin.signature.Bundle", mock_bundle_cls),
116+
patch("ggshield.core.plugin.signature.AllOf", mock_all_of_cls),
117+
patch("ggshield.core.plugin.signature.OIDCIssuer", mock_oidc_issuer_cls),
118+
patch(
119+
"ggshield.core.plugin.signature.GitHubWorkflowRepository",
120+
mock_gh_repo_cls,
121+
),
122+
):
123+
verify_wheel_signature(wheel, SignatureVerificationMode.WARN)
124+
125+
# The TUF refresh runs inside Verifier.production() unless offline=True
126+
# is passed; --allow-unsigned must opt into offline verification.
127+
mock_verifier_cls.production.assert_called_once_with(offline=True)
128+
129+
def test_strict_mode_uses_online_verifier(self, tmp_path: Path) -> None:
130+
wheel = tmp_path / "plugin-1.0.0.whl"
131+
wheel.write_bytes(b"fake wheel content")
132+
bundle = tmp_path / "plugin-1.0.0.whl.sigstore"
133+
bundle.write_bytes(b'{"fake": "bundle"}')
134+
135+
mock_verifier_cls = MagicMock()
136+
mock_bundle_cls = MagicMock()
137+
mock_all_of_cls = MagicMock()
138+
mock_oidc_issuer_cls = MagicMock()
139+
mock_gh_repo_cls = MagicMock()
140+
141+
mock_verifier_cls.production.return_value.verify_artifact.return_value = None
142+
mock_bundle_cls.from_json.return_value = MagicMock()
143+
144+
with (
145+
patch("ggshield.core.plugin.signature.Verifier", mock_verifier_cls),
146+
patch("ggshield.core.plugin.signature.Bundle", mock_bundle_cls),
147+
patch("ggshield.core.plugin.signature.AllOf", mock_all_of_cls),
148+
patch("ggshield.core.plugin.signature.OIDCIssuer", mock_oidc_issuer_cls),
149+
patch(
150+
"ggshield.core.plugin.signature.GitHubWorkflowRepository",
151+
mock_gh_repo_cls,
152+
),
153+
):
154+
verify_wheel_signature(wheel, SignatureVerificationMode.STRICT)
155+
156+
mock_verifier_cls.production.assert_called_once_with(offline=False)
157+
158+
95159
class TestBundleVerification:
96160
"""Tests for bundle verification with mocked sigstore."""
97161

0 commit comments

Comments
 (0)