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