mirror of
https://github.com/golang/oauth2.git
synced 2025-07-21 00:00:09 +08:00
The fix for CVE-2025-22868 relies on strings.Count, which isn't ideal because it precludes failing fast when the token contains an unexpected number of periods. Moreover, Verify still allocates more than necessary. Eschew strings.Count in favor of strings.Cut. Some benchmark results: goos: darwin goarch: amd64 pkg: golang.org/x/oauth2/jws cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz │ old │ new │ │ sec/op │ sec/op vs base │ Verify/full_of_periods-8 24862.50n ± 1% 57.87n ± 0% -99.77% (p=0.000 n=20) Verify/two_trailing_periods-8 3.485m ± 1% 3.445m ± 1% -1.13% (p=0.003 n=20) geomean 294.3µ 14.12µ -95.20% │ old │ new │ │ B/op │ B/op vs base │ Verify/full_of_periods-8 16.00 ± 0% 16.00 ± 0% ~ (p=1.000 n=20) ¹ Verify/two_trailing_periods-8 2.001Mi ± 0% 1.001Mi ± 0% -49.98% (p=0.000 n=20) geomean 5.658Ki 4.002Ki -29.27% ¹ all samples are equal │ old │ new │ │ allocs/op │ allocs/op vs base │ Verify/full_of_periods-8 1.000 ± 0% 1.000 ± 0% ~ (p=1.000 n=20) ¹ Verify/two_trailing_periods-8 12.000 ± 0% 9.000 ± 0% -25.00% (p=0.000 n=20) geomean 3.464 3.000 -13.40% ¹ all samples are equal Also, remove all remaining calls to strings.Split. Updates golang/go#71490 Change-Id: Icac3c7a81562161ab6533d892ba19247d6d5b943 GitHub-Last-Rev: 3a82900f747798f5f36065126385880277c0fce7 GitHub-Pull-Request: golang/oauth2#774 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/655455 Commit-Queue: Neal Patel <nealpatel@google.com> Reviewed-by: Roland Shoemaker <roland@golang.org> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Neal Patel <nealpatel@google.com> Auto-Submit: Neal Patel <nealpatel@google.com>
199 lines
6.2 KiB
Go
199 lines
6.2 KiB
Go
// Copyright 2014 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package jws provides a partial implementation
|
|
// of JSON Web Signature encoding and decoding.
|
|
// It exists to support the golang.org/x/oauth2 package.
|
|
//
|
|
// See RFC 7515.
|
|
//
|
|
// Deprecated: this package is not intended for public use and might be
|
|
// removed in the future. It exists for internal use only.
|
|
// Please switch to another JWS package or copy this package into your own
|
|
// source tree.
|
|
package jws // import "golang.org/x/oauth2/jws"
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ClaimSet contains information about the JWT signature including the
|
|
// permissions being requested (scopes), the target of the token, the issuer,
|
|
// the time the token was issued, and the lifetime of the token.
|
|
type ClaimSet struct {
|
|
Iss string `json:"iss"` // email address of the client_id of the application making the access token request
|
|
Scope string `json:"scope,omitempty"` // space-delimited list of the permissions the application requests
|
|
Aud string `json:"aud"` // descriptor of the intended target of the assertion (Optional).
|
|
Exp int64 `json:"exp"` // the expiration time of the assertion (seconds since Unix epoch)
|
|
Iat int64 `json:"iat"` // the time the assertion was issued (seconds since Unix epoch)
|
|
Typ string `json:"typ,omitempty"` // token type (Optional).
|
|
|
|
// Email for which the application is requesting delegated access (Optional).
|
|
Sub string `json:"sub,omitempty"`
|
|
|
|
// The old name of Sub. Client keeps setting Prn to be
|
|
// complaint with legacy OAuth 2.0 providers. (Optional)
|
|
Prn string `json:"prn,omitempty"`
|
|
|
|
// See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3
|
|
// This array is marshalled using custom code (see (c *ClaimSet) encode()).
|
|
PrivateClaims map[string]interface{} `json:"-"`
|
|
}
|
|
|
|
func (c *ClaimSet) encode() (string, error) {
|
|
// Reverting time back for machines whose time is not perfectly in sync.
|
|
// If client machine's time is in the future according
|
|
// to Google servers, an access token will not be issued.
|
|
now := time.Now().Add(-10 * time.Second)
|
|
if c.Iat == 0 {
|
|
c.Iat = now.Unix()
|
|
}
|
|
if c.Exp == 0 {
|
|
c.Exp = now.Add(time.Hour).Unix()
|
|
}
|
|
if c.Exp < c.Iat {
|
|
return "", fmt.Errorf("jws: invalid Exp = %v; must be later than Iat = %v", c.Exp, c.Iat)
|
|
}
|
|
|
|
b, err := json.Marshal(c)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(c.PrivateClaims) == 0 {
|
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
|
}
|
|
|
|
// Marshal private claim set and then append it to b.
|
|
prv, err := json.Marshal(c.PrivateClaims)
|
|
if err != nil {
|
|
return "", fmt.Errorf("jws: invalid map of private claims %v", c.PrivateClaims)
|
|
}
|
|
|
|
// Concatenate public and private claim JSON objects.
|
|
if !bytes.HasSuffix(b, []byte{'}'}) {
|
|
return "", fmt.Errorf("jws: invalid JSON %s", b)
|
|
}
|
|
if !bytes.HasPrefix(prv, []byte{'{'}) {
|
|
return "", fmt.Errorf("jws: invalid JSON %s", prv)
|
|
}
|
|
b[len(b)-1] = ',' // Replace closing curly brace with a comma.
|
|
b = append(b, prv[1:]...) // Append private claims.
|
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
|
}
|
|
|
|
// Header represents the header for the signed JWS payloads.
|
|
type Header struct {
|
|
// The algorithm used for signature.
|
|
Algorithm string `json:"alg"`
|
|
|
|
// Represents the token type.
|
|
Typ string `json:"typ"`
|
|
|
|
// The optional hint of which key is being used.
|
|
KeyID string `json:"kid,omitempty"`
|
|
}
|
|
|
|
func (h *Header) encode() (string, error) {
|
|
b, err := json.Marshal(h)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
|
}
|
|
|
|
// Decode decodes a claim set from a JWS payload.
|
|
func Decode(payload string) (*ClaimSet, error) {
|
|
// decode returned id token to get expiry
|
|
_, claims, _, ok := parseToken(payload)
|
|
if !ok {
|
|
// TODO(jbd): Provide more context about the error.
|
|
return nil, errors.New("jws: invalid token received")
|
|
}
|
|
decoded, err := base64.RawURLEncoding.DecodeString(claims)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c := &ClaimSet{}
|
|
err = json.NewDecoder(bytes.NewBuffer(decoded)).Decode(c)
|
|
return c, err
|
|
}
|
|
|
|
// Signer returns a signature for the given data.
|
|
type Signer func(data []byte) (sig []byte, err error)
|
|
|
|
// EncodeWithSigner encodes a header and claim set with the provided signer.
|
|
func EncodeWithSigner(header *Header, c *ClaimSet, sg Signer) (string, error) {
|
|
head, err := header.encode()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
cs, err := c.encode()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
ss := fmt.Sprintf("%s.%s", head, cs)
|
|
sig, err := sg([]byte(ss))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("%s.%s", ss, base64.RawURLEncoding.EncodeToString(sig)), nil
|
|
}
|
|
|
|
// Encode encodes a signed JWS with provided header and claim set.
|
|
// This invokes EncodeWithSigner using crypto/rsa.SignPKCS1v15 with the given RSA private key.
|
|
func Encode(header *Header, c *ClaimSet, key *rsa.PrivateKey) (string, error) {
|
|
sg := func(data []byte) (sig []byte, err error) {
|
|
h := sha256.New()
|
|
h.Write(data)
|
|
return rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, h.Sum(nil))
|
|
}
|
|
return EncodeWithSigner(header, c, sg)
|
|
}
|
|
|
|
// Verify tests whether the provided JWT token's signature was produced by the private key
|
|
// associated with the supplied public key.
|
|
func Verify(token string, key *rsa.PublicKey) error {
|
|
header, claims, sig, ok := parseToken(token)
|
|
if !ok {
|
|
return errors.New("jws: invalid token received, token must have 3 parts")
|
|
}
|
|
signatureString, err := base64.RawURLEncoding.DecodeString(sig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
h := sha256.New()
|
|
h.Write([]byte(header + tokenDelim + claims))
|
|
return rsa.VerifyPKCS1v15(key, crypto.SHA256, h.Sum(nil), signatureString)
|
|
}
|
|
|
|
func parseToken(s string) (header, claims, sig string, ok bool) {
|
|
header, s, ok = strings.Cut(s, tokenDelim)
|
|
if !ok { // no period found
|
|
return "", "", "", false
|
|
}
|
|
claims, s, ok = strings.Cut(s, tokenDelim)
|
|
if !ok { // only one period found
|
|
return "", "", "", false
|
|
}
|
|
sig, _, ok = strings.Cut(s, tokenDelim)
|
|
if ok { // three periods found
|
|
return "", "", "", false
|
|
}
|
|
return header, claims, sig, true
|
|
}
|
|
|
|
const tokenDelim = "."
|