161 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			161 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			Go
		
	
	
	
package unpause
 | 
						|
 | 
						|
import (
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/go-jose/go-jose/v4"
 | 
						|
	"github.com/go-jose/go-jose/v4/jwt"
 | 
						|
	"github.com/jmhodges/clock"
 | 
						|
 | 
						|
	"github.com/letsencrypt/boulder/cmd"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	// API
 | 
						|
 | 
						|
	// Changing this value will invalidate all existing JWTs.
 | 
						|
	APIVersion = "v1"
 | 
						|
	APIPrefix  = "/sfe/" + APIVersion
 | 
						|
	GetForm    = APIPrefix + "/unpause"
 | 
						|
 | 
						|
	// BatchSize is the maximum number of identifiers that the SA will unpause
 | 
						|
	// in a single batch.
 | 
						|
	BatchSize = 10000
 | 
						|
 | 
						|
	// MaxBatches is the maximum number of batches that the SA will unpause in a
 | 
						|
	// single request.
 | 
						|
	MaxBatches = 5
 | 
						|
 | 
						|
	// RequestLimit is the maximum number of identifiers that the SA will
 | 
						|
	// unpause in a single request. This is used by the SFE to infer whether
 | 
						|
	// there are more identifiers to unpause.
 | 
						|
	RequestLimit = BatchSize * MaxBatches
 | 
						|
 | 
						|
	// JWT
 | 
						|
	defaultIssuer   = "WFE"
 | 
						|
	defaultAudience = "SFE Unpause"
 | 
						|
)
 | 
						|
 | 
						|
// JWTSigner is a type alias for jose.Signer. To create a JWTSigner instance,
 | 
						|
// use the NewJWTSigner function provided in this package.
 | 
						|
type JWTSigner = jose.Signer
 | 
						|
 | 
						|
// NewJWTSigner loads the HMAC key from the provided configuration and returns a
 | 
						|
// new JWT signer.
 | 
						|
func NewJWTSigner(hmacKey cmd.HMACKeyConfig) (JWTSigner, error) {
 | 
						|
	key, err := hmacKey.Load()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key}, nil)
 | 
						|
}
 | 
						|
 | 
						|
// JWTClaims represents the claims of a JWT token issued by the WFE for
 | 
						|
// redemption by the SFE. The following claims required for unpausing:
 | 
						|
//   - Subject: the account ID of the Subscriber
 | 
						|
//   - V: the API version this JWT was created for
 | 
						|
//   - I: a set of ACME identifier values. Identifier types are omitted
 | 
						|
//     since DNS and IP string representations do not overlap.
 | 
						|
type JWTClaims struct {
 | 
						|
	jwt.Claims
 | 
						|
 | 
						|
	// V is the API version this JWT was created for.
 | 
						|
	V string `json:"version"`
 | 
						|
 | 
						|
	// I is set of comma separated ACME identifiers.
 | 
						|
	I string `json:"identifiers"`
 | 
						|
}
 | 
						|
 | 
						|
// GenerateJWT generates a serialized unpause JWT with the provided claims.
 | 
						|
func GenerateJWT(signer JWTSigner, regID int64, idents []string, lifetime time.Duration, clk clock.Clock) (string, error) {
 | 
						|
	claims := JWTClaims{
 | 
						|
		Claims: jwt.Claims{
 | 
						|
			Issuer:   defaultIssuer,
 | 
						|
			Subject:  fmt.Sprintf("%d", regID),
 | 
						|
			Audience: jwt.Audience{defaultAudience},
 | 
						|
			// IssuedAt is necessary for metrics.
 | 
						|
			IssuedAt: jwt.NewNumericDate(clk.Now()),
 | 
						|
			Expiry:   jwt.NewNumericDate(clk.Now().Add(lifetime)),
 | 
						|
		},
 | 
						|
		V: APIVersion,
 | 
						|
		I: strings.Join(idents, ","),
 | 
						|
	}
 | 
						|
 | 
						|
	serialized, err := jwt.Signed(signer).Claims(&claims).Serialize()
 | 
						|
	if err != nil {
 | 
						|
		return "", fmt.Errorf("serializing JWT: %s", err)
 | 
						|
	}
 | 
						|
 | 
						|
	return serialized, nil
 | 
						|
}
 | 
						|
 | 
						|
// ErrMalformedJWT is returned when the JWT is malformed.
 | 
						|
var ErrMalformedJWT = errors.New("malformed JWT")
 | 
						|
 | 
						|
// RedeemJWT deserializes an unpause JWT and returns the validated claims. The
 | 
						|
// key is used to validate the signature of the JWT. The version is the expected
 | 
						|
// API version of the JWT. This function validates that the JWT is:
 | 
						|
//   - well-formed,
 | 
						|
//   - valid for the current time (+/- 1 minute leeway),
 | 
						|
//   - issued by the WFE,
 | 
						|
//   - intended for the SFE,
 | 
						|
//   - contains an Account ID as the 'Subject',
 | 
						|
//   - subject can be parsed as a 64-bit integer,
 | 
						|
//   - contains a set of paused identifiers as 'Identifiers', and
 | 
						|
//   - contains the API the expected version as 'Version'.
 | 
						|
//
 | 
						|
// If the JWT is malformed or invalid in any way, ErrMalformedJWT is returned.
 | 
						|
func RedeemJWT(token string, key []byte, version string, clk clock.Clock) (JWTClaims, error) {
 | 
						|
	parsedToken, err := jwt.ParseSigned(token, []jose.SignatureAlgorithm{jose.HS256})
 | 
						|
	if err != nil {
 | 
						|
		return JWTClaims{}, errors.Join(ErrMalformedJWT, err)
 | 
						|
	}
 | 
						|
 | 
						|
	claims := JWTClaims{}
 | 
						|
	err = parsedToken.Claims(key, &claims)
 | 
						|
	if err != nil {
 | 
						|
		return JWTClaims{}, errors.Join(ErrMalformedJWT, err)
 | 
						|
	}
 | 
						|
 | 
						|
	err = claims.Validate(jwt.Expected{
 | 
						|
		Issuer:      defaultIssuer,
 | 
						|
		AnyAudience: jwt.Audience{defaultAudience},
 | 
						|
 | 
						|
		// By default, the go-jose library validates the NotBefore and Expiry
 | 
						|
		// fields with a default leeway of 1 minute.
 | 
						|
		Time: clk.Now(),
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		return JWTClaims{}, fmt.Errorf("validating JWT: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	if len(claims.Subject) == 0 {
 | 
						|
		return JWTClaims{}, errors.New("no account ID specified in the JWT")
 | 
						|
	}
 | 
						|
	account, err := strconv.ParseInt(claims.Subject, 10, 64)
 | 
						|
	if err != nil {
 | 
						|
		return JWTClaims{}, errors.New("invalid account ID specified in the JWT")
 | 
						|
	}
 | 
						|
	if account == 0 {
 | 
						|
		return JWTClaims{}, errors.New("no account ID specified in the JWT")
 | 
						|
	}
 | 
						|
 | 
						|
	if claims.V == "" {
 | 
						|
		return JWTClaims{}, errors.New("no API version specified in the JWT")
 | 
						|
	}
 | 
						|
 | 
						|
	if claims.V != version {
 | 
						|
		return JWTClaims{}, fmt.Errorf("unexpected API version in the JWT: %s", claims.V)
 | 
						|
	}
 | 
						|
 | 
						|
	if claims.I == "" {
 | 
						|
		return JWTClaims{}, errors.New("no identifiers specified in the JWT")
 | 
						|
	}
 | 
						|
 | 
						|
	return claims, nil
 | 
						|
}
 |