1# Copyright 2022 The Sigstore Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""
16APIs for describing identity verification "policies", which describe how the identities
17passed into an individual verification step are verified.
18"""
19
20from __future__ import annotations
21
22import logging
23from abc import ABC, abstractmethod
24from typing import Protocol
25
26from cryptography.hazmat import asn1
27from cryptography.x509 import (
28 Certificate,
29 ExtensionNotFound,
30 ObjectIdentifier,
31 OtherName,
32 RFC822Name,
33 SubjectAlternativeName,
34 UniformResourceIdentifier,
35)
36
37from sigstore.errors import VerificationError
38
39_logger = logging.getLogger(__name__)
40
41# From: https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md
42_OIDC_ISSUER_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.1")
43_OIDC_GITHUB_WORKFLOW_TRIGGER_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.2")
44_OIDC_GITHUB_WORKFLOW_SHA_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.3")
45_OIDC_GITHUB_WORKFLOW_NAME_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.4")
46_OIDC_GITHUB_WORKFLOW_REPOSITORY_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.5")
47_OIDC_GITHUB_WORKFLOW_REF_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.6")
48_OTHERNAME_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.7")
49_OIDC_ISSUER_V2_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.8")
50_OIDC_BUILD_SIGNER_URI_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.9")
51_OIDC_BUILD_SIGNER_DIGEST_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.10")
52_OIDC_RUNNER_ENVIRONMENT_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.11")
53_OIDC_SOURCE_REPOSITORY_URI_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.12")
54_OIDC_SOURCE_REPOSITORY_DIGEST_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.13")
55_OIDC_SOURCE_REPOSITORY_REF_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.14")
56_OIDC_SOURCE_REPOSITORY_IDENTIFIER_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.15")
57_OIDC_SOURCE_REPOSITORY_OWNER_URI_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.16")
58_OIDC_SOURCE_REPOSITORY_OWNER_IDENTIFIER_OID = ObjectIdentifier(
59 "1.3.6.1.4.1.57264.1.17"
60)
61_OIDC_BUILD_CONFIG_URI_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.18")
62_OIDC_BUILD_CONFIG_DIGEST_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.19")
63_OIDC_BUILD_TRIGGER_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.20")
64_OIDC_RUN_INVOCATION_URI_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.21")
65_OIDC_SOURCE_REPOSITORY_VISIBILITY_OID = ObjectIdentifier("1.3.6.1.4.1.57264.1.22")
66
67
68class _SingleX509ExtPolicy(ABC):
69 """
70 An ABC for verification policies that boil down to checking a single
71 X.509 extension's value.
72 """
73
74 oid: ObjectIdentifier
75 """
76 The OID of the extension being checked.
77 """
78
79 def __init__(self, value: str) -> None:
80 """
81 Creates the new policy, with `value` as the expected value during
82 verification.
83 """
84 self._value = value
85
86 def verify(self, cert: Certificate) -> None:
87 """
88 Verify this policy against `cert`.
89
90 Raises `VerificationError` on failure.
91 """
92 try:
93 ext = cert.extensions.get_extension_for_oid(self.oid).value
94 except ExtensionNotFound:
95 raise VerificationError(
96 f"Certificate does not contain {self.__class__.__name__} "
97 f"({self.oid.dotted_string}) extension"
98 )
99
100 # NOTE(ww): mypy is confused by the `Extension[ExtensionType]` returned
101 # by `get_extension_for_oid` above.
102 ext_value = ext.value.decode() # type: ignore[attr-defined]
103 if ext_value != self._value:
104 raise VerificationError(
105 f"Certificate's {self.__class__.__name__} does not match "
106 f"(got '{ext_value}', expected '{self._value}')"
107 )
108
109
110class _SingleX509ExtPolicyV2(_SingleX509ExtPolicy):
111 """
112 An base class for verification policies that boil down to checking a single
113 X.509 extension's value, where the value is formatted as a DER-encoded string,
114 the ASN.1 tag is UTF8String (0x0C) and the tag class is universal.
115 """
116
117 def verify(self, cert: Certificate) -> None:
118 """
119 Verify this policy against `cert`.
120
121 Raises `VerificationError` on failure.
122 """
123 try:
124 ext = cert.extensions.get_extension_for_oid(self.oid).value
125 except ExtensionNotFound:
126 raise VerificationError(
127 f"Certificate does not contain {self.__class__.__name__} "
128 f"({self.oid.dotted_string}) extension"
129 )
130
131 # NOTE(ww): mypy is confused by the `Extension[ExtensionType]` returned
132 # by `get_extension_for_oid` above.
133 ext_value = asn1.decode_der(str, ext.value) # type: ignore[attr-defined]
134 if ext_value != self._value:
135 raise VerificationError(
136 f"Certificate's {self.__class__.__name__} does not match "
137 f"(got {ext_value}, expected {self._value})"
138 )
139
140
141class OIDCIssuer(_SingleX509ExtPolicy):
142 """
143 Verifies the certificate's OIDC issuer, identified by
144 an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.1`.
145 """
146
147 oid = _OIDC_ISSUER_OID
148
149
150class GitHubWorkflowTrigger(_SingleX509ExtPolicy):
151 """
152 Verifies the certificate's GitHub Actions workflow trigger,
153 identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.2`.
154 """
155
156 oid = _OIDC_GITHUB_WORKFLOW_TRIGGER_OID
157
158
159class GitHubWorkflowSHA(_SingleX509ExtPolicy):
160 """
161 Verifies the certificate's GitHub Actions workflow commit SHA,
162 identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.3`.
163 """
164
165 oid = _OIDC_GITHUB_WORKFLOW_SHA_OID
166
167
168class GitHubWorkflowName(_SingleX509ExtPolicy):
169 """
170 Verifies the certificate's GitHub Actions workflow name,
171 identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.4`.
172 """
173
174 oid = _OIDC_GITHUB_WORKFLOW_NAME_OID
175
176
177class GitHubWorkflowRepository(_SingleX509ExtPolicy):
178 """
179 Verifies the certificate's GitHub Actions workflow repository,
180 identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.5`.
181 """
182
183 oid = _OIDC_GITHUB_WORKFLOW_REPOSITORY_OID
184
185
186class GitHubWorkflowRef(_SingleX509ExtPolicy):
187 """
188 Verifies the certificate's GitHub Actions workflow ref,
189 identified by an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.6`.
190 """
191
192 oid = _OIDC_GITHUB_WORKFLOW_REF_OID
193
194
195class OIDCIssuerV2(_SingleX509ExtPolicyV2):
196 """
197 Verifies the certificate's OIDC issuer, identified by
198 an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.8`.
199 The difference with `OIDCIssuer` is that the value for
200 this extension is formatted to the RFC 5280 specification
201 as a DER-encoded string.
202 """
203
204 oid = _OIDC_ISSUER_V2_OID
205
206
207class OIDCBuildSignerURI(_SingleX509ExtPolicyV2):
208 """
209 Verifies the certificate's OIDC Build Signer URI, identified by
210 an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.9`.
211 """
212
213 oid = _OIDC_BUILD_SIGNER_URI_OID
214
215
216class OIDCBuildSignerDigest(_SingleX509ExtPolicyV2):
217 """
218 Verifies the certificate's OIDC Build Signer Digest, identified by
219 an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.10`.
220 """
221
222 oid = _OIDC_BUILD_SIGNER_DIGEST_OID
223
224
225class OIDCRunnerEnvironment(_SingleX509ExtPolicyV2):
226 """
227 Verifies the certificate's OIDC Runner Environment, identified by
228 an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.11`.
229 """
230
231 oid = _OIDC_RUNNER_ENVIRONMENT_OID
232
233
234class OIDCSourceRepositoryURI(_SingleX509ExtPolicyV2):
235 """
236 Verifies the certificate's OIDC Source Repository URI, identified by
237 an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.12`.
238 """
239
240 oid = _OIDC_SOURCE_REPOSITORY_URI_OID
241
242
243class OIDCSourceRepositoryDigest(_SingleX509ExtPolicyV2):
244 """
245 Verifies the certificate's OIDC Source Repository Digest, identified by
246 an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.13`.
247 """
248
249 oid = _OIDC_SOURCE_REPOSITORY_DIGEST_OID
250
251
252class OIDCSourceRepositoryRef(_SingleX509ExtPolicyV2):
253 """
254 Verifies the certificate's OIDC Source Repository Ref, identified by
255 an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.14`.
256 """
257
258 oid = _OIDC_SOURCE_REPOSITORY_REF_OID
259
260
261class OIDCSourceRepositoryIdentifier(_SingleX509ExtPolicyV2):
262 """
263 Verifies the certificate's OIDC Source Repository Identifier, identified by
264 an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.15`.
265 """
266
267 oid = _OIDC_SOURCE_REPOSITORY_IDENTIFIER_OID
268
269
270class OIDCSourceRepositoryOwnerURI(_SingleX509ExtPolicyV2):
271 """
272 Verifies the certificate's OIDC Source Repository Owner URI, identified by
273 an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.16`.
274 """
275
276 oid = _OIDC_SOURCE_REPOSITORY_OWNER_URI_OID
277
278
279class OIDCSourceRepositoryOwnerIdentifier(_SingleX509ExtPolicyV2):
280 """
281 Verifies the certificate's OIDC Source Repository Owner Identifier, identified by
282 an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.17`.
283 """
284
285 oid = _OIDC_SOURCE_REPOSITORY_OWNER_IDENTIFIER_OID
286
287
288class OIDCBuildConfigURI(_SingleX509ExtPolicyV2):
289 """
290 Verifies the certificate's OIDC Build Config URI, identified by
291 an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.18`.
292 """
293
294 oid = _OIDC_BUILD_CONFIG_URI_OID
295
296
297class OIDCBuildConfigDigest(_SingleX509ExtPolicyV2):
298 """
299 Verifies the certificate's OIDC Build Config Digest, identified by
300 an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.19`.
301 """
302
303 oid = _OIDC_BUILD_CONFIG_DIGEST_OID
304
305
306class OIDCBuildTrigger(_SingleX509ExtPolicyV2):
307 """
308 Verifies the certificate's OIDC Build Trigger, identified by
309 an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.20`.
310 """
311
312 oid = _OIDC_BUILD_TRIGGER_OID
313
314
315class OIDCRunInvocationURI(_SingleX509ExtPolicyV2):
316 """
317 Verifies the certificate's OIDC Run Invocation URI, identified by
318 an X.509v3 extension tagged with `1.3.6.1.4.1.57264.1.21`.
319 """
320
321 oid = _OIDC_RUN_INVOCATION_URI_OID
322
323
324class OIDCSourceRepositoryVisibility(_SingleX509ExtPolicyV2):
325 """
326 Verifies the certificate's OIDC Source Repository Visibility
327 At Signing, identified by an X.509v3 extension tagged with
328 `1.3.6.1.4.1.57264.1.22`.
329 """
330
331 oid = _OIDC_SOURCE_REPOSITORY_VISIBILITY_OID
332
333
334class VerificationPolicy(Protocol):
335 """
336 A protocol type describing the interface that all verification policies
337 conform to.
338 """
339
340 @abstractmethod
341 def verify(self, cert: Certificate) -> None:
342 """
343 Verify the given `cert` against this policy, raising `VerificationError`
344 on failure.
345 """
346 raise NotImplementedError # pragma: no cover
347
348
349class AnyOf:
350 """
351 The "any of" policy, corresponding to a logical OR between child policies.
352
353 An empty list of child policies is considered trivially invalid.
354 """
355
356 def __init__(self, children: list[VerificationPolicy]):
357 """
358 Create a new `AnyOf`, with the given child policies.
359 """
360 self._children = children
361
362 def verify(self, cert: Certificate) -> None:
363 """
364 Verify `cert` against the policy.
365
366 Raises `VerificationError` on failure.
367 """
368
369 for child in self._children:
370 try:
371 child.verify(cert)
372 except VerificationError:
373 pass
374 else:
375 return
376
377 raise VerificationError(f"0 of {len(self._children)} policies succeeded")
378
379
380class AllOf:
381 """
382 The "all of" policy, corresponding to a logical AND between child
383 policies.
384
385 An empty list of child policies is considered trivially invalid.
386 """
387
388 def __init__(self, children: list[VerificationPolicy]):
389 """
390 Create a new `AllOf`, with the given child policies.
391 """
392
393 self._children = children
394
395 def verify(self, cert: Certificate) -> None:
396 """
397 Verify `cert` against the policy.
398 """
399
400 # Without this, we'd consider empty lists of child policies trivially valid.
401 # This is almost certainly not what the user wants and is a potential
402 # source of API misuse, so we explicitly disallow it.
403 if len(self._children) < 1:
404 raise VerificationError("no child policies to verify")
405
406 for child in self._children:
407 child.verify(cert)
408
409
410class UnsafeNoOp:
411 """
412 The "no-op" policy, corresponding to a no-op "verification".
413
414 **This policy is fundamentally insecure. You cannot use it safely.
415 It must not be used to verify any sort of certificate identity, because
416 it cannot do so. Using this policy is equivalent to reducing the
417 verification proof down to an integrity check against a completely
418 untrusted and potentially attacker-created signature. It must only
419 be used for testing purposes.**
420 """
421
422 def verify(self, cert: Certificate) -> None:
423 """
424 Verify `cert` against the policy.
425 """
426
427 _logger.warning(
428 "unsafe (no-op) verification policy used! no verification performed!"
429 )
430
431
432class Identity:
433 """
434 Verifies the certificate's "identity", corresponding to the X.509v3 SAN.
435
436 Identities can be verified modulo an OIDC issuer, to prevent an unexpected
437 issuer from offering a particular identity.
438
439 Supported SAN types include emails, URIs, and Sigstore-specific "other names".
440 """
441
442 _issuer: OIDCIssuer | None
443
444 def __init__(self, *, identity: str, issuer: str | None = None):
445 """
446 Create a new `Identity`, with the given expected identity and issuer values.
447 """
448
449 self._identity = identity
450 if issuer:
451 self._issuer = OIDCIssuer(issuer)
452 else:
453 self._issuer = None
454
455 def verify(self, cert: Certificate) -> None:
456 """
457 Verify `cert` against the policy.
458 """
459
460 if self._issuer:
461 self._issuer.verify(cert)
462
463 # Build a set of all valid identities.
464 san_ext = cert.extensions.get_extension_for_class(SubjectAlternativeName).value
465 all_sans = set(san_ext.get_values_for_type(RFC822Name))
466 all_sans.update(san_ext.get_values_for_type(UniformResourceIdentifier))
467 all_sans.update(
468 [
469 on.value.decode()
470 for on in san_ext.get_values_for_type(OtherName)
471 if on.type_id == _OTHERNAME_OID
472 ]
473 )
474
475 verified = self._identity in all_sans
476 if not verified:
477 raise VerificationError(
478 f"Certificate's SANs do not match {self._identity}; actual SANs: {all_sans}"
479 )