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  }