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 }