blob: 9e8a6e10edf13f7ecde1573944557d80b9fad37c [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package signing
import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"net/url"
"sort"
"strconv"
"strings"
"sync"
"time"
"go.chromium.org/luci/server/auth/internal"
"go.chromium.org/luci/server/caching"
)
// "url:..." | "email:..." | "google_auth2_certs" => *PublicCertificates.
var certsCache = caching.RegisterLRUCache(1024)
const (
robotCertsURL = "https://www.googleapis.com/robot/v1/metadata/x509/"
oauth2CertsURL = "https://www.googleapis.com/oauth2/v1/certs"
)
// CertsCacheExpiration defines how long to cache fetched certificates in local
// memory.
const CertsCacheExpiration = time.Hour
// Certificate is public certificate of some service. Must not be mutated once
// initialized.
type Certificate struct {
// KeyName identifies the key used for signing.
KeyName string `json:"key_name"`
// X509CertificatePEM is PEM encoded certificate.
X509CertificatePEM string `json:"x509_certificate_pem"`
}
// PublicCertificates is a bundle of recent certificates of some service. Must
// not be mutated once initialized.
type PublicCertificates struct {
// AppID is GAE app ID of a service that owns the keys if it is on GAE.
AppID string `json:"app_id,omitempty"`
// ServiceAccountName is name of a service account that owns the key, if any.
ServiceAccountName string `json:"service_account_name,omitempty"`
// Certificates is the list of certificates.
Certificates []Certificate `json:"certificates"`
// Timestamp is Unix time (microseconds) of when this list was generated.
Timestamp JSONTime `json:"timestamp"`
lock sync.RWMutex
cache map[string]*x509.Certificate
}
// JSONTime is time.Time that serializes as unix timestamp (in microseconds).
type JSONTime time.Time
// Time casts value to time.Time.
func (t JSONTime) Time() time.Time {
return time.Time(t)
}
// UnmarshalJSON implements json.Unmarshaler.
func (t *JSONTime) UnmarshalJSON(data []byte) error {
ts, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return err
}
*t = JSONTime(time.Unix(0, ts*1000))
return nil
}
// MarshalJSON implements json.Marshaler.
func (t JSONTime) MarshalJSON() ([]byte, error) {
ts := t.Time().UnixNano() / 1000
return []byte(strconv.FormatInt(ts, 10)), nil
}
// FetchCertificates fetches certificates from the given URL.
//
// The server is expected to reply with JSON described by PublicCertificates
// struct (like LUCI services do). Uses the process cache to cache them for
// CertsCacheExpiration minutes.
//
// LUCI services serve certificates at /auth/api/v1/server/certificates.
func FetchCertificates(c context.Context, url string) (*PublicCertificates, error) {
certs, err := certsCache.LRU(c).GetOrCreate(c, "url:"+url, func() (interface{}, time.Duration, error) {
certs := &PublicCertificates{}
req := internal.Request{
Method: "GET",
URL: url,
Out: certs,
}
if err := req.Do(c); err != nil {
return nil, 0, err
}
return certs, CertsCacheExpiration, nil
})
if err != nil {
return nil, err
}
return certs.(*PublicCertificates), nil
}
// FetchCertificatesFromLUCIService is shortcut for FetchCertificates
// that uses LUCI-specific endpoint.
//
// 'serviceURL' is root URL of the service (e.g. 'https://example.com').
func FetchCertificatesFromLUCIService(c context.Context, serviceURL string) (*PublicCertificates, error) {
return FetchCertificates(c, serviceURL+"/auth/api/v1/server/certificates")
}
// FetchCertificatesForServiceAccount fetches certificates of some Google
// service account.
//
// Works only with Google service accounts (@*.gserviceaccount.com). Uses the
// process cache to cache them for CertsCacheExpiration minutes.
//
// Usage (roughly):
//
// certs, err := signing.FetchCertificatesForServiceAccount(ctx, <email>)
// if certs.CheckSignature(<key id>, <blob>, <signature>) == nil {
// <signature is valid!>
// }
func FetchCertificatesForServiceAccount(c context.Context, email string) (*PublicCertificates, error) {
// Do only basic validation and offload full validation to the google backend.
if !strings.HasSuffix(email, ".gserviceaccount.com") {
return nil, fmt.Errorf("signature: not a google service account %q", email)
}
certs, err := certsCache.LRU(c).GetOrCreate(c, "email:"+email, func() (interface{}, time.Duration, error) {
certs, err := fetchCertsJSON(c, robotCertsURL+url.QueryEscape(email))
if err != nil {
return nil, 0, err
}
certs.ServiceAccountName = email
return certs, CertsCacheExpiration, nil
})
if err != nil {
return nil, err
}
return certs.(*PublicCertificates), nil
}
// FetchGoogleOAuth2Certificates fetches root certificates of Google OAuth2
// service.
//
// They can be used to verify signatures on various JWTs issued by Google
// OAuth2 backends (like OpenID identity tokens and GCE signed metadata JWTs).
//
// Uses the process cache to cache them for CertsCacheExpiration minutes.
func FetchGoogleOAuth2Certificates(c context.Context) (*PublicCertificates, error) {
certs, err := certsCache.LRU(c).GetOrCreate(c, "google_auth2_certs", func() (interface{}, time.Duration, error) {
certs, err := fetchCertsJSON(c, oauth2CertsURL)
if err != nil {
return nil, 0, err
}
return certs, CertsCacheExpiration, nil
})
if err != nil {
return nil, err
}
return certs.(*PublicCertificates), nil
}
// fetchCertsJSON loads certificates from a JSON dict "key id => x509 PEM cert".
//
// This is the format served by Google certificate endpoints.
func fetchCertsJSON(c context.Context, url string) (*PublicCertificates, error) {
keysAndCerts := map[string]string{}
req := internal.Request{
Method: "GET",
URL: url,
Out: &keysAndCerts,
}
if err := req.Do(c); err != nil {
return nil, err
}
// Sort by key for reproducibility of return values.
keys := make([]string, 0, len(keysAndCerts))
for key := range keysAndCerts {
keys = append(keys, key)
}
sort.Strings(keys)
// Convert to PublicCertificates struct.
certs := &PublicCertificates{}
for _, key := range keys {
certs.Certificates = append(certs.Certificates, Certificate{
KeyName: key,
X509CertificatePEM: keysAndCerts[key],
})
}
return certs, nil
}
// CertificateForKey finds the certificate for given key and deserializes it.
func (pc *PublicCertificates) CertificateForKey(key string) (*x509.Certificate, error) {
// Use fast reader lock first.
pc.lock.RLock()
cert, ok := pc.cache[key]
pc.lock.RUnlock()
if ok {
return cert, nil
}
// Grab the write lock and recheck the cache.
pc.lock.Lock()
defer pc.lock.Unlock()
if cert, ok := pc.cache[key]; ok {
return cert, nil
}
for _, cert := range pc.Certificates {
if cert.KeyName == key {
block, _ := pem.Decode([]byte(cert.X509CertificatePEM))
if block == nil {
return nil, fmt.Errorf("signature: the certificate %q is not PEM encoded", key)
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
if pc.cache == nil {
pc.cache = make(map[string]*x509.Certificate)
}
pc.cache[key] = cert
return cert, nil
}
}
return nil, fmt.Errorf("signature: no such certificate %q", key)
}
// CheckSignature returns nil if `signed` was indeed signed by given key.
func (pc *PublicCertificates) CheckSignature(key string, signed, signature []byte) error {
cert, err := pc.CertificateForKey(key)
if err != nil {
return err
}
return cert.CheckSignature(x509.SHA256WithRSA, signed, signature)
}