-
-
Notifications
You must be signed in to change notification settings - Fork 606
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
240 additions
and
278 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,71 +1,141 @@ | ||
package pause | ||
|
||
import ( | ||
"crypto/ed25519" | ||
"crypto" | ||
"errors" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/go-jose/go-jose/v4" | ||
"github.com/go-jose/go-jose/v4/jwt" | ||
|
||
"github.com/letsencrypt/boulder/core" | ||
) | ||
|
||
// UnpauseJWT is generated by a WFE and is used by the SFE to create an unpause | ||
// request for the account claimed within the JWT. | ||
type UnpauseJWT string | ||
|
||
// CreateJWTForAccount is a standin for a WFE method that returns an UnpauseJWT or | ||
// an error. The JWT contains a set of claims which should be validated by the | ||
// caller. | ||
func CreateJWTForAccount(notBefore time.Time, issuedAt time.Time, expiresAt time.Time, seed []byte, regID int64, apiVersion string) (UnpauseJWT, error) { | ||
// A seed must be at least 16 bytes (32 elements) or go-jose will panic. | ||
if len(seed) != 32 { | ||
return "", errors.New("seed length invalid") | ||
} | ||
// UnpauseClaims is used to store both RFC 7519 claims and a custom claim. | ||
type UnpauseClaims struct { | ||
jwt.Claims | ||
|
||
// Only the private key is needed for signing. The derives its own public | ||
// key from the shared seed for validating the signature. | ||
privateKey := ed25519.NewKeyFromSeed(seed) | ||
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.EdDSA, Key: privateKey}, (&jose.SignerOptions{}).WithType("JWT")) | ||
if err != nil { | ||
return "", fmt.Errorf("making signer: %s", err) | ||
} | ||
// Version is a custom claim used to mass invalidate existing JWTs by | ||
// changing the API version via unpausePath. | ||
Version string `json:"apiVersion,omitempty"` | ||
} | ||
|
||
// Ensure that we test an empty subject | ||
var subject string | ||
if regID == 0 { | ||
subject = "" | ||
} else { | ||
subject = fmt.Sprint(regID) | ||
} | ||
// ExpectedClaims are used to verify a set of received UnpauseClaims matches our | ||
// expectations. | ||
type ExpectedClaims struct { | ||
issuer string | ||
audience []string | ||
time time.Time | ||
} | ||
|
||
// GetIssuer returns the issuer field of an ExpectedClaims | ||
func (e ExpectedClaims) GetIssuer() string { | ||
return e.issuer | ||
} | ||
|
||
// Ensure that we test receiving an empty API version string while | ||
// defaulting the rest to match SFE unpausePath. | ||
if apiVersion == "magicEmptyString" { | ||
apiVersion = "" | ||
} else if apiVersion == "" { | ||
apiVersion = "v1" | ||
// GetAudience returns the audience field of an ExpectedClaims as a | ||
// jwt.Audience. | ||
func (e ExpectedClaims) GetAudience() jwt.Audience { | ||
return jwt.Audience(e.audience) | ||
} | ||
|
||
// GetTime returns the time field of an ExpectedClaims | ||
func (e ExpectedClaims) GetTime() time.Time { | ||
return e.time | ||
} | ||
|
||
type UnpauseJWT struct { | ||
// jwt is purposefully not exported so that it can only be produced in the | ||
// pause package. | ||
jwt string | ||
} | ||
|
||
// GetJWT returns the unexported JWT value from an UnpauseJWT. | ||
func (u UnpauseJWT) GetJWT() string { | ||
return u.jwt | ||
} | ||
|
||
// NewUnpauseJWTForAccount creates and returns a signed UnpauseJWT or an error. | ||
// The UnpauseJWT contains a set of RFC 7519 claims and custom claims to be | ||
// validated by a receiver. | ||
func NewUnpauseJWTForAccount(signer jose.Signer, notBefore time.Time, issuedAt time.Time, expiresAt time.Time, regID int64, apiVersion string) (UnpauseJWT, error) { | ||
if core.IsAnyNilOrZero(regID, apiVersion) { | ||
return UnpauseJWT{}, fmt.Errorf("invalid parameters") | ||
} | ||
|
||
// The Version is used to mass-invalidate JWTs. To perform that operation, | ||
// an operator can change the API version in the SFE unpausePath variable | ||
// which will cause incoming JWTs to fail claim validation. | ||
customClaims := struct { | ||
Version string `json:"apiVersion,omitempty"` | ||
}{ | ||
apiVersion, | ||
} | ||
|
||
wfeClaims := jwt.Claims{ | ||
Issuer: "WFE", | ||
Subject: subject, | ||
Audience: jwt.Audience{"SFE Unpause"}, | ||
rfcClaims := jwt.Claims{ | ||
Issuer: "WFE", | ||
Subject: fmt.Sprint(regID), | ||
Audience: jwt.Audience{"SFE Unpause"}, | ||
// We explicitly set the time for testing purposes. | ||
NotBefore: jwt.NewNumericDate(notBefore), | ||
IssuedAt: jwt.NewNumericDate(issuedAt), | ||
Expiry: jwt.NewNumericDate(expiresAt), | ||
} | ||
|
||
signedJWT, err := jwt.Signed(signer).Claims(&wfeClaims).Claims(&customClaims).Serialize() | ||
signedJWT, err := jwt.Signed(signer).Claims(&rfcClaims).Claims(&customClaims).Serialize() | ||
if err != nil { | ||
return "", fmt.Errorf("signing JWT: %s", err) | ||
return UnpauseJWT{}, fmt.Errorf("signing JWT: %s", err) | ||
} | ||
|
||
return UnpauseJWT{jwt: signedJWT}, nil | ||
} | ||
|
||
// ValidateJWTForAccount takes a public key corresponding to the private key | ||
// which signed the incoming JWT, a set of expected claims to verify basic facts | ||
// about the incoming JWT, and a versioned API string. An error is returned if | ||
// any step in the validation process fails. | ||
func (u UnpauseJWT) ValidateJWTForAccount(claimValidationKey crypto.PublicKey, ec ExpectedClaims, apiVersion string) error { | ||
token, err := jwt.ParseSigned(u.jwt, []jose.SignatureAlgorithm{jose.EdDSA}) | ||
if err != nil { | ||
return fmt.Errorf("parsing JWT: %s", err) | ||
} | ||
|
||
incomingClaims := UnpauseClaims{} | ||
err = token.Claims(claimValidationKey, &incomingClaims) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
/* | ||
expectedClaims := jwt.Expected{ | ||
Issuer: "WFE", | ||
AnyAudience: jwt.Audience{"SFE Unpause"}, | ||
// Time is passed into the jwt package for tests to manipulate time. | ||
Time: sfe.clk.Now(), | ||
} | ||
*/ | ||
|
||
err = incomingClaims.Validate(jwt.Expected{ | ||
Issuer: ec.GetIssuer(), | ||
AnyAudience: ec.GetAudience(), | ||
Time: ec.GetTime(), | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if len(incomingClaims.Subject) == 0 { | ||
return errors.New("Account ID required for account unpausing") | ||
} | ||
|
||
if incomingClaims.Version == "" { | ||
return errors.New("Incoming JWT was created with no API version") | ||
} | ||
|
||
if incomingClaims.Version != apiVersion { | ||
return fmt.Errorf("JWT created for unpause API version %s provided to incompatible API version %s", incomingClaims.Version, apiVersion) | ||
} | ||
|
||
return unpauseJWT(signedJWT), nil | ||
return nil | ||
} |
Oops, something went wrong.