ci_oidc.go RawHistoryFolder
1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain
4
5import "time"
6
7// OIDC ID tokens of CI jobs (CI-4, D5): a job asks the server for an RS256 JWT
8// with GitHub's claims, which a relying party (Vault, a cloud) verifies against
9// the server's JWKS instead of holding a long-lived secret.
10
11// OIDCRequestTokenPrefix starts the secret a job presents to the token
12// endpoint (ACTIONS_ID_TOKEN_REQUEST_TOKEN). It is a job repository token
13// (CIRepoToken.OIDC) that authenticates nothing else.
14const OIDCRequestTokenPrefix = "ermiteoidc_"
15
16// IsOIDCRequestToken reports whether s has the shape of an ID token request
17// token.
18func IsOIDCRequestToken(s string) bool { return hasTokenShape(s, OIDCRequestTokenPrefix) }
19
20// PermIDToken is the permission that lets a job request ID tokens.
21const PermIDToken = "id-token"
22
23// IDTokenAllowed reports whether a job with the permissions stored for it may
24// request ID tokens: the run is trusted and the job has id-token: write. The
25// permissions are lowered by the trust rule first (TokenPermissions), so an
26// untrusted run never qualifies, whatever it asks.
27func IDTokenAllowed(stored map[string]string, trusted bool) bool {
28 return trusted && TokenPermissions(stored, trusted)[PermIDToken] == PermissionWrite
29}
30
31// OIDCKeyRetention is how long a replaced signing key stays in the JWKS: far
32// longer than the life of the tokens it signed (OIDCTokenLifetime).
33const OIDCKeyRetention = 24 * time.Hour
34
35// OIDCTokenLifetime is how long an ID token is valid after it is issued, and
36// OIDCNotBeforeSkew how far before its issue it is already valid (GitHub's
37// values: exp = iat + 5 min, nbf = iat - 10 min).
38const (
39 OIDCTokenLifetime = 5 * time.Minute
40 OIDCNotBeforeSkew = 10 * time.Minute
41)
42
43// OIDCRunnerEnvironment is the runner_environment claim: ermite's runners are
44// the owner's machines, which GitHub calls self-hosted.
45const OIDCRunnerEnvironment = "self-hosted"
46
47// MaxOIDCAudienceLen bounds the audience a job may ask for.
48const MaxOIDCAudienceLen = 1024
49
50// OIDCKey is a signing key of the ID tokens as stored: the private key sealed
51// with the secrets key, bound to its path (OIDCKeyPath). A key without
52// RetiredAt signs; a retired one is only published until RetiredAt plus
53// OIDCKeyRetention.
54type OIDCKey struct {
55 // ID is the kid: the RFC 7638 thumbprint of the public key.
56 ID string
57 // Sealed is the PKCS #8 DER of the private key, sealed.
58 Sealed []byte
59 CreatedAt time.Time
60 RetiredAt time.Time
61}
62
63// OIDCKeyPath is where the key with that id is stored, and the additional data
64// it is sealed with, so that a sealed key moved to another id does not open.
65func OIDCKeyPath(kid string) string { return "oidc/key/" + kid }
66
67// Published reports whether the key is still in the JWKS at now.
68func (k OIDCKey) Published(now time.Time) bool {
69 return k.RetiredAt.IsZero() || now.Before(k.RetiredAt.Add(OIDCKeyRetention))
70}
71
72// OIDCSubject is the sub claim of a job's ID token in GitHub's default format:
73// the environment of the job when it has one, else "pull_request" for the
74// events of merge requests, else the ref. repository is "owner/name".
75//
76// GitHub gives pull_request_target runs the pull_request context too, though
77// they run the workflow of the default branch: a relying party that trusts
78// "repo:o/r:pull_request" trusts both.
79func OIDCSubject(repository, eventName, environment, ref string) string {
80 prefix := "repo:" + repository + ":"
81 switch {
82 case environment != "":
83 return prefix + "environment:" + environment
84 case eventName == "pull_request" || eventName == "pull_request_target":
85 return prefix + "pull_request"
86 }
87 return prefix + "ref:" + ref
88}