github.com/letsencrypt/boulder@v0.20251208.0/unpause/unpause.go (about)

     1  package unpause
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/go-jose/go-jose/v4"
    11  	"github.com/go-jose/go-jose/v4/jwt"
    12  	"github.com/jmhodges/clock"
    13  
    14  	"github.com/letsencrypt/boulder/cmd"
    15  )
    16  
    17  const (
    18  	// API
    19  
    20  	// Changing this value will invalidate all existing JWTs.
    21  	APIVersion = "v1"
    22  	APIPrefix  = "/sfe/" + APIVersion
    23  	GetForm    = APIPrefix + "/unpause"
    24  
    25  	// BatchSize is the maximum number of identifiers that the SA will unpause
    26  	// in a single batch.
    27  	BatchSize = 10000
    28  
    29  	// MaxBatches is the maximum number of batches that the SA will unpause in a
    30  	// single request.
    31  	MaxBatches = 5
    32  
    33  	// RequestLimit is the maximum number of identifiers that the SA will
    34  	// unpause in a single request. This is used by the SFE to infer whether
    35  	// there are more identifiers to unpause.
    36  	RequestLimit = BatchSize * MaxBatches
    37  
    38  	// JWT
    39  	defaultIssuer   = "WFE"
    40  	defaultAudience = "SFE Unpause"
    41  )
    42  
    43  // JWTSigner is a type alias for jose.Signer. To create a JWTSigner instance,
    44  // use the NewJWTSigner function provided in this package.
    45  type JWTSigner = jose.Signer
    46  
    47  // NewJWTSigner loads the HMAC key from the provided configuration and returns a
    48  // new JWT signer.
    49  func NewJWTSigner(hmacKey cmd.HMACKeyConfig) (JWTSigner, error) {
    50  	key, err := hmacKey.Load()
    51  	if err != nil {
    52  		return nil, err
    53  	}
    54  	return jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key}, nil)
    55  }
    56  
    57  // JWTClaims represents the claims of a JWT token issued by the WFE for
    58  // redemption by the SFE. The following claims required for unpausing:
    59  //   - Subject: the account ID of the Subscriber
    60  //   - V: the API version this JWT was created for
    61  //   - I: a set of ACME identifier values. Identifier types are omitted
    62  //     since DNS and IP string representations do not overlap.
    63  type JWTClaims struct {
    64  	jwt.Claims
    65  
    66  	// V is the API version this JWT was created for.
    67  	V string `json:"version"`
    68  
    69  	// I is set of comma separated ACME identifiers.
    70  	I string `json:"identifiers"`
    71  }
    72  
    73  // GenerateJWT generates a serialized unpause JWT with the provided claims.
    74  func GenerateJWT(signer JWTSigner, regID int64, idents []string, lifetime time.Duration, clk clock.Clock) (string, error) {
    75  	claims := JWTClaims{
    76  		Claims: jwt.Claims{
    77  			Issuer:   defaultIssuer,
    78  			Subject:  fmt.Sprintf("%d", regID),
    79  			Audience: jwt.Audience{defaultAudience},
    80  			// IssuedAt is necessary for metrics.
    81  			IssuedAt: jwt.NewNumericDate(clk.Now()),
    82  			Expiry:   jwt.NewNumericDate(clk.Now().Add(lifetime)),
    83  		},
    84  		V: APIVersion,
    85  		I: strings.Join(idents, ","),
    86  	}
    87  
    88  	serialized, err := jwt.Signed(signer).Claims(&claims).Serialize()
    89  	if err != nil {
    90  		return "", fmt.Errorf("serializing JWT: %s", err)
    91  	}
    92  
    93  	return serialized, nil
    94  }
    95  
    96  // ErrMalformedJWT is returned when the JWT is malformed.
    97  var ErrMalformedJWT = errors.New("malformed JWT")
    98  
    99  // RedeemJWT deserializes an unpause JWT and returns the validated claims. The
   100  // key is used to validate the signature of the JWT. The version is the expected
   101  // API version of the JWT. This function validates that the JWT is:
   102  //   - well-formed,
   103  //   - valid for the current time (+/- 1 minute leeway),
   104  //   - issued by the WFE,
   105  //   - intended for the SFE,
   106  //   - contains an Account ID as the 'Subject',
   107  //   - subject can be parsed as a 64-bit integer,
   108  //   - contains a set of paused identifiers as 'Identifiers', and
   109  //   - contains the API the expected version as 'Version'.
   110  //
   111  // If the JWT is malformed or invalid in any way, ErrMalformedJWT is returned.
   112  func RedeemJWT(token string, key []byte, version string, clk clock.Clock) (JWTClaims, error) {
   113  	parsedToken, err := jwt.ParseSigned(token, []jose.SignatureAlgorithm{jose.HS256})
   114  	if err != nil {
   115  		return JWTClaims{}, errors.Join(ErrMalformedJWT, err)
   116  	}
   117  
   118  	claims := JWTClaims{}
   119  	err = parsedToken.Claims(key, &claims)
   120  	if err != nil {
   121  		return JWTClaims{}, errors.Join(ErrMalformedJWT, err)
   122  	}
   123  
   124  	err = claims.Validate(jwt.Expected{
   125  		Issuer:      defaultIssuer,
   126  		AnyAudience: jwt.Audience{defaultAudience},
   127  
   128  		// By default, the go-jose library validates the NotBefore and Expiry
   129  		// fields with a default leeway of 1 minute.
   130  		Time: clk.Now(),
   131  	})
   132  	if err != nil {
   133  		return JWTClaims{}, fmt.Errorf("validating JWT: %w", err)
   134  	}
   135  
   136  	if len(claims.Subject) == 0 {
   137  		return JWTClaims{}, errors.New("no account ID specified in the JWT")
   138  	}
   139  	account, err := strconv.ParseInt(claims.Subject, 10, 64)
   140  	if err != nil {
   141  		return JWTClaims{}, errors.New("invalid account ID specified in the JWT")
   142  	}
   143  	if account == 0 {
   144  		return JWTClaims{}, errors.New("no account ID specified in the JWT")
   145  	}
   146  
   147  	if claims.V == "" {
   148  		return JWTClaims{}, errors.New("no API version specified in the JWT")
   149  	}
   150  
   151  	if claims.V != version {
   152  		return JWTClaims{}, fmt.Errorf("unexpected API version in the JWT: %s", claims.V)
   153  	}
   154  
   155  	if claims.I == "" {
   156  		return JWTClaims{}, errors.New("no identifiers specified in the JWT")
   157  	}
   158  
   159  	return claims, nil
   160  }