go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/signing/certs.go (about) 1 // Copyright 2015 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 signing 16 17 import ( 18 "context" 19 "crypto/x509" 20 "encoding/pem" 21 "fmt" 22 "net/url" 23 "sort" 24 "strconv" 25 "strings" 26 "sync" 27 "time" 28 29 "go.chromium.org/luci/server/auth/internal" 30 "go.chromium.org/luci/server/caching" 31 ) 32 33 // "url:..." | "email:..." | "google_auth2_certs" => *PublicCertificates. 34 var certsCache = caching.RegisterLRUCache[string, *PublicCertificates](1024) 35 36 const ( 37 robotCertsURL = "https://www.googleapis.com/robot/v1/metadata/x509/" 38 oauth2CertsURL = "https://www.googleapis.com/oauth2/v1/certs" 39 ) 40 41 // CertsCacheExpiration defines how long to cache fetched certificates in local 42 // memory. 43 const CertsCacheExpiration = time.Hour 44 45 // Certificate is public certificate of some service. Must not be mutated once 46 // initialized. 47 type Certificate struct { 48 // KeyName identifies the key used for signing. 49 KeyName string `json:"key_name"` 50 // X509CertificatePEM is PEM encoded certificate. 51 X509CertificatePEM string `json:"x509_certificate_pem"` 52 } 53 54 // PublicCertificates is a bundle of recent certificates of some service. Must 55 // not be mutated once initialized. 56 type PublicCertificates struct { 57 // AppID is GAE app ID of a service that owns the keys if it is on GAE. 58 AppID string `json:"app_id,omitempty"` 59 // ServiceAccountName is name of a service account that owns the key, if any. 60 ServiceAccountName string `json:"service_account_name,omitempty"` 61 // Certificates is the list of certificates. 62 Certificates []Certificate `json:"certificates"` 63 // Timestamp is Unix time (microseconds) of when this list was generated. 64 Timestamp JSONTime `json:"timestamp"` 65 66 lock sync.RWMutex 67 cache map[string]*x509.Certificate 68 } 69 70 // JSONTime is time.Time that serializes as unix timestamp (in microseconds). 71 type JSONTime time.Time 72 73 // Time casts value to time.Time. 74 func (t JSONTime) Time() time.Time { 75 return time.Time(t) 76 } 77 78 // UnmarshalJSON implements json.Unmarshaler. 79 func (t *JSONTime) UnmarshalJSON(data []byte) error { 80 ts, err := strconv.ParseInt(string(data), 10, 64) 81 if err != nil { 82 return err 83 } 84 *t = JSONTime(time.Unix(0, ts*1000)) 85 return nil 86 } 87 88 // MarshalJSON implements json.Marshaler. 89 func (t JSONTime) MarshalJSON() ([]byte, error) { 90 ts := t.Time().UnixNano() / 1000 91 return []byte(strconv.FormatInt(ts, 10)), nil 92 } 93 94 // FetchCertificates fetches certificates from the given URL. 95 // 96 // The server is expected to reply with JSON described by PublicCertificates 97 // struct (like LUCI services do). Uses the process cache to cache them for 98 // CertsCacheExpiration minutes. 99 // 100 // LUCI services serve certificates at /auth/api/v1/server/certificates. 101 func FetchCertificates(ctx context.Context, url string) (*PublicCertificates, error) { 102 return certsCache.LRU(ctx).GetOrCreate(ctx, "url:"+url, func() (*PublicCertificates, time.Duration, error) { 103 certs := &PublicCertificates{} 104 req := internal.Request{ 105 Method: "GET", 106 URL: url, 107 Out: certs, 108 } 109 if err := req.Do(ctx); err != nil { 110 return nil, 0, err 111 } 112 return certs, CertsCacheExpiration, nil 113 }) 114 } 115 116 // FetchCertificatesFromLUCIService is shortcut for FetchCertificates 117 // that uses LUCI-specific endpoint. 118 // 119 // 'serviceURL' is root URL of the service (e.g. 'https://example.com'). 120 func FetchCertificatesFromLUCIService(ctx context.Context, serviceURL string) (*PublicCertificates, error) { 121 return FetchCertificates(ctx, serviceURL+"/auth/api/v1/server/certificates") 122 } 123 124 // FetchCertificatesForServiceAccount fetches certificates of some Google 125 // service account. 126 // 127 // Works only with Google service accounts (@*.gserviceaccount.com). Uses the 128 // process cache to cache them for CertsCacheExpiration minutes. 129 // 130 // Usage (roughly): 131 // 132 // certs, err := signing.FetchCertificatesForServiceAccount(ctx, <email>) 133 // if certs.CheckSignature(<key id>, <blob>, <signature>) == nil { 134 // <signature is valid!> 135 // } 136 func FetchCertificatesForServiceAccount(ctx context.Context, email string) (*PublicCertificates, error) { 137 // Do only basic validation and offload full validation to the google backend. 138 if !strings.HasSuffix(email, ".gserviceaccount.com") { 139 return nil, fmt.Errorf("signature: not a google service account %q", email) 140 } 141 return certsCache.LRU(ctx).GetOrCreate(ctx, "email:"+email, func() (*PublicCertificates, time.Duration, error) { 142 certs, err := fetchCertsJSON(ctx, robotCertsURL+url.QueryEscape(email)) 143 if err != nil { 144 return nil, 0, err 145 } 146 certs.ServiceAccountName = email 147 return certs, CertsCacheExpiration, nil 148 }) 149 } 150 151 // FetchGoogleOAuth2Certificates fetches root certificates of Google OAuth2 152 // service. 153 // 154 // They can be used to verify signatures on various JWTs issued by Google 155 // OAuth2 backends (like OpenID identity tokens and GCE signed metadata JWTs). 156 // 157 // Uses the process cache to cache them for CertsCacheExpiration minutes. 158 func FetchGoogleOAuth2Certificates(ctx context.Context) (*PublicCertificates, error) { 159 return certsCache.LRU(ctx).GetOrCreate(ctx, "google_auth2_certs", func() (*PublicCertificates, time.Duration, error) { 160 certs, err := fetchCertsJSON(ctx, oauth2CertsURL) 161 if err != nil { 162 return nil, 0, err 163 } 164 return certs, CertsCacheExpiration, nil 165 }) 166 } 167 168 // fetchCertsJSON loads certificates from a JSON dict "key id => x509 PEM cert". 169 // 170 // This is the format served by Google certificate endpoints. 171 func fetchCertsJSON(ctx context.Context, url string) (*PublicCertificates, error) { 172 keysAndCerts := map[string]string{} 173 req := internal.Request{ 174 Method: "GET", 175 URL: url, 176 Out: &keysAndCerts, 177 } 178 if err := req.Do(ctx); err != nil { 179 return nil, err 180 } 181 182 // Sort by key for reproducibility of return values. 183 keys := make([]string, 0, len(keysAndCerts)) 184 for key := range keysAndCerts { 185 keys = append(keys, key) 186 } 187 sort.Strings(keys) 188 189 // Convert to PublicCertificates struct. 190 certs := &PublicCertificates{} 191 for _, key := range keys { 192 certs.Certificates = append(certs.Certificates, Certificate{ 193 KeyName: key, 194 X509CertificatePEM: keysAndCerts[key], 195 }) 196 } 197 return certs, nil 198 } 199 200 // CertificateForKey finds the certificate for given key and deserializes it. 201 func (pc *PublicCertificates) CertificateForKey(key string) (*x509.Certificate, error) { 202 // Use fast reader lock first. 203 pc.lock.RLock() 204 cert, ok := pc.cache[key] 205 pc.lock.RUnlock() 206 if ok { 207 return cert, nil 208 } 209 210 // Grab the write lock and recheck the cache. 211 pc.lock.Lock() 212 defer pc.lock.Unlock() 213 if cert, ok := pc.cache[key]; ok { 214 return cert, nil 215 } 216 217 for _, cert := range pc.Certificates { 218 if cert.KeyName == key { 219 block, _ := pem.Decode([]byte(cert.X509CertificatePEM)) 220 if block == nil { 221 return nil, fmt.Errorf("signature: the certificate %q is not PEM encoded", key) 222 } 223 cert, err := x509.ParseCertificate(block.Bytes) 224 if err != nil { 225 return nil, err 226 } 227 if pc.cache == nil { 228 pc.cache = make(map[string]*x509.Certificate) 229 } 230 pc.cache[key] = cert 231 return cert, nil 232 } 233 } 234 235 return nil, fmt.Errorf("signature: no such certificate %q", key) 236 } 237 238 // CheckSignature returns nil if `signed` was indeed signed by given key. 239 func (pc *PublicCertificates) CheckSignature(key string, signed, signature []byte) error { 240 cert, err := pc.CertificateForKey(key) 241 if err != nil { 242 return err 243 } 244 return cert.CheckSignature(x509.SHA256WithRSA, signed, signature) 245 }