| // Copyright 2017 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 openid |
| |
| import ( |
| "crypto" |
| "crypto/rsa" |
| "crypto/sha256" |
| "encoding/base64" |
| "encoding/binary" |
| "math/big" |
| |
| "go.chromium.org/luci/common/errors" |
| ) |
| |
| // JSONWebKeySet implements subset of functionality described in RFC7517. |
| // |
| // It currently supports only RSA keys and RS256 alg. It's intended to be used |
| // to represent keys fetched from https://www.googleapis.com/oauth2/v3/certs. |
| // |
| // It's used to verify ID token signatures. |
| type JSONWebKeySet struct { |
| keys map[string]rsa.PublicKey // key ID => the key |
| } |
| |
| // JSONWebKeySetStruct defines the JSON structure of JSONWebKeySet. |
| // |
| // Read it from the wire and pass to NewJSONWebKeySet to get a usable object. |
| // |
| // See https://www.iana.org/assignments/jose/jose.xhtml#web-key-parameters. |
| // We care only about RSA public keys (thus use 'n' and 'e'). |
| type JSONWebKeySetStruct struct { |
| Keys []JSONWebKeyStruct `json:"keys"` |
| } |
| |
| // JSONWebKeyStruct defines the JSON structure of a single key in the key set. |
| type JSONWebKeyStruct struct { |
| Kty string `json:"kty"` |
| Alg string `json:"alg"` |
| Use string `json:"use"` |
| Kid string `json:"kid"` |
| N string `json:"n"` // raw URL-safe base64, NOT standard base64 |
| E string `json:"e"` // same |
| } |
| |
| // NewJSONWebKeySet makes the keyset from raw JSON Web Key set struct. |
| func NewJSONWebKeySet(parsed *JSONWebKeySetStruct) (*JSONWebKeySet, error) { |
| // Pick keys used to verify RS256 signatures. |
| keys := make(map[string]rsa.PublicKey, len(parsed.Keys)) |
| for _, k := range parsed.Keys { |
| if k.Kty != "RSA" || k.Alg != "RS256" || k.Use != "sig" { |
| continue // not an RSA public key |
| } |
| if k.Kid == "" { |
| // Per spec 'kid' field is optional, but providers we support return them, |
| // so make them required to keep the code simpler. |
| return nil, errors.Reason("bad JSON web key: missing 'kid' field").Err() |
| } |
| pub, err := decodeRSAPublicKey(k.N, k.E) |
| if err != nil { |
| return nil, errors.Annotate(err, "failed to parse RSA public key in JSON web key").Err() |
| } |
| keys[k.Kid] = pub |
| } |
| |
| if len(keys) == 0 { |
| return nil, errors.Reason("the JSON web key doc didn't have any signing keys").Err() |
| } |
| |
| return &JSONWebKeySet{keys: keys}, nil |
| } |
| |
| // CheckSignature returns nil if `signed` was indeed signed by the given key. |
| func (k *JSONWebKeySet) CheckSignature(keyID string, signed, signature []byte) error { |
| pub, ok := k.keys[keyID] |
| if !ok { |
| return errors.Reason("unknown signing key %q", keyID).Err() |
| } |
| digest := sha256.Sum256(signed) |
| if err := rsa.VerifyPKCS1v15(&pub, crypto.SHA256, digest[:], signature); err != nil { |
| return errors.Reason("bad signature").Err() |
| } |
| return nil |
| } |
| |
| func decodeRSAPublicKey(n, e string) (rsa.PublicKey, error) { |
| modulus, err := base64.RawURLEncoding.DecodeString(n) |
| if err != nil { |
| return rsa.PublicKey{}, errors.Annotate(err, "bad modulus encoding").Err() |
| } |
| exp, err := base64.RawURLEncoding.DecodeString(e) |
| if err != nil { |
| return rsa.PublicKey{}, errors.Annotate(err, "bad exponent encoding").Err() |
| } |
| |
| // The exponent should be 4 bytes in BigEndian order. Pad it with zeros if |
| // necessary. |
| if len(exp) < 4 { |
| padded := make([]byte, 4) |
| copy(padded[4-len(exp):], exp) |
| exp = padded |
| } |
| |
| return rsa.PublicKey{ |
| N: (&big.Int{}).SetBytes(modulus), |
| E: int(binary.BigEndian.Uint32(exp)), |
| }, nil |
| } |