Skip to content

Commit

Permalink
I'm lost in the spaghetti
Browse files Browse the repository at this point in the history
  • Loading branch information
pgporada committed Jun 18, 2024
1 parent f1a0375 commit 28b4e34
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 278 deletions.
33 changes: 33 additions & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cmd

import (
"crypto"
"crypto/ed25519"
"crypto/tls"
"crypto/x509"
"errors"
Expand Down Expand Up @@ -553,3 +555,34 @@ type DNSProvider struct {
// 1 1 8153 0a4d4d4d.addr.dc1.consul.
SRVLookup ServiceDomain `validate:"required"`
}

type UnpauseConfig struct {
// Seed is a secret that should contain 256 bits (32 bytes) of random data
// used to derive an x/crypto/ed25519 keypair (e.g. the output of `openssl
// rand -hex 16`). In a multi-DC deployment this value should be the same
// across all boulder-wfe and sfe instances. This should not be accessed
// directly. Callers should instead use UnpausePublicKey() or
// UnpausePrivateKey() as needed.
Seed PasswordConfig `validate:"-"`
}

// GenerateKeyPair generates an Ed25519 key pair derived from a seed value in a
// configuration file.
func (u *UnpauseConfig) GenerateKeyPair() (crypto.PrivateKey, crypto.PublicKey, error) {
if u.Seed.PasswordFile != "" {
return nil, nil, errors.New("failed to load unpause seed")
}

seed, err := u.Seed.Pass()
if err != nil {
return nil, nil, err
}

if len(seed) != 32 {
return nil, nil, errors.New("unpause seed should be 32 hexadecimal characters e.g. the output of 'openssl rand -hex 16'")
}

privateKey := ed25519.NewKeyFromSeed([]byte(seed))

return privateKey, privateKey.Public(), nil
}
36 changes: 7 additions & 29 deletions cmd/sfe/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package notmain
import (
"bytes"
"context"
"crypto"
"flag"
"fmt"
"log"
Expand Down Expand Up @@ -44,9 +45,6 @@ type Config struct {
// shutting down any listening servers.
ShutdownStopTimeout config.Duration

// AllowOrigins is for setting CORS on OPTIONS requests
AllowOrigins []string

ServerCertificatePath string `validate:"required_with=TLSListenAddress"`
ServerKeyPath string `validate:"required_with=TLSListenAddress"`

Expand All @@ -55,40 +53,21 @@ type Config struct {
RAService *cmd.GRPCClientConfig
SAService *cmd.GRPCClientConfig

Unpause struct {
// Seed is a secret that should contain 256 bits (32 bytes) of
// random data used to derive an x/crypto/ed25519 keypair (e.g. the
// output of `openssl rand -hex 16`). In a multi-DC deployment this
// value should be the same across all boulder-wfe and sfe
// instances.
Seed cmd.PasswordConfig `validate:"-"`
}

Features features.Config
}

Unpause cmd.UnpauseConfig

Syslog cmd.SyslogConfig
OpenTelemetry cmd.OpenTelemetryConfig

// OpenTelemetryHTTPConfig configures tracing on incoming HTTP requests
OpenTelemetryHTTPConfig cmd.OpenTelemetryHTTPConfig
}

func setupSFE(c Config, scope prometheus.Registerer, clk clock.Clock) (rapb.RegistrationAuthorityClient, sapb.StorageAuthorityReadOnlyClient, string) {
var unpauseSeed string
if c.SFE.Unpause.Seed.PasswordFile != "" {
var err error
unpauseSeed, err = c.SFE.Unpause.Seed.Pass()
cmd.FailOnError(err, "Failed to load unpauseKey")
if unpauseSeed == "" {
cmd.Fail("unpauseKey must not be empty")
}
// The seed is used to generate an x/crypto/ed25519 keypair which
// requires a SeedSize of 32 bytes or the generator will panic.
if len(unpauseSeed) != 32 {
cmd.Fail("unpauseSeed should be 32 hexadecimal characters e.g. the output of 'openssl rand -hex 16'")
}
}
func setupSFE(c Config, scope prometheus.Registerer, clk clock.Clock) (rapb.RegistrationAuthorityClient, sapb.StorageAuthorityReadOnlyClient, crypto.PublicKey) {
_, unpausePubKey, err := c.Unpause.GenerateKeyPair()
cmd.FailOnError(err, "Generating unpause public key")

tlsConfig, err := c.SFE.TLS.Load(scope)
cmd.FailOnError(err, "TLS config")
Expand All @@ -101,7 +80,7 @@ func setupSFE(c Config, scope prometheus.Registerer, clk clock.Clock) (rapb.Regi
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
sac := sapb.NewStorageAuthorityReadOnlyClient(saConn)

return rac, sac, unpauseSeed
return rac, sac, unpausePubKey
}

type errorWriter struct {
Expand Down Expand Up @@ -167,7 +146,6 @@ func main() {
unpauseSeed,
)
cmd.FailOnError(err, "Unable to create SFE")
sfei.AllowOrigins = c.SFE.AllowOrigins

logger.Infof("Server running, listening on %s....", c.SFE.ListenAddress)
handler := sfei.Handler(stats, c.OpenTelemetryHTTPConfig.Options()...)
Expand Down
150 changes: 110 additions & 40 deletions pause/pause.go
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
}
Loading

0 comments on commit 28b4e34

Please sign in to comment.