go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/delegation/checker.go (about) 1 // Copyright 2016 The LUCI 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 package delegation 16 17 import ( 18 "context" 19 "encoding/base64" 20 "fmt" 21 "strings" 22 23 "google.golang.org/protobuf/proto" 24 25 "go.chromium.org/luci/auth/identity" 26 "go.chromium.org/luci/common/clock" 27 "go.chromium.org/luci/common/errors" 28 "go.chromium.org/luci/common/logging" 29 "go.chromium.org/luci/common/retry/transient" 30 31 "go.chromium.org/luci/server/auth/delegation/messages" 32 "go.chromium.org/luci/server/auth/internal/tracing" 33 "go.chromium.org/luci/server/auth/signing" 34 ) 35 36 const ( 37 // maxTokenSize is upper bound for expected size of a token (after base64 38 // decoding). Larger tokens will be ignored right away. 39 maxTokenSize = 8 * 1024 40 41 // allowedClockDriftSec is how much clock difference we accept, in seconds. 42 allowedClockDriftSec = int64(30) 43 ) 44 45 var ( 46 // ErrMalformedDelegationToken is returned when delegation token cannot be 47 // deserialized. 48 ErrMalformedDelegationToken = errors.New("auth: malformed delegation token") 49 50 // ErrUnsignedDelegationToken is returned if token's signature cannot be 51 // verified. 52 ErrUnsignedDelegationToken = errors.New("auth: unsigned delegation token") 53 54 // ErrForbiddenDelegationToken is returned if token is structurally correct, 55 // but some of its constraints prevents it from being used. For example, it is 56 // already expired or it was minted for some other services, etc. See logs for 57 // details. 58 ErrForbiddenDelegationToken = errors.New("auth: forbidden delegation token") 59 ) 60 61 // CertificatesProvider is used by 'CheckToken', it is implemented by authdb.DB. 62 // 63 // It returns certificates of services trusted to sign tokens. 64 type CertificatesProvider interface { 65 // GetCertificates returns a bundle with certificates of a trusted signer. 66 // 67 // Returns (nil, nil) if the given signer is not trusted. 68 // 69 // Returns errors (usually transient) if the bundle can't be fetched. 70 GetCertificates(ctx context.Context, id identity.Identity) (*signing.PublicCertificates, error) 71 } 72 73 // GroupsChecker is accepted by 'CheckToken', it is implemented by authdb.DB. 74 type GroupsChecker interface { 75 // IsMember returns true if the given identity belongs to any of the groups. 76 // 77 // Unknown groups are considered empty. May return errors if underlying 78 // datastore has issues. 79 IsMember(ctx context.Context, id identity.Identity, groups []string) (bool, error) 80 } 81 82 // CheckTokenParams is passed to CheckToken. 83 type CheckTokenParams struct { 84 Token string // the delegation token to check 85 PeerID identity.Identity // identity of the caller, as extracted from its credentials 86 CertificatesProvider CertificatesProvider // returns certificates with trusted keys 87 GroupsChecker GroupsChecker // knows how to do group lookups 88 OwnServiceIdentity identity.Identity // identity of the current service 89 } 90 91 // CheckToken verifies validity of a delegation token. 92 // 93 // If the token is valid, it returns the delegated identity (embedded in the 94 // token). 95 // 96 // May return transient errors. 97 func CheckToken(ctx context.Context, params CheckTokenParams) (_ identity.Identity, err error) { 98 ctx, span := tracing.Start(ctx, "go.chromium.org/luci/server/auth/delegation.CheckToken") 99 defer func() { tracing.End(span, err) }() 100 101 // base64-encoded token -> DelegationToken proto (with signed serialized 102 // subtoken). 103 tok, err := deserializeToken(params.Token) 104 if err != nil { 105 logging.Warningf(ctx, "auth: Failed to deserialize delegation token - %s", err) 106 return "", ErrMalformedDelegationToken 107 } 108 109 // Signed serialized subtoken -> Subtoken proto. 110 subtoken, err := unsealToken(ctx, tok, params.CertificatesProvider) 111 if err != nil { 112 if transient.Tag.In(err) { 113 logging.Warningf(ctx, "auth: Transient error when checking delegation token signature - %s", err) 114 return "", err 115 } 116 logging.Warningf(ctx, "auth: Failed to check delegation token signature - %s", err) 117 return "", ErrUnsignedDelegationToken 118 } 119 120 // Validate all constrains encoded in the token and derive the delegated 121 // identity. 122 return checkSubtoken(ctx, subtoken, ¶ms) 123 } 124 125 // deserializeToken deserializes DelegationToken proto message. 126 func deserializeToken(token string) (*messages.DelegationToken, error) { 127 blob, err := base64.RawURLEncoding.DecodeString(token) 128 if err != nil { 129 return nil, err 130 } 131 if len(blob) > maxTokenSize { 132 return nil, fmt.Errorf("the delegation token is too big (%d bytes)", len(blob)) 133 } 134 tok := &messages.DelegationToken{} 135 if err = proto.Unmarshal(blob, tok); err != nil { 136 return nil, err 137 } 138 return tok, nil 139 } 140 141 // unsealToken verifies token's signature and deserializes the subtoken. 142 // 143 // May return transient errors. 144 func unsealToken(ctx context.Context, tok *messages.DelegationToken, certsProvider CertificatesProvider) (*messages.Subtoken, error) { 145 // Grab the public keys of the service that signed the token, if we trust it. 146 signerID, err := identity.MakeIdentity(tok.SignerId) 147 if err != nil { 148 return nil, fmt.Errorf("bad signer_id %q - %s", tok.SignerId, err) 149 } 150 certs, err := certsProvider.GetCertificates(ctx, signerID) 151 switch { 152 case err != nil: 153 return nil, fmt.Errorf("failed to grab certificates of %q - %s", tok.SignerId, err) 154 case certs == nil: 155 return nil, fmt.Errorf("the signer %q is not trusted", tok.SignerId) 156 } 157 158 // Check the signature on the token. 159 err = certs.CheckSignature(tok.SigningKeyId, tok.SerializedSubtoken, tok.Pkcs1Sha256Sig) 160 if err != nil { 161 return nil, err 162 } 163 164 // The signature is correct! Deserialize the subtoken. 165 msg := &messages.Subtoken{} 166 if err = proto.Unmarshal(tok.SerializedSubtoken, msg); err != nil { 167 return nil, err 168 } 169 170 return msg, nil 171 } 172 173 // checkSubtoken validates the delegation subtoken. 174 // 175 // It extracts and returns original delegated_identity. 176 func checkSubtoken(ctx context.Context, subtoken *messages.Subtoken, params *CheckTokenParams) (identity.Identity, error) { 177 if subtoken.Kind != messages.Subtoken_BEARER_DELEGATION_TOKEN { 178 logging.Warningf(ctx, "auth: Invalid delegation token kind - %s", subtoken.Kind) 179 return "", ErrForbiddenDelegationToken 180 } 181 182 // Do fast checks before heavy ones. 183 now := clock.Now(ctx).Unix() 184 if err := checkSubtokenExpiration(subtoken, now); err != nil { 185 logging.Warningf(ctx, "auth: Bad delegation token expiration - %s", err) 186 return "", ErrForbiddenDelegationToken 187 } 188 if err := checkSubtokenServices(subtoken, params.OwnServiceIdentity); err != nil { 189 logging.Warningf(ctx, "auth: Forbidden delegation token - %s", err) 190 return "", ErrForbiddenDelegationToken 191 } 192 193 // Do the audience check (may use group lookups). 194 if err := checkSubtokenAudience(ctx, subtoken, params.PeerID, params.GroupsChecker); err != nil { 195 if transient.Tag.In(err) { 196 logging.Warningf(ctx, "auth: Transient error when checking delegation token audience - %s", err) 197 return "", err 198 } 199 logging.Warningf(ctx, "auth: Bad delegation token audience - %s", err) 200 return "", ErrForbiddenDelegationToken 201 } 202 203 // Grab delegated identity. 204 ident, err := identity.MakeIdentity(subtoken.DelegatedIdentity) 205 if err != nil { 206 logging.Warningf(ctx, "auth: Invalid delegated_identity in the delegation token - %s", err) 207 return "", ErrMalformedDelegationToken 208 } 209 210 return ident, nil 211 } 212 213 // checkSubtokenExpiration checks 'CreationTime' and 'ValidityDuration' fields. 214 func checkSubtokenExpiration(t *messages.Subtoken, now int64) error { 215 if t.CreationTime <= 0 { 216 return fmt.Errorf("invalid 'creation_time' field: %d", t.CreationTime) 217 } 218 dur := int64(t.ValidityDuration) 219 if dur <= 0 { 220 return fmt.Errorf("invalid validity_duration: %d", dur) 221 } 222 if t.CreationTime >= now+allowedClockDriftSec { 223 return fmt.Errorf("token is not active yet (created at %d)", t.CreationTime) 224 } 225 if t.CreationTime+dur < now { 226 return fmt.Errorf("token has expired %d sec ago", now-(t.CreationTime+dur)) 227 } 228 return nil 229 } 230 231 // checkSubtokenServices makes sure the token is usable by the current service. 232 func checkSubtokenServices(t *messages.Subtoken, serviceID identity.Identity) error { 233 // Empty services field is not allowed. 234 if len(t.Services) == 0 { 235 return fmt.Errorf("the token's services list is empty") 236 } 237 // Else, make sure we are in the 'services' list or it contains '*'. 238 for _, allowed := range t.Services { 239 if allowed == "*" || allowed == string(serviceID) { 240 return nil 241 } 242 } 243 return fmt.Errorf("token is not intended for %s", serviceID) 244 } 245 246 // checkSubtokenAudience makes sure the token is intended for use by given 247 // identity. 248 // 249 // May return transient errors. 250 func checkSubtokenAudience(ctx context.Context, t *messages.Subtoken, ident identity.Identity, checker GroupsChecker) error { 251 // Empty audience field is not allowed. 252 if len(t.Audience) == 0 { 253 return fmt.Errorf("the token's audience list is empty") 254 } 255 // Try to find a direct hit first, to avoid calling expensive group lookups. 256 // Collect the groups along the way for the check below. 257 groups := make([]string, 0, len(t.Audience)) 258 for _, aud := range t.Audience { 259 if aud == "*" || aud == string(ident) { 260 return nil 261 } 262 if strings.HasPrefix(aud, "group:") { 263 groups = append(groups, strings.TrimPrefix(aud, "group:")) 264 } 265 } 266 // Search through groups now. 267 switch ok, err := checker.IsMember(ctx, ident, groups); { 268 case err != nil: 269 return err // transient error during group lookup 270 case ok: 271 return nil // success, 'ident' is in the target audience 272 } 273 return fmt.Errorf("%s is not allowed to use the token", ident) 274 }