1680 lines
		
	
	
		
			55 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			1680 lines
		
	
	
		
			55 KiB
		
	
	
	
		
			Go
		
	
	
	
package wfe2
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"crypto"
 | 
						|
	"crypto/dsa"
 | 
						|
	"crypto/ecdsa"
 | 
						|
	"crypto/elliptic"
 | 
						|
	"crypto/rsa"
 | 
						|
	"fmt"
 | 
						|
	"net/http"
 | 
						|
	"strings"
 | 
						|
	"testing"
 | 
						|
 | 
						|
	"github.com/letsencrypt/boulder/core"
 | 
						|
	corepb "github.com/letsencrypt/boulder/core/proto"
 | 
						|
	"github.com/letsencrypt/boulder/goodkey"
 | 
						|
	bgrpc "github.com/letsencrypt/boulder/grpc"
 | 
						|
	"github.com/letsencrypt/boulder/mocks"
 | 
						|
	"github.com/letsencrypt/boulder/probs"
 | 
						|
	sapb "github.com/letsencrypt/boulder/sa/proto"
 | 
						|
	"github.com/letsencrypt/boulder/test"
 | 
						|
	"github.com/letsencrypt/boulder/web"
 | 
						|
	"github.com/prometheus/client_golang/prometheus"
 | 
						|
 | 
						|
	"google.golang.org/grpc"
 | 
						|
	"gopkg.in/go-jose/go-jose.v2"
 | 
						|
)
 | 
						|
 | 
						|
// sigAlgForKey uses `signatureAlgorithmForKey` but fails immediately using the
 | 
						|
// testing object if the sig alg is unknown.
 | 
						|
func sigAlgForKey(t *testing.T, key interface{}) jose.SignatureAlgorithm {
 | 
						|
	var sigAlg jose.SignatureAlgorithm
 | 
						|
	var err error
 | 
						|
	// Gracefully handle the case where a non-pointer public key is given where
 | 
						|
	// sigAlgorithmForKey always wants a pointer. It may be tempting to try and do
 | 
						|
	// `sigAlgorithmForKey(&jose.JSONWebKey{Key: &key})` without a type switch but this produces
 | 
						|
	// `*interface {}` and not the desired `*rsa.PublicKey` or `*ecdsa.PublicKey`.
 | 
						|
	switch k := key.(type) {
 | 
						|
	case rsa.PublicKey:
 | 
						|
		sigAlg, err = sigAlgorithmForKey(&jose.JSONWebKey{Key: &k})
 | 
						|
	case ecdsa.PublicKey:
 | 
						|
		sigAlg, err = sigAlgorithmForKey(&jose.JSONWebKey{Key: &k})
 | 
						|
	default:
 | 
						|
		sigAlg, err = sigAlgorithmForKey(&jose.JSONWebKey{Key: k})
 | 
						|
	}
 | 
						|
	test.Assert(t, err == nil, fmt.Sprintf("Error getting signature algorithm for key %#v", key))
 | 
						|
	return sigAlg
 | 
						|
}
 | 
						|
 | 
						|
// keyAlgForKey returns a JWK key algorithm based on the provided private key.
 | 
						|
// Only ECDSA and RSA private keys are supported.
 | 
						|
func keyAlgForKey(t *testing.T, key interface{}) string {
 | 
						|
	switch key.(type) {
 | 
						|
	case *rsa.PrivateKey, rsa.PrivateKey:
 | 
						|
		return "RSA"
 | 
						|
	case *ecdsa.PrivateKey, ecdsa.PrivateKey:
 | 
						|
		return "ECDSA"
 | 
						|
	}
 | 
						|
	t.Fatalf("Can't figure out keyAlgForKey: %#v", key)
 | 
						|
	return ""
 | 
						|
}
 | 
						|
 | 
						|
// pubKeyForKey returns the public key of an RSA/ECDSA private key provided as
 | 
						|
// argument.
 | 
						|
func pubKeyForKey(t *testing.T, privKey interface{}) interface{} {
 | 
						|
	switch k := privKey.(type) {
 | 
						|
	case *rsa.PrivateKey:
 | 
						|
		return k.PublicKey
 | 
						|
	case *ecdsa.PrivateKey:
 | 
						|
		return k.PublicKey
 | 
						|
	}
 | 
						|
	t.Fatalf("Unable to get public key for private key %#v", privKey)
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// requestSigner offers methods to sign requests that will be accepted by a
 | 
						|
// specific WFE in unittests. It is only valid for the lifetime of a single
 | 
						|
// unittest.
 | 
						|
type requestSigner struct {
 | 
						|
	t            *testing.T
 | 
						|
	nonceService jose.NonceSource
 | 
						|
}
 | 
						|
 | 
						|
// embeddedJWK creates a JWS for a given request body with an embedded JWK
 | 
						|
// corresponding to the private key provided. The URL and nonce extra headers
 | 
						|
// are set based on the additional arguments. A computed JWS, the corresponding
 | 
						|
// embedded JWK and the JWS in serialized string form are returned.
 | 
						|
func (rs requestSigner) embeddedJWK(
 | 
						|
	privateKey interface{},
 | 
						|
	url string,
 | 
						|
	req string) (*jose.JSONWebSignature, *jose.JSONWebKey, string) {
 | 
						|
	// if no key is provided default to test1KeyPrivatePEM
 | 
						|
	var publicKey interface{}
 | 
						|
	if privateKey == nil {
 | 
						|
		signer := loadKey(rs.t, []byte(test1KeyPrivatePEM))
 | 
						|
		privateKey = signer
 | 
						|
		publicKey = signer.Public()
 | 
						|
	} else {
 | 
						|
		publicKey = pubKeyForKey(rs.t, privateKey)
 | 
						|
	}
 | 
						|
 | 
						|
	signerKey := jose.SigningKey{
 | 
						|
		Key:       privateKey,
 | 
						|
		Algorithm: sigAlgForKey(rs.t, publicKey),
 | 
						|
	}
 | 
						|
 | 
						|
	opts := &jose.SignerOptions{
 | 
						|
		NonceSource: rs.nonceService,
 | 
						|
		EmbedJWK:    true,
 | 
						|
	}
 | 
						|
	if url != "" {
 | 
						|
		opts.ExtraHeaders = map[jose.HeaderKey]interface{}{
 | 
						|
			"url": url,
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	signer, err := jose.NewSigner(signerKey, opts)
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to make signer")
 | 
						|
 | 
						|
	jws, err := signer.Sign([]byte(req))
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to sign req")
 | 
						|
 | 
						|
	body := jws.FullSerialize()
 | 
						|
	parsedJWS, err := jose.ParseSigned(body)
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to parse generated JWS")
 | 
						|
 | 
						|
	return parsedJWS, parsedJWS.Signatures[0].Header.JSONWebKey, body
 | 
						|
}
 | 
						|
 | 
						|
// signRequestKeyID creates a JWS for a given request body with key ID specified
 | 
						|
// based on the ID number provided. The URL and nonce extra headers
 | 
						|
// are set based on the additional arguments. A computed JWS, the corresponding
 | 
						|
// embedded JWK and the JWS in serialized string form are returned.
 | 
						|
func (rs requestSigner) byKeyID(
 | 
						|
	keyID int64,
 | 
						|
	privateKey interface{},
 | 
						|
	url string,
 | 
						|
	req string) (*jose.JSONWebSignature, *jose.JSONWebKey, string) {
 | 
						|
	// if no key is provided default to test1KeyPrivatePEM
 | 
						|
	if privateKey == nil {
 | 
						|
		privateKey = loadKey(rs.t, []byte(test1KeyPrivatePEM))
 | 
						|
	}
 | 
						|
 | 
						|
	jwk := &jose.JSONWebKey{
 | 
						|
		Key:       privateKey,
 | 
						|
		Algorithm: keyAlgForKey(rs.t, privateKey),
 | 
						|
		KeyID:     fmt.Sprintf("http://localhost/acme/acct/%d", keyID),
 | 
						|
	}
 | 
						|
 | 
						|
	signerKey := jose.SigningKey{
 | 
						|
		Key:       jwk,
 | 
						|
		Algorithm: jose.RS256,
 | 
						|
	}
 | 
						|
 | 
						|
	opts := &jose.SignerOptions{
 | 
						|
		NonceSource: rs.nonceService,
 | 
						|
		ExtraHeaders: map[jose.HeaderKey]interface{}{
 | 
						|
			"url": url,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	signer, err := jose.NewSigner(signerKey, opts)
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to make signer")
 | 
						|
	jws, err := signer.Sign([]byte(req))
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to sign req")
 | 
						|
 | 
						|
	body := jws.FullSerialize()
 | 
						|
	parsedJWS, err := jose.ParseSigned(body)
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to parse generated JWS")
 | 
						|
 | 
						|
	return parsedJWS, jwk, body
 | 
						|
}
 | 
						|
 | 
						|
// missingNonce returns an otherwise well-signed request that is missing its
 | 
						|
// nonce.
 | 
						|
func (rs requestSigner) missingNonce() *jose.JSONWebSignature {
 | 
						|
	privateKey := loadKey(rs.t, []byte(test1KeyPrivatePEM))
 | 
						|
	jwk := &jose.JSONWebKey{
 | 
						|
		Key:       privateKey,
 | 
						|
		Algorithm: keyAlgForKey(rs.t, privateKey),
 | 
						|
		KeyID:     "http://localhost/acme/acct/1",
 | 
						|
	}
 | 
						|
	signerKey := jose.SigningKey{
 | 
						|
		Key:       jwk,
 | 
						|
		Algorithm: jose.RS256,
 | 
						|
	}
 | 
						|
 | 
						|
	opts := &jose.SignerOptions{
 | 
						|
		ExtraHeaders: map[jose.HeaderKey]interface{}{
 | 
						|
			"url": "https://example.com/acme/foo",
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	signer, err := jose.NewSigner(signerKey, opts)
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to make signer")
 | 
						|
	jws, err := signer.Sign([]byte(""))
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to sign req")
 | 
						|
 | 
						|
	return jws
 | 
						|
}
 | 
						|
 | 
						|
// invalidNonce returns an otherwise well-signed request with an invalid nonce.
 | 
						|
func (rs requestSigner) invalidNonce() *jose.JSONWebSignature {
 | 
						|
	privateKey := loadKey(rs.t, []byte(test1KeyPrivatePEM))
 | 
						|
	jwk := &jose.JSONWebKey{
 | 
						|
		Key:       privateKey,
 | 
						|
		Algorithm: keyAlgForKey(rs.t, privateKey),
 | 
						|
		KeyID:     "http://localhost/acme/acct/1",
 | 
						|
	}
 | 
						|
	signerKey := jose.SigningKey{
 | 
						|
		Key:       jwk,
 | 
						|
		Algorithm: jose.RS256,
 | 
						|
	}
 | 
						|
 | 
						|
	opts := &jose.SignerOptions{
 | 
						|
		NonceSource: badNonceProvider{},
 | 
						|
		ExtraHeaders: map[jose.HeaderKey]interface{}{
 | 
						|
			"url": "https://example.com/acme/foo",
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	signer, err := jose.NewSigner(signerKey, opts)
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to make signer")
 | 
						|
	jws, err := signer.Sign([]byte(""))
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to sign req")
 | 
						|
 | 
						|
	body := jws.FullSerialize()
 | 
						|
	parsedJWS, err := jose.ParseSigned(body)
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to parse generated JWS")
 | 
						|
 | 
						|
	return parsedJWS
 | 
						|
}
 | 
						|
 | 
						|
func TestRejectsNone(t *testing.T) {
 | 
						|
	noneJWSBody := `
 | 
						|
		{
 | 
						|
			"header": {
 | 
						|
				"alg": "none",
 | 
						|
				"jwk": {
 | 
						|
					"kty": "RSA",
 | 
						|
					"n": "vrjT",
 | 
						|
					"e": "AQAB"
 | 
						|
				}
 | 
						|
			},
 | 
						|
			"payload": "aGkK",
 | 
						|
  		"signature": "ghTIjrhiRl2pQ09vAkUUBbF5KziJdhzOTB-okM9SPRzU8Hyj0W1H5JA1Zoc-A-LuJGNAtYYHWqMw1SeZbT0l9FHcbMPeWDaJNkHS9jz5_g_Oyol8vcrWur2GDtB2Jgw6APtZKrbuGATbrF7g41Wijk6Kk9GXDoCnlfOQOhHhsrFFcWlCPLG-03TtKD6EBBoVBhmlp8DRLs7YguWRZ6jWNaEX-1WiRntBmhLqoqQFtvZxCBw_PRuaRw_RZBd1x2_BNYqEdOmVNC43UHMSJg3y_3yrPo905ur09aUTscf-C_m4Sa4M0FuDKn3bQ_pFrtz-aCCq6rcTIyxYpDqNvHMT2Q"
 | 
						|
		}
 | 
						|
	`
 | 
						|
	noneJWS, err := jose.ParseSigned(noneJWSBody)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal("Unable to parse noneJWS")
 | 
						|
	}
 | 
						|
	noneJWK := noneJWS.Signatures[0].Header.JSONWebKey
 | 
						|
 | 
						|
	err = checkAlgorithm(noneJWK, noneJWS.Signatures[0].Header)
 | 
						|
	if err == nil {
 | 
						|
		t.Fatalf("checkAlgorithm did not reject JWS with alg: 'none'")
 | 
						|
	}
 | 
						|
	if err.Error() != "JWS signature header contains unsupported algorithm \"none\", expected one of RS256, ES256, ES384 or ES512" {
 | 
						|
		t.Fatalf("checkAlgorithm rejected JWS with alg: 'none', but for wrong reason: %#v", err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestRejectsHS256(t *testing.T) {
 | 
						|
	hs256JWSBody := `
 | 
						|
		{
 | 
						|
			"header": {
 | 
						|
				"alg": "HS256",
 | 
						|
				"jwk": {
 | 
						|
					"kty": "RSA",
 | 
						|
					"n": "vrjT",
 | 
						|
					"e": "AQAB"
 | 
						|
				}
 | 
						|
			},
 | 
						|
			"payload": "aGkK",
 | 
						|
  		"signature": "ghTIjrhiRl2pQ09vAkUUBbF5KziJdhzOTB-okM9SPRzU8Hyj0W1H5JA1Zoc-A-LuJGNAtYYHWqMw1SeZbT0l9FHcbMPeWDaJNkHS9jz5_g_Oyol8vcrWur2GDtB2Jgw6APtZKrbuGATbrF7g41Wijk6Kk9GXDoCnlfOQOhHhsrFFcWlCPLG-03TtKD6EBBoVBhmlp8DRLs7YguWRZ6jWNaEX-1WiRntBmhLqoqQFtvZxCBw_PRuaRw_RZBd1x2_BNYqEdOmVNC43UHMSJg3y_3yrPo905ur09aUTscf-C_m4Sa4M0FuDKn3bQ_pFrtz-aCCq6rcTIyxYpDqNvHMT2Q"
 | 
						|
		}
 | 
						|
	`
 | 
						|
 | 
						|
	hs256JWS, err := jose.ParseSigned(hs256JWSBody)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal("Unable to parse hs256JWSBody")
 | 
						|
	}
 | 
						|
	hs256JWK := hs256JWS.Signatures[0].Header.JSONWebKey
 | 
						|
 | 
						|
	err = checkAlgorithm(hs256JWK, hs256JWS.Signatures[0].Header)
 | 
						|
	if err == nil {
 | 
						|
		t.Fatalf("checkAlgorithm did not reject JWS with alg: 'HS256'")
 | 
						|
	}
 | 
						|
	expected := "JWS signature header contains unsupported algorithm \"HS256\", expected one of RS256, ES256, ES384 or ES512"
 | 
						|
	if err.Error() != expected {
 | 
						|
		t.Fatalf("checkAlgorithm rejected JWS with alg: 'none', but for wrong reason: got %q, wanted %q", err.Error(), expected)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestCheckAlgorithm(t *testing.T) {
 | 
						|
	testCases := []struct {
 | 
						|
		key         jose.JSONWebKey
 | 
						|
		jws         jose.JSONWebSignature
 | 
						|
		expectedErr string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			jose.JSONWebKey{},
 | 
						|
			jose.JSONWebSignature{
 | 
						|
				Signatures: []jose.Signature{
 | 
						|
					{
 | 
						|
						Header: jose.Header{
 | 
						|
							Algorithm: "RS256",
 | 
						|
						},
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			"JWK contains unsupported key type (expected RSA, or ECDSA P-256, P-384, or P-521)",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			jose.JSONWebKey{
 | 
						|
				Algorithm: "HS256",
 | 
						|
				Key:       &rsa.PublicKey{},
 | 
						|
			},
 | 
						|
			jose.JSONWebSignature{
 | 
						|
				Signatures: []jose.Signature{
 | 
						|
					{
 | 
						|
						Header: jose.Header{
 | 
						|
							Algorithm: "HS256",
 | 
						|
						},
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			"JWS signature header contains unsupported algorithm \"HS256\", expected one of RS256, ES256, ES384 or ES512",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			jose.JSONWebKey{
 | 
						|
				Algorithm: "ES256",
 | 
						|
				Key:       &dsa.PublicKey{},
 | 
						|
			},
 | 
						|
			jose.JSONWebSignature{
 | 
						|
				Signatures: []jose.Signature{
 | 
						|
					{
 | 
						|
						Header: jose.Header{
 | 
						|
							Algorithm: "ES512",
 | 
						|
						},
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			"JWK contains unsupported key type (expected RSA, or ECDSA P-256, P-384, or P-521)",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			jose.JSONWebKey{
 | 
						|
				Algorithm: "RS256",
 | 
						|
				Key:       &rsa.PublicKey{},
 | 
						|
			},
 | 
						|
			jose.JSONWebSignature{
 | 
						|
				Signatures: []jose.Signature{
 | 
						|
					{
 | 
						|
						Header: jose.Header{
 | 
						|
							Algorithm: "ES512",
 | 
						|
						},
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			"JWS signature header algorithm \"ES512\" does not match expected algorithm \"RS256\" for JWK",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			jose.JSONWebKey{
 | 
						|
				Algorithm: "HS256",
 | 
						|
				Key:       &rsa.PublicKey{},
 | 
						|
			},
 | 
						|
			jose.JSONWebSignature{
 | 
						|
				Signatures: []jose.Signature{
 | 
						|
					{
 | 
						|
						Header: jose.Header{
 | 
						|
							Algorithm: "RS256",
 | 
						|
						},
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			"JWK key header algorithm \"HS256\" does not match expected algorithm \"RS256\" for JWK",
 | 
						|
		},
 | 
						|
	}
 | 
						|
	for i, tc := range testCases {
 | 
						|
		err := checkAlgorithm(&tc.key, tc.jws.Signatures[0].Header)
 | 
						|
		if tc.expectedErr != "" && err.Error() != tc.expectedErr {
 | 
						|
			t.Errorf("TestCheckAlgorithm %d: Expected %q, got %q", i, tc.expectedErr, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestCheckAlgorithmSuccess(t *testing.T) {
 | 
						|
	jwsRS256 := &jose.JSONWebSignature{
 | 
						|
		Signatures: []jose.Signature{
 | 
						|
			{
 | 
						|
				Header: jose.Header{
 | 
						|
					Algorithm: "RS256",
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
	goodJSONWebKeyRS256 := &jose.JSONWebKey{
 | 
						|
		Algorithm: "RS256",
 | 
						|
		Key:       &rsa.PublicKey{},
 | 
						|
	}
 | 
						|
	err := checkAlgorithm(goodJSONWebKeyRS256, jwsRS256.Signatures[0].Header)
 | 
						|
	test.AssertNotError(t, err, "RS256 key: Expected nil error")
 | 
						|
 | 
						|
	badJSONWebKeyRS256 := &jose.JSONWebKey{
 | 
						|
		Algorithm: "ObviouslyWrongButNotZeroValue",
 | 
						|
		Key:       &rsa.PublicKey{},
 | 
						|
	}
 | 
						|
	err = checkAlgorithm(badJSONWebKeyRS256, jwsRS256.Signatures[0].Header)
 | 
						|
	test.AssertError(t, err, "RS256 key: Expected nil error")
 | 
						|
	test.AssertContains(t, err.Error(), "JWK key header algorithm \"ObviouslyWrongButNotZeroValue\" does not match expected algorithm \"RS256\" for JWK")
 | 
						|
 | 
						|
	jwsES256 := &jose.JSONWebSignature{
 | 
						|
		Signatures: []jose.Signature{
 | 
						|
			{
 | 
						|
				Header: jose.Header{
 | 
						|
					Algorithm: "ES256",
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
	goodJSONWebKeyES256 := &jose.JSONWebKey{
 | 
						|
		Algorithm: "ES256",
 | 
						|
		Key: &ecdsa.PublicKey{
 | 
						|
			Curve: elliptic.P256(),
 | 
						|
		},
 | 
						|
	}
 | 
						|
	err = checkAlgorithm(goodJSONWebKeyES256, jwsES256.Signatures[0].Header)
 | 
						|
	test.AssertNotError(t, err, "ES256 key: Expected nil error")
 | 
						|
 | 
						|
	badJSONWebKeyES256 := &jose.JSONWebKey{
 | 
						|
		Algorithm: "ObviouslyWrongButNotZeroValue",
 | 
						|
		Key: &ecdsa.PublicKey{
 | 
						|
			Curve: elliptic.P256(),
 | 
						|
		},
 | 
						|
	}
 | 
						|
	err = checkAlgorithm(badJSONWebKeyES256, jwsES256.Signatures[0].Header)
 | 
						|
	test.AssertError(t, err, "ES256 key: Expected nil error")
 | 
						|
	test.AssertContains(t, err.Error(), "JWK key header algorithm \"ObviouslyWrongButNotZeroValue\" does not match expected algorithm \"ES256\" for JWK")
 | 
						|
}
 | 
						|
 | 
						|
func TestValidPOSTRequest(t *testing.T) {
 | 
						|
	wfe, _, _ := setupWFE(t)
 | 
						|
 | 
						|
	dummyContentLength := []string{"pretty long, idk, maybe a nibble or two?"}
 | 
						|
 | 
						|
	testCases := []struct {
 | 
						|
		Name               string
 | 
						|
		Headers            map[string][]string
 | 
						|
		Body               *string
 | 
						|
		HTTPStatus         int
 | 
						|
		ProblemDetail      string
 | 
						|
		ErrorStatType      string
 | 
						|
		EnforceContentType bool
 | 
						|
	}{
 | 
						|
		// POST requests without a Content-Length should produce a problem
 | 
						|
		{
 | 
						|
			Name:          "POST without a Content-Length header",
 | 
						|
			Headers:       nil,
 | 
						|
			HTTPStatus:    http.StatusLengthRequired,
 | 
						|
			ProblemDetail: "missing Content-Length header",
 | 
						|
			ErrorStatType: "ContentLengthRequired",
 | 
						|
		},
 | 
						|
		// POST requests with a Replay-Nonce header should produce a problem
 | 
						|
		{
 | 
						|
			Name: "POST with a Replay-Nonce HTTP header",
 | 
						|
			Headers: map[string][]string{
 | 
						|
				"Content-Length": dummyContentLength,
 | 
						|
				"Replay-Nonce":   {"ima-misplaced-nonce"},
 | 
						|
				"Content-Type":   {expectedJWSContentType},
 | 
						|
			},
 | 
						|
			HTTPStatus:    http.StatusBadRequest,
 | 
						|
			ProblemDetail: "HTTP requests should NOT contain Replay-Nonce header. Use JWS nonce field",
 | 
						|
			ErrorStatType: "ReplayNonceOutsideJWS",
 | 
						|
		},
 | 
						|
		// POST requests without a body should produce a problem
 | 
						|
		{
 | 
						|
			Name: "POST with an empty POST body",
 | 
						|
			Headers: map[string][]string{
 | 
						|
				"Content-Length": dummyContentLength,
 | 
						|
				"Content-Type":   {expectedJWSContentType},
 | 
						|
			},
 | 
						|
			HTTPStatus:    http.StatusBadRequest,
 | 
						|
			ProblemDetail: "No body on POST",
 | 
						|
			ErrorStatType: "NoPOSTBody",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name: "POST without a Content-Type header",
 | 
						|
			Headers: map[string][]string{
 | 
						|
				"Content-Length": dummyContentLength,
 | 
						|
			},
 | 
						|
			HTTPStatus: http.StatusUnsupportedMediaType,
 | 
						|
			ProblemDetail: fmt.Sprintf(
 | 
						|
				"No Content-Type header on POST. Content-Type must be %q",
 | 
						|
				expectedJWSContentType),
 | 
						|
			ErrorStatType:      "NoContentType",
 | 
						|
			EnforceContentType: true,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name: "POST with an invalid Content-Type header",
 | 
						|
			Headers: map[string][]string{
 | 
						|
				"Content-Length": dummyContentLength,
 | 
						|
				"Content-Type":   {"fresh.and.rare"},
 | 
						|
			},
 | 
						|
			HTTPStatus: http.StatusUnsupportedMediaType,
 | 
						|
			ProblemDetail: fmt.Sprintf(
 | 
						|
				"Invalid Content-Type header on POST. Content-Type must be %q",
 | 
						|
				expectedJWSContentType),
 | 
						|
			ErrorStatType:      "WrongContentType",
 | 
						|
			EnforceContentType: true,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testCases {
 | 
						|
		input := &http.Request{
 | 
						|
			Method: "POST",
 | 
						|
			URL:    mustParseURL("/"),
 | 
						|
			Header: tc.Headers,
 | 
						|
		}
 | 
						|
		t.Run(tc.Name, func(t *testing.T) {
 | 
						|
			prob := wfe.validPOSTRequest(input)
 | 
						|
			test.Assert(t, prob != nil, "No error returned for invalid POST")
 | 
						|
			test.AssertEquals(t, prob.Type, probs.MalformedProblem)
 | 
						|
			test.AssertEquals(t, prob.HTTPStatus, tc.HTTPStatus)
 | 
						|
			test.AssertEquals(t, prob.Detail, tc.ProblemDetail)
 | 
						|
			test.AssertMetricWithLabelsEquals(
 | 
						|
				t, wfe.stats.httpErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestEnforceJWSAuthType(t *testing.T) {
 | 
						|
	wfe, _, signer := setupWFE(t)
 | 
						|
 | 
						|
	testKeyIDJWS, _, _ := signer.byKeyID(1, nil, "", "")
 | 
						|
	testEmbeddedJWS, _, _ := signer.embeddedJWK(nil, "", "")
 | 
						|
 | 
						|
	// A hand crafted JWS that has both a Key ID and an embedded JWK
 | 
						|
	conflictJWSBody := `
 | 
						|
{
 | 
						|
  "header": {
 | 
						|
    "alg": "RS256", 
 | 
						|
    "jwk": {
 | 
						|
      "e": "AQAB", 
 | 
						|
      "kty": "RSA", 
 | 
						|
      "n": "ppbqGaMFnnq9TeMUryR6WW4Lr5WMgp46KlBXZkNaGDNQoifWt6LheeR5j9MgYkIFU7Z8Jw5-bpJzuBeEVwb-yHGh4Umwo_qKtvAJd44iLjBmhBSxq-OSe6P5hX1LGCByEZlYCyoy98zOtio8VK_XyS5VoOXqchCzBXYf32ksVUTrtH1jSlamKHGz0Q0pRKIsA2fLqkE_MD3jP6wUDD6ExMw_tKYLx21lGcK41WSrRpDH-kcZo1QdgCy2ceNzaliBX1eHmKG0-H8tY4tPQudk-oHQmWTdvUIiHO6gSKMGDZNWv6bq74VTCsRfUEAkuWhqUhgRSGzlvlZ24wjHv5Qdlw"
 | 
						|
    }
 | 
						|
  }, 
 | 
						|
  "protected": "eyJub25jZSI6ICJibTl1WTJVIiwgInVybCI6ICJodHRwOi8vbG9jYWxob3N0L3Rlc3QiLCAia2lkIjogInRlc3RrZXkifQ", 
 | 
						|
  "payload": "Zm9v", 
 | 
						|
  "signature": "ghTIjrhiRl2pQ09vAkUUBbF5KziJdhzOTB-okM9SPRzU8Hyj0W1H5JA1Zoc-A-LuJGNAtYYHWqMw1SeZbT0l9FHcbMPeWDaJNkHS9jz5_g_Oyol8vcrWur2GDtB2Jgw6APtZKrbuGATbrF7g41Wijk6Kk9GXDoCnlfOQOhHhsrFFcWlCPLG-03TtKD6EBBoVBhmlp8DRLs7YguWRZ6jWNaEX-1WiRntBmhLqoqQFtvZxCBw_PRuaRw_RZBd1x2_BNYqEdOmVNC43UHMSJg3y_3yrPo905ur09aUTscf-C_m4Sa4M0FuDKn3bQ_pFrtz-aCCq6rcTIyxYpDqNvHMT2Q"
 | 
						|
}
 | 
						|
`
 | 
						|
 | 
						|
	conflictJWS, err := jose.ParseSigned(conflictJWSBody)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal("Unable to parse conflict JWS")
 | 
						|
	}
 | 
						|
 | 
						|
	testCases := []struct {
 | 
						|
		Name             string
 | 
						|
		JWS              *jose.JSONWebSignature
 | 
						|
		ExpectedAuthType jwsAuthType
 | 
						|
		ExpectedResult   *probs.ProblemDetails
 | 
						|
		ErrorStatType    string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			Name:             "Key ID and embedded JWS",
 | 
						|
			JWS:              conflictJWS,
 | 
						|
			ExpectedAuthType: invalidAuthType,
 | 
						|
			ExpectedResult: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "jwk and kid header fields are mutually exclusive",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSAuthTypeInvalid",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:             "Key ID when expected is embedded JWK",
 | 
						|
			JWS:              testKeyIDJWS,
 | 
						|
			ExpectedAuthType: embeddedJWK,
 | 
						|
			ExpectedResult: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "No embedded JWK in JWS header",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSAuthTypeWrong",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:             "Embedded JWK when expected is Key ID",
 | 
						|
			JWS:              testEmbeddedJWS,
 | 
						|
			ExpectedAuthType: embeddedKeyID,
 | 
						|
			ExpectedResult: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "No Key ID in JWS header",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSAuthTypeWrong",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:             "Key ID when expected is KeyID",
 | 
						|
			JWS:              testKeyIDJWS,
 | 
						|
			ExpectedAuthType: embeddedKeyID,
 | 
						|
			ExpectedResult:   nil,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:             "Embedded JWK when expected is embedded JWK",
 | 
						|
			JWS:              testEmbeddedJWS,
 | 
						|
			ExpectedAuthType: embeddedJWK,
 | 
						|
			ExpectedResult:   nil,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testCases {
 | 
						|
		t.Run(tc.Name, func(t *testing.T) {
 | 
						|
			wfe.stats.joseErrorCount.Reset()
 | 
						|
			prob := wfe.enforceJWSAuthType(tc.JWS.Signatures[0].Header, tc.ExpectedAuthType)
 | 
						|
			if tc.ExpectedResult == nil && prob != nil {
 | 
						|
				t.Fatalf("Expected nil result, got %#v", prob)
 | 
						|
			} else {
 | 
						|
				test.AssertMarshaledEquals(t, prob, tc.ExpectedResult)
 | 
						|
			}
 | 
						|
			if tc.ErrorStatType != "" {
 | 
						|
				test.AssertMetricWithLabelsEquals(
 | 
						|
					t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type badNonceProvider struct {
 | 
						|
}
 | 
						|
 | 
						|
func (badNonceProvider) Nonce() (string, error) {
 | 
						|
	return "im-a-nonce", nil
 | 
						|
}
 | 
						|
 | 
						|
func TestValidNonce(t *testing.T) {
 | 
						|
	wfe, _, signer := setupWFE(t)
 | 
						|
 | 
						|
	goodJWS, _, _ := signer.embeddedJWK(nil, "", "")
 | 
						|
 | 
						|
	testCases := []struct {
 | 
						|
		Name           string
 | 
						|
		JWS            *jose.JSONWebSignature
 | 
						|
		ExpectedResult *probs.ProblemDetails
 | 
						|
		ErrorStatType  string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			Name: "No nonce in JWS",
 | 
						|
			JWS:  signer.missingNonce(),
 | 
						|
			ExpectedResult: &probs.ProblemDetails{
 | 
						|
				Type:       probs.BadNonceProblem,
 | 
						|
				Detail:     "JWS has no anti-replay nonce",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSMissingNonce",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name: "Invalid nonce in JWS",
 | 
						|
			JWS:  signer.invalidNonce(),
 | 
						|
			ExpectedResult: &probs.ProblemDetails{
 | 
						|
				Type:       probs.BadNonceProblem,
 | 
						|
				Detail:     "JWS has an invalid anti-replay nonce: \"im-a-nonce\"",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSInvalidNonce",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:           "Valid nonce in JWS",
 | 
						|
			JWS:            goodJWS,
 | 
						|
			ExpectedResult: nil,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testCases {
 | 
						|
		t.Run(tc.Name, func(t *testing.T) {
 | 
						|
			wfe.stats.joseErrorCount.Reset()
 | 
						|
			prob := wfe.validNonce(context.Background(), tc.JWS.Signatures[0].Header)
 | 
						|
			if tc.ExpectedResult == nil && prob != nil {
 | 
						|
				t.Fatalf("Expected nil result, got %#v", prob)
 | 
						|
			} else {
 | 
						|
				test.AssertMarshaledEquals(t, prob, tc.ExpectedResult)
 | 
						|
			}
 | 
						|
			if tc.ErrorStatType != "" {
 | 
						|
				test.AssertMetricWithLabelsEquals(
 | 
						|
					t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (rs requestSigner) signExtraHeaders(
 | 
						|
	headers map[jose.HeaderKey]interface{}) (*jose.JSONWebSignature, string) {
 | 
						|
	privateKey := loadKey(rs.t, []byte(test1KeyPrivatePEM))
 | 
						|
 | 
						|
	signerKey := jose.SigningKey{
 | 
						|
		Key:       privateKey,
 | 
						|
		Algorithm: sigAlgForKey(rs.t, privateKey.Public()),
 | 
						|
	}
 | 
						|
 | 
						|
	opts := &jose.SignerOptions{
 | 
						|
		NonceSource:  rs.nonceService,
 | 
						|
		EmbedJWK:     true,
 | 
						|
		ExtraHeaders: headers,
 | 
						|
	}
 | 
						|
 | 
						|
	signer, err := jose.NewSigner(signerKey, opts)
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to make signer")
 | 
						|
 | 
						|
	jws, err := signer.Sign([]byte(""))
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to sign req")
 | 
						|
 | 
						|
	body := jws.FullSerialize()
 | 
						|
	parsedJWS, err := jose.ParseSigned(body)
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to parse generated JWS")
 | 
						|
 | 
						|
	return parsedJWS, body
 | 
						|
}
 | 
						|
 | 
						|
func TestValidPOSTURL(t *testing.T) {
 | 
						|
	wfe, _, signer := setupWFE(t)
 | 
						|
 | 
						|
	// A JWS and HTTP request with no extra headers
 | 
						|
	noHeadersJWS, noHeadersJWSBody := signer.signExtraHeaders(nil)
 | 
						|
	noHeadersRequest := makePostRequestWithPath("test-path", noHeadersJWSBody)
 | 
						|
 | 
						|
	// A JWS and HTTP request with extra headers, but no "url" extra header
 | 
						|
	noURLHeaders := map[jose.HeaderKey]interface{}{
 | 
						|
		"nifty": "swell",
 | 
						|
	}
 | 
						|
	noURLHeaderJWS, noURLHeaderJWSBody := signer.signExtraHeaders(noURLHeaders)
 | 
						|
	noURLHeaderRequest := makePostRequestWithPath("test-path", noURLHeaderJWSBody)
 | 
						|
 | 
						|
	// A JWS and HTTP request with a mismatched HTTP URL to JWS "url" header
 | 
						|
	wrongURLHeaders := map[jose.HeaderKey]interface{}{
 | 
						|
		"url": "foobar",
 | 
						|
	}
 | 
						|
	wrongURLHeaderJWS, wrongURLHeaderJWSBody := signer.signExtraHeaders(wrongURLHeaders)
 | 
						|
	wrongURLHeaderRequest := makePostRequestWithPath("test-path", wrongURLHeaderJWSBody)
 | 
						|
 | 
						|
	correctURLHeaderJWS, _, correctURLHeaderJWSBody := signer.embeddedJWK(nil, "http://localhost/test-path", "")
 | 
						|
	correctURLHeaderRequest := makePostRequestWithPath("test-path", correctURLHeaderJWSBody)
 | 
						|
 | 
						|
	testCases := []struct {
 | 
						|
		Name           string
 | 
						|
		JWS            *jose.JSONWebSignature
 | 
						|
		Request        *http.Request
 | 
						|
		ExpectedResult *probs.ProblemDetails
 | 
						|
		ErrorStatType  string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			Name:    "No extra headers in JWS",
 | 
						|
			JWS:     noHeadersJWS,
 | 
						|
			Request: noHeadersRequest,
 | 
						|
			ExpectedResult: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "JWS header parameter 'url' required",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSNoExtraHeaders",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "No URL header in JWS",
 | 
						|
			JWS:     noURLHeaderJWS,
 | 
						|
			Request: noURLHeaderRequest,
 | 
						|
			ExpectedResult: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "JWS header parameter 'url' required",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSMissingURL",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "Wrong URL header in JWS",
 | 
						|
			JWS:     wrongURLHeaderJWS,
 | 
						|
			Request: wrongURLHeaderRequest,
 | 
						|
			ExpectedResult: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "JWS header parameter 'url' incorrect. Expected \"http://localhost/test-path\" got \"foobar\"",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSMismatchedURL",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:           "Correct URL header in JWS",
 | 
						|
			JWS:            correctURLHeaderJWS,
 | 
						|
			Request:        correctURLHeaderRequest,
 | 
						|
			ExpectedResult: nil,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testCases {
 | 
						|
		t.Run(tc.Name, func(t *testing.T) {
 | 
						|
			tc.Request.Header.Add("Content-Type", expectedJWSContentType)
 | 
						|
			wfe.stats.joseErrorCount.Reset()
 | 
						|
			prob := wfe.validPOSTURL(tc.Request, tc.JWS.Signatures[0].Header)
 | 
						|
			if tc.ExpectedResult == nil && prob != nil {
 | 
						|
				t.Fatalf("Expected nil result, got %#v", prob)
 | 
						|
			} else {
 | 
						|
				test.AssertMarshaledEquals(t, prob, tc.ExpectedResult)
 | 
						|
			}
 | 
						|
			if tc.ErrorStatType != "" {
 | 
						|
				test.AssertMetricWithLabelsEquals(
 | 
						|
					t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (rs requestSigner) multiSigJWS() (*jose.JSONWebSignature, string) {
 | 
						|
	privateKeyA := loadKey(rs.t, []byte(test1KeyPrivatePEM))
 | 
						|
	privateKeyB := loadKey(rs.t, []byte(test2KeyPrivatePEM))
 | 
						|
 | 
						|
	signerKeyA := jose.SigningKey{
 | 
						|
		Key:       privateKeyA,
 | 
						|
		Algorithm: sigAlgForKey(rs.t, privateKeyA.Public()),
 | 
						|
	}
 | 
						|
 | 
						|
	signerKeyB := jose.SigningKey{
 | 
						|
		Key:       privateKeyB,
 | 
						|
		Algorithm: sigAlgForKey(rs.t, privateKeyB.Public()),
 | 
						|
	}
 | 
						|
 | 
						|
	opts := &jose.SignerOptions{
 | 
						|
		NonceSource: rs.nonceService,
 | 
						|
		EmbedJWK:    true,
 | 
						|
	}
 | 
						|
 | 
						|
	signer, err := jose.NewMultiSigner([]jose.SigningKey{signerKeyA, signerKeyB}, opts)
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to make multi signer")
 | 
						|
 | 
						|
	jws, err := signer.Sign([]byte(""))
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to sign req")
 | 
						|
 | 
						|
	body := jws.FullSerialize()
 | 
						|
	parsedJWS, err := jose.ParseSigned(body)
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to parse generated JWS")
 | 
						|
 | 
						|
	return parsedJWS, body
 | 
						|
}
 | 
						|
 | 
						|
func TestParseJWSRequest(t *testing.T) {
 | 
						|
	wfe, _, signer := setupWFE(t)
 | 
						|
 | 
						|
	_, tooManySigsJWSBody := signer.multiSigJWS()
 | 
						|
 | 
						|
	_, _, validJWSBody := signer.embeddedJWK(nil, "http://localhost/test-path", "")
 | 
						|
	validJWSRequest := makePostRequestWithPath("test-path", validJWSBody)
 | 
						|
 | 
						|
	missingSigsJWSBody := `{"payload":"Zm9x","protected":"eyJhbGciOiJSUzI1NiIsImp3ayI6eyJrdHkiOiJSU0EiLCJuIjoicW5BUkxyVDdYejRnUmNLeUxkeWRtQ3ItZXk5T3VQSW1YNFg0MHRoazNvbjI2RmtNem5SM2ZSanM2NmVMSzdtbVBjQlo2dU9Kc2VVUlU2d0FhWk5tZW1vWXgxZE12cXZXV0l5aVFsZUhTRDdROHZCcmhSNnVJb080akF6SlpSLUNoelp1U0R0N2lITi0zeFVWc3B1NVhHd1hVX01WSlpzaFR3cDRUYUZ4NWVsSElUX09iblR2VE9VM1hoaXNoMDdBYmdaS21Xc1ZiWGg1cy1DcklpY1U0T2V4SlBndW5XWl9ZSkp1ZU9LbVR2bkxsVFY0TXpLUjJvWmxCS1oyN1MwLVNmZFZfUUR4X3lkbGU1b01BeUtWdGxBVjM1Y3lQTUlzWU53Z1VHQkNkWV8yVXppNWVYMGxUYzdNUFJ3ejZxUjFraXAtaTU5VmNHY1VRZ3FIVjZGeXF3IiwiZSI6IkFRQUIifSwia2lkIjoiIiwibm9uY2UiOiJyNHpuenZQQUVwMDlDN1JwZUtYVHhvNkx3SGwxZVBVdmpGeXhOSE1hQnVvIiwidXJsIjoiaHR0cDovL2xvY2FsaG9zdC9hY21lL25ldy1yZWcifQ"}`
 | 
						|
	missingSigsJWSRequest := makePostRequestWithPath("test-path", missingSigsJWSBody)
 | 
						|
 | 
						|
	unprotectedHeadersJWSBody := `
 | 
						|
{
 | 
						|
  "header": {
 | 
						|
    "alg": "RS256",
 | 
						|
    "kid": "unprotected key id"
 | 
						|
  },
 | 
						|
  "protected": "eyJub25jZSI6ICJibTl1WTJVIiwgInVybCI6ICJodHRwOi8vbG9jYWxob3N0L3Rlc3QiLCAia2lkIjogInRlc3RrZXkifQ", 
 | 
						|
  "payload": "Zm9v",
 | 
						|
  "signature": "PKWWclRsiHF4bm-nmpxDez6Y_3Mdtu263YeYklbGYt1EiMOLiKY_dr_EqhUUKAKEWysFLO-hQLXVU7kVkHeYWQFFOA18oFgcZgkSF2Pr3DNZrVj9e2gl0eZ2i2jk6X5GYPt1lIfok_DrL92wrxEKGcrmxqXXGm0JgP6Al2VGapKZK2HaYbCHoGvtzNmzUX9rC21sKewq5CquJRvTmvQp5bmU7Q9KeafGibFr0jl6IA3W5LBGgf6xftuUtEVEbKmKaKtaG7tXsQH1mIVOPUZZoLWz9sWJSFLmV0QSXm3ZHV0DrOhLfcADbOCoQBMeGdseBQZuUO541A3BEKGv2Aikjw"
 | 
						|
}
 | 
						|
`
 | 
						|
 | 
						|
	wrongSignaturesFieldJWSBody := `
 | 
						|
{
 | 
						|
  "protected": "eyJub25jZSI6ICJibTl1WTJVIiwgInVybCI6ICJodHRwOi8vbG9jYWxob3N0L3Rlc3QiLCAia2lkIjogInRlc3RrZXkifQ", 
 | 
						|
  "payload": "Zm9v",
 | 
						|
  "signatures": ["PKWWclRsiHF4bm-nmpxDez6Y_3Mdtu263YeYklbGYt1EiMOLiKY_dr_EqhUUKAKEWysFLO-hQLXVU7kVkHeYWQFFOA18oFgcZgkSF2Pr3DNZrVj9e2gl0eZ2i2jk6X5GYPt1lIfok_DrL92wrxEKGcrmxqXXGm0JgP6Al2VGapKZK2HaYbCHoGvtzNmzUX9rC21sKewq5CquJRvTmvQp5bmU7Q9KeafGibFr0jl6IA3W5LBGgf6xftuUtEVEbKmKaKtaG7tXsQH1mIVOPUZZoLWz9sWJSFLmV0QSXm3ZHV0DrOhLfcADbOCoQBMeGdseBQZuUO541A3BEKGv2Aikjw"]
 | 
						|
}
 | 
						|
`
 | 
						|
 | 
						|
	testCases := []struct {
 | 
						|
		Name            string
 | 
						|
		Request         *http.Request
 | 
						|
		ExpectedProblem *probs.ProblemDetails
 | 
						|
		ErrorStatType   string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			Name: "Invalid POST request",
 | 
						|
			// No Content-Length, something that validPOSTRequest should be flagging
 | 
						|
			Request: &http.Request{
 | 
						|
				Method: "POST",
 | 
						|
				URL:    mustParseURL("/"),
 | 
						|
			},
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "missing Content-Length header",
 | 
						|
				HTTPStatus: http.StatusLengthRequired,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "Invalid JWS in POST body",
 | 
						|
			Request: makePostRequestWithPath("test-path", `{`),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "Parse error reading JWS",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSUnmarshalFailed",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "Too few signatures in JWS",
 | 
						|
			Request: missingSigsJWSRequest,
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "POST JWS not signed",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSEmptySignature",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "Too many signatures in JWS",
 | 
						|
			Request: makePostRequestWithPath("test-path", tooManySigsJWSBody),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "JWS \"signatures\" field not allowed. Only the \"signature\" field should contain a signature",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSMultiSig",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "Unprotected JWS headers",
 | 
						|
			Request: makePostRequestWithPath("test-path", unprotectedHeadersJWSBody),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "JWS \"header\" field not allowed. All headers must be in \"protected\" field",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSUnprotectedHeaders",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "Unsupported signatures field in JWS",
 | 
						|
			Request: makePostRequestWithPath("test-path", wrongSignaturesFieldJWSBody),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "JWS \"signatures\" field not allowed. Only the \"signature\" field should contain a signature",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSMultiSig",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:            "Valid JWS in POST request",
 | 
						|
			Request:         validJWSRequest,
 | 
						|
			ExpectedProblem: nil,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name: "POST body too large",
 | 
						|
			Request: makePostRequestWithPath("test-path",
 | 
						|
				fmt.Sprintf(`{"a":"%s"}`, strings.Repeat("a", 50000))),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.UnauthorizedProblem,
 | 
						|
				Detail:     "request body too large",
 | 
						|
				HTTPStatus: http.StatusForbidden,
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testCases {
 | 
						|
		t.Run(tc.Name, func(t *testing.T) {
 | 
						|
			wfe.stats.joseErrorCount.Reset()
 | 
						|
			_, prob := wfe.parseJWSRequest(tc.Request)
 | 
						|
			if tc.ExpectedProblem == nil && prob != nil {
 | 
						|
				t.Fatalf("Expected nil problem, got %#v\n", prob)
 | 
						|
			} else {
 | 
						|
				test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem)
 | 
						|
			}
 | 
						|
			if tc.ErrorStatType != "" {
 | 
						|
				test.AssertMetricWithLabelsEquals(
 | 
						|
					t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestExtractJWK(t *testing.T) {
 | 
						|
	wfe, _, signer := setupWFE(t)
 | 
						|
 | 
						|
	keyIDJWS, _, _ := signer.byKeyID(1, nil, "", "")
 | 
						|
	goodJWS, goodJWK, _ := signer.embeddedJWK(nil, "", "")
 | 
						|
 | 
						|
	testCases := []struct {
 | 
						|
		Name            string
 | 
						|
		JWS             *jose.JSONWebSignature
 | 
						|
		ExpectedKey     *jose.JSONWebKey
 | 
						|
		ExpectedProblem *probs.ProblemDetails
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			Name: "JWS with wrong auth type (Key ID vs embedded JWK)",
 | 
						|
			JWS:  keyIDJWS,
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "No embedded JWK in JWS header",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:        "Valid JWS with embedded JWK",
 | 
						|
			JWS:         goodJWS,
 | 
						|
			ExpectedKey: goodJWK,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testCases {
 | 
						|
		t.Run(tc.Name, func(t *testing.T) {
 | 
						|
			jwkHeader, prob := wfe.extractJWK(tc.JWS.Signatures[0].Header)
 | 
						|
			if tc.ExpectedProblem == nil && prob != nil {
 | 
						|
				t.Fatalf("Expected nil problem, got %#v\n", prob)
 | 
						|
			} else if tc.ExpectedProblem == nil {
 | 
						|
				test.AssertMarshaledEquals(t, jwkHeader, tc.ExpectedKey)
 | 
						|
			} else {
 | 
						|
				test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (rs requestSigner) specifyKeyID(keyID string) (*jose.JSONWebSignature, string) {
 | 
						|
	privateKey := loadKey(rs.t, []byte(test1KeyPrivatePEM))
 | 
						|
 | 
						|
	if keyID == "" {
 | 
						|
		keyID = "this is an invalid non-numeric key ID"
 | 
						|
	}
 | 
						|
 | 
						|
	jwk := &jose.JSONWebKey{
 | 
						|
		Key:       privateKey,
 | 
						|
		Algorithm: "RSA",
 | 
						|
		KeyID:     keyID,
 | 
						|
	}
 | 
						|
 | 
						|
	signerKey := jose.SigningKey{
 | 
						|
		Key:       jwk,
 | 
						|
		Algorithm: jose.RS256,
 | 
						|
	}
 | 
						|
 | 
						|
	opts := &jose.SignerOptions{
 | 
						|
		NonceSource: rs.nonceService,
 | 
						|
		ExtraHeaders: map[jose.HeaderKey]interface{}{
 | 
						|
			"url": "http://localhost",
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	signer, err := jose.NewSigner(signerKey, opts)
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to make signer")
 | 
						|
 | 
						|
	jws, err := signer.Sign([]byte(""))
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to sign req")
 | 
						|
 | 
						|
	body := jws.FullSerialize()
 | 
						|
	parsedJWS, err := jose.ParseSigned(body)
 | 
						|
	test.AssertNotError(rs.t, err, "Failed to parse generated JWS")
 | 
						|
 | 
						|
	return parsedJWS, body
 | 
						|
}
 | 
						|
 | 
						|
func TestLookupJWK(t *testing.T) {
 | 
						|
	wfe, _, signer := setupWFE(t)
 | 
						|
 | 
						|
	embeddedJWS, _, embeddedJWSBody := signer.embeddedJWK(nil, "", "")
 | 
						|
	invalidKeyIDJWS, invalidKeyIDJWSBody := signer.specifyKeyID("https://acme-99.lettuceencrypt.org/acme/reg/1")
 | 
						|
	// ID 100 is mocked to return a non-missing error from sa.GetRegistration
 | 
						|
	errorIDJWS, _, errorIDJWSBody := signer.byKeyID(100, nil, "", "")
 | 
						|
	// ID 102 is mocked to return an account does not exist error from sa.GetRegistration
 | 
						|
	missingIDJWS, _, missingIDJWSBody := signer.byKeyID(102, nil, "", "")
 | 
						|
	// ID 3 is mocked to return a deactivated account from sa.GetRegistration
 | 
						|
	deactivatedIDJWS, _, deactivatedIDJWSBody := signer.byKeyID(3, nil, "", "")
 | 
						|
 | 
						|
	wfe.LegacyKeyIDPrefix = "https://acme-v00.lettuceencrypt.org/acme/reg/"
 | 
						|
	legacyKeyIDJWS, legacyKeyIDJWSBody := signer.specifyKeyID(wfe.LegacyKeyIDPrefix + "1")
 | 
						|
 | 
						|
	nonNumericKeyIDJWS, nonNumericKeyIDJWSBody := signer.specifyKeyID(wfe.LegacyKeyIDPrefix + "abcd")
 | 
						|
 | 
						|
	validJWS, validKey, validJWSBody := signer.byKeyID(1, nil, "", "")
 | 
						|
	validAccountPB, _ := wfe.sa.GetRegistration(context.Background(), &sapb.RegistrationID{Id: 1})
 | 
						|
	validAccount, _ := bgrpc.PbToRegistration(validAccountPB)
 | 
						|
 | 
						|
	// good key, log event requester is set
 | 
						|
 | 
						|
	testCases := []struct {
 | 
						|
		Name            string
 | 
						|
		JWS             *jose.JSONWebSignature
 | 
						|
		Request         *http.Request
 | 
						|
		ExpectedProblem *probs.ProblemDetails
 | 
						|
		ExpectedKey     *jose.JSONWebKey
 | 
						|
		ExpectedAccount *core.Registration
 | 
						|
		ErrorStatType   string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			Name:    "JWS with wrong auth type (embedded JWK vs Key ID)",
 | 
						|
			JWS:     embeddedJWS,
 | 
						|
			Request: makePostRequestWithPath("test-path", embeddedJWSBody),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "No Key ID in JWS header",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSAuthTypeWrong",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "JWS with invalid key ID URL",
 | 
						|
			JWS:     invalidKeyIDJWS,
 | 
						|
			Request: makePostRequestWithPath("test-path", invalidKeyIDJWSBody),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "KeyID header contained an invalid account URL: \"https://acme-99.lettuceencrypt.org/acme/reg/1\"",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSInvalidKeyID",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "JWS with non-numeric account ID in key ID URL",
 | 
						|
			JWS:     nonNumericKeyIDJWS,
 | 
						|
			Request: makePostRequestWithPath("test-path", nonNumericKeyIDJWSBody),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "Malformed account ID in KeyID header URL: \"https://acme-v00.lettuceencrypt.org/acme/reg/abcd\"",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSInvalidKeyID",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "JWS with account ID that causes GetRegistration error",
 | 
						|
			JWS:     errorIDJWS,
 | 
						|
			Request: makePostRequestWithPath("test-path", errorIDJWSBody),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.ServerInternalProblem,
 | 
						|
				Detail:     "Error retrieving account \"http://localhost/acme/acct/100\"",
 | 
						|
				HTTPStatus: http.StatusInternalServerError,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSKeyIDLookupFailed",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "JWS with account ID that doesn't exist",
 | 
						|
			JWS:     missingIDJWS,
 | 
						|
			Request: makePostRequestWithPath("test-path", missingIDJWSBody),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.AccountDoesNotExistProblem,
 | 
						|
				Detail:     "Account \"http://localhost/acme/acct/102\" not found",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSKeyIDNotFound",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "JWS with account ID that is deactivated",
 | 
						|
			JWS:     deactivatedIDJWS,
 | 
						|
			Request: makePostRequestWithPath("test-path", deactivatedIDJWSBody),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.UnauthorizedProblem,
 | 
						|
				Detail:     "Account is not valid, has status \"deactivated\"",
 | 
						|
				HTTPStatus: http.StatusForbidden,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSKeyIDAccountInvalid",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:            "Valid JWS with legacy account ID",
 | 
						|
			JWS:             legacyKeyIDJWS,
 | 
						|
			Request:         makePostRequestWithPath("test-path", legacyKeyIDJWSBody),
 | 
						|
			ExpectedKey:     validKey,
 | 
						|
			ExpectedAccount: &validAccount,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:            "Valid JWS with valid account ID",
 | 
						|
			JWS:             validJWS,
 | 
						|
			Request:         makePostRequestWithPath("test-path", validJWSBody),
 | 
						|
			ExpectedKey:     validKey,
 | 
						|
			ExpectedAccount: &validAccount,
 | 
						|
		},
 | 
						|
	}
 | 
						|
	for _, tc := range testCases {
 | 
						|
		t.Run(tc.Name, func(t *testing.T) {
 | 
						|
			wfe.stats.joseErrorCount.Reset()
 | 
						|
			inputLogEvent := newRequestEvent()
 | 
						|
			jwkHeader, acct, prob := wfe.lookupJWK(tc.JWS.Signatures[0].Header, context.Background(), tc.Request, inputLogEvent)
 | 
						|
			if tc.ExpectedProblem == nil && prob != nil {
 | 
						|
				t.Fatalf("Expected nil problem, got %#v\n", prob)
 | 
						|
			} else if tc.ExpectedProblem == nil {
 | 
						|
				inThumb, _ := tc.ExpectedKey.Thumbprint(crypto.SHA256)
 | 
						|
				outThumb, _ := jwkHeader.Thumbprint(crypto.SHA256)
 | 
						|
				test.AssertDeepEquals(t, inThumb, outThumb)
 | 
						|
				test.AssertMarshaledEquals(t, acct, tc.ExpectedAccount)
 | 
						|
				test.AssertEquals(t, inputLogEvent.Requester, acct.ID)
 | 
						|
				test.AssertEquals(t, fmt.Sprint(inputLogEvent.Contacts), fmt.Sprint(*acct.Contact))
 | 
						|
			} else {
 | 
						|
				test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem)
 | 
						|
			}
 | 
						|
			if tc.ErrorStatType != "" {
 | 
						|
				test.AssertMetricWithLabelsEquals(
 | 
						|
					t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestValidJWSForKey(t *testing.T) {
 | 
						|
	wfe, _, signer := setupWFE(t)
 | 
						|
 | 
						|
	payload := `{ "test": "payload" }`
 | 
						|
	testURL := "http://localhost/test"
 | 
						|
	goodJWS, goodJWK, _ := signer.embeddedJWK(nil, testURL, payload)
 | 
						|
 | 
						|
	// badSigJWSBody is a JWS that has had the payload changed by 1 byte to break the signature
 | 
						|
	badSigJWSBody := `{"payload":"Zm9x","protected":"eyJhbGciOiJSUzI1NiIsImp3ayI6eyJrdHkiOiJSU0EiLCJuIjoicW5BUkxyVDdYejRnUmNLeUxkeWRtQ3ItZXk5T3VQSW1YNFg0MHRoazNvbjI2RmtNem5SM2ZSanM2NmVMSzdtbVBjQlo2dU9Kc2VVUlU2d0FhWk5tZW1vWXgxZE12cXZXV0l5aVFsZUhTRDdROHZCcmhSNnVJb080akF6SlpSLUNoelp1U0R0N2lITi0zeFVWc3B1NVhHd1hVX01WSlpzaFR3cDRUYUZ4NWVsSElUX09iblR2VE9VM1hoaXNoMDdBYmdaS21Xc1ZiWGg1cy1DcklpY1U0T2V4SlBndW5XWl9ZSkp1ZU9LbVR2bkxsVFY0TXpLUjJvWmxCS1oyN1MwLVNmZFZfUUR4X3lkbGU1b01BeUtWdGxBVjM1Y3lQTUlzWU53Z1VHQkNkWV8yVXppNWVYMGxUYzdNUFJ3ejZxUjFraXAtaTU5VmNHY1VRZ3FIVjZGeXF3IiwiZSI6IkFRQUIifSwia2lkIjoiIiwibm9uY2UiOiJyNHpuenZQQUVwMDlDN1JwZUtYVHhvNkx3SGwxZVBVdmpGeXhOSE1hQnVvIiwidXJsIjoiaHR0cDovL2xvY2FsaG9zdC9hY21lL25ldy1yZWcifQ","signature":"jcTdxSygm_cvD7KbXqsxgnoPApCTSkV4jolToSOd2ciRkg5W7Yl0ZKEEKwOc-dYIbQiwGiDzisyPCicwWsOUA1WSqHylKvZ3nxSMc6KtwJCW2DaOqcf0EEjy5VjiZJUrOt2c-r6b07tbn8sfOJKwlF2lsOeGi4s-rtvvkeQpAU-AWauzl9G4bv2nDUeCviAZjHx_PoUC-f9GmZhYrbDzAvXZ859ktM6RmMeD0OqPN7bhAeju2j9Gl0lnryZMtq2m0J2m1ucenQBL1g4ZkP1JiJvzd2cAz5G7Ftl2YeJJyWhqNd3qq0GVOt1P11s8PTGNaSoM0iR9QfUxT9A6jxARtg"}`
 | 
						|
	badJWS, err := jose.ParseSigned(badSigJWSBody)
 | 
						|
	test.AssertNotError(t, err, "error loading badSigJWS body")
 | 
						|
 | 
						|
	// wrongAlgJWS is a JWS that has an invalid "HS256" algorithm in its header
 | 
						|
	wrongAlgJWS := &jose.JSONWebSignature{
 | 
						|
		Signatures: []jose.Signature{
 | 
						|
			{
 | 
						|
				Header: jose.Header{
 | 
						|
					Algorithm: "HS256",
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	// A JWS and HTTP request with a mismatched HTTP URL to JWS "url" header
 | 
						|
	wrongURLHeaders := map[jose.HeaderKey]interface{}{
 | 
						|
		"url": "foobar",
 | 
						|
	}
 | 
						|
	wrongURLHeaderJWS, _ := signer.signExtraHeaders(wrongURLHeaders)
 | 
						|
 | 
						|
	// badJSONJWS has a valid signature over a body that is not valid JSON
 | 
						|
	badJSONJWS, _, _ := signer.embeddedJWK(nil, testURL, `{`)
 | 
						|
 | 
						|
	testCases := []struct {
 | 
						|
		Name            string
 | 
						|
		JWS             bJSONWebSignature
 | 
						|
		JWK             *jose.JSONWebKey
 | 
						|
		Body            string
 | 
						|
		ExpectedProblem *probs.ProblemDetails
 | 
						|
		ErrorStatType   string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			Name: "JWS with an invalid algorithm",
 | 
						|
			JWS:  bJSONWebSignature{wrongAlgJWS},
 | 
						|
			JWK:  goodJWK,
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.BadSignatureAlgorithmProblem,
 | 
						|
				Detail:     "JWS signature header contains unsupported algorithm \"HS256\", expected one of RS256, ES256, ES384 or ES512",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSAlgorithmCheckFailed",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name: "JWS with an invalid nonce",
 | 
						|
			JWS:  bJSONWebSignature{signer.invalidNonce()},
 | 
						|
			JWK:  goodJWK,
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.BadNonceProblem,
 | 
						|
				Detail:     "JWS has an invalid anti-replay nonce: \"im-a-nonce\"",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSInvalidNonce",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name: "JWS with broken signature",
 | 
						|
			JWS:  bJSONWebSignature{badJWS},
 | 
						|
			JWK:  badJWS.Signatures[0].Header.JSONWebKey,
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "JWS verification error",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSVerifyFailed",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name: "JWS with incorrect URL",
 | 
						|
			JWS:  bJSONWebSignature{wrongURLHeaderJWS},
 | 
						|
			JWK:  wrongURLHeaderJWS.Signatures[0].Header.JSONWebKey,
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "JWS header parameter 'url' incorrect. Expected \"http://localhost/test\" got \"foobar\"",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSMismatchedURL",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name: "Valid JWS with invalid JSON in the protected body",
 | 
						|
			JWS:  bJSONWebSignature{badJSONJWS},
 | 
						|
			JWK:  goodJWK,
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "Request payload did not parse as JSON",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSBodyUnmarshalFailed",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name: "Good JWS and JWK",
 | 
						|
			JWS:  bJSONWebSignature{goodJWS},
 | 
						|
			JWK:  goodJWK,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testCases {
 | 
						|
		t.Run(tc.Name, func(t *testing.T) {
 | 
						|
			wfe.stats.joseErrorCount.Reset()
 | 
						|
			request := makePostRequestWithPath("test", tc.Body)
 | 
						|
			outPayload, prob := wfe.validJWSForKey(context.Background(), &tc.JWS, tc.JWK, request)
 | 
						|
			if tc.ExpectedProblem == nil && prob != nil {
 | 
						|
				t.Fatalf("Expected nil problem, got %#v\n", prob)
 | 
						|
			} else if tc.ExpectedProblem == nil {
 | 
						|
				test.AssertEquals(t, string(outPayload), payload)
 | 
						|
			} else {
 | 
						|
				test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem)
 | 
						|
			}
 | 
						|
			if tc.ErrorStatType != "" {
 | 
						|
				test.AssertMetricWithLabelsEquals(
 | 
						|
					t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestValidPOSTForAccount(t *testing.T) {
 | 
						|
	wfe, _, signer := setupWFE(t)
 | 
						|
 | 
						|
	validJWS, _, validJWSBody := signer.byKeyID(1, nil, "http://localhost/test", `{"test":"passed"}`)
 | 
						|
	validAccountPB, _ := wfe.sa.GetRegistration(context.Background(), &sapb.RegistrationID{Id: 1})
 | 
						|
	validAccount, _ := bgrpc.PbToRegistration(validAccountPB)
 | 
						|
 | 
						|
	// ID 102 is mocked to return missing
 | 
						|
	_, _, missingJWSBody := signer.byKeyID(102, nil, "http://localhost/test", "{}")
 | 
						|
 | 
						|
	// ID 3 is mocked to return deactivated
 | 
						|
	key3 := loadKey(t, []byte(test3KeyPrivatePEM))
 | 
						|
	_, _, deactivatedJWSBody := signer.byKeyID(3, key3, "http://localhost/test", "{}")
 | 
						|
 | 
						|
	_, _, embeddedJWSBody := signer.embeddedJWK(nil, "http://localhost/test", `{"test":"passed"}`)
 | 
						|
 | 
						|
	testCases := []struct {
 | 
						|
		Name            string
 | 
						|
		Request         *http.Request
 | 
						|
		ExpectedProblem *probs.ProblemDetails
 | 
						|
		ExpectedPayload string
 | 
						|
		ExpectedAcct    *core.Registration
 | 
						|
		ExpectedJWS     *jose.JSONWebSignature
 | 
						|
		ErrorStatType   string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			Name:    "Invalid JWS",
 | 
						|
			Request: makePostRequestWithPath("test", "foo"),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "Parse error reading JWS",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSUnmarshalFailed",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "Embedded Key JWS",
 | 
						|
			Request: makePostRequestWithPath("test", embeddedJWSBody),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "No Key ID in JWS header",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSAuthTypeWrong",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "JWS signed by account that doesn't exist",
 | 
						|
			Request: makePostRequestWithPath("test", missingJWSBody),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.AccountDoesNotExistProblem,
 | 
						|
				Detail:     "Account \"http://localhost/acme/acct/102\" not found",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSKeyIDNotFound",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "JWS signed by account that's deactivated",
 | 
						|
			Request: makePostRequestWithPath("test", deactivatedJWSBody),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.UnauthorizedProblem,
 | 
						|
				Detail:     "Account is not valid, has status \"deactivated\"",
 | 
						|
				HTTPStatus: http.StatusForbidden,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSKeyIDAccountInvalid",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:            "Valid JWS for account",
 | 
						|
			Request:         makePostRequestWithPath("test", validJWSBody),
 | 
						|
			ExpectedPayload: `{"test":"passed"}`,
 | 
						|
			ExpectedAcct:    &validAccount,
 | 
						|
			ExpectedJWS:     validJWS,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testCases {
 | 
						|
		t.Run(tc.Name, func(t *testing.T) {
 | 
						|
			wfe.stats.joseErrorCount.Reset()
 | 
						|
			inputLogEvent := newRequestEvent()
 | 
						|
			outPayload, jws, acct, prob := wfe.validPOSTForAccount(tc.Request, context.Background(), inputLogEvent)
 | 
						|
			if tc.ExpectedProblem == nil && prob != nil {
 | 
						|
				t.Fatalf("Expected nil problem, got %#v\n", prob)
 | 
						|
			} else if tc.ExpectedProblem == nil {
 | 
						|
				test.AssertEquals(t, string(outPayload), tc.ExpectedPayload)
 | 
						|
				test.AssertMarshaledEquals(t, acct, tc.ExpectedAcct)
 | 
						|
				test.AssertMarshaledEquals(t, jws, tc.ExpectedJWS)
 | 
						|
			} else {
 | 
						|
				test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem)
 | 
						|
			}
 | 
						|
			if tc.ErrorStatType != "" {
 | 
						|
				test.AssertMetricWithLabelsEquals(
 | 
						|
					t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// TestValidPOSTAsGETForAccount tests POST-as-GET processing. Because
 | 
						|
// wfe.validPOSTAsGETForAccount calls `wfe.validPOSTForAccount` to do all
 | 
						|
// processing except the empty body test we do not duplicate the
 | 
						|
// `TestValidPOSTForAccount` testcases here.
 | 
						|
func TestValidPOSTAsGETForAccount(t *testing.T) {
 | 
						|
	wfe, _, signer := setupWFE(t)
 | 
						|
 | 
						|
	// an invalid POST-as-GET request contains a non-empty payload. In this case
 | 
						|
	// we test with the empty JSON payload ("{}")
 | 
						|
	_, _, invalidPayloadRequest := signer.byKeyID(1, nil, "http://localhost/test", "{}")
 | 
						|
	// a valid POST-as-GET request contains an empty payload.
 | 
						|
	_, _, validRequest := signer.byKeyID(1, nil, "http://localhost/test", "")
 | 
						|
 | 
						|
	testCases := []struct {
 | 
						|
		Name             string
 | 
						|
		Request          *http.Request
 | 
						|
		ExpectedProblem  *probs.ProblemDetails
 | 
						|
		ExpectedLogEvent web.RequestEvent
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			Name:            "Non-empty JWS payload",
 | 
						|
			Request:         makePostRequestWithPath("test", invalidPayloadRequest),
 | 
						|
			ExpectedProblem: probs.Malformed("POST-as-GET requests must have an empty payload"),
 | 
						|
			ExpectedLogEvent: web.RequestEvent{
 | 
						|
				Contacts: []string{"mailto:person@mail.com"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "Valid POST-as-GET",
 | 
						|
			Request: makePostRequestWithPath("test", validRequest),
 | 
						|
			ExpectedLogEvent: web.RequestEvent{
 | 
						|
				Contacts: []string{"mailto:person@mail.com"},
 | 
						|
				Method:   "POST-as-GET",
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testCases {
 | 
						|
		ev := newRequestEvent()
 | 
						|
		_, prob := wfe.validPOSTAsGETForAccount(
 | 
						|
			tc.Request,
 | 
						|
			context.Background(),
 | 
						|
			ev)
 | 
						|
		if tc.ExpectedProblem == nil && prob != nil {
 | 
						|
			t.Fatalf("Expected nil problem, got %#v\n", prob)
 | 
						|
		} else if tc.ExpectedProblem != nil {
 | 
						|
			test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem)
 | 
						|
		}
 | 
						|
		test.AssertMarshaledEquals(t, *ev, tc.ExpectedLogEvent)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type mockSADifferentStoredKey struct {
 | 
						|
	sapb.StorageAuthorityReadOnlyClient
 | 
						|
}
 | 
						|
 | 
						|
// mockSADifferentStoredKey has a GetRegistration that will always return an
 | 
						|
// account with the test 2 key, no matter the provided ID
 | 
						|
func (sa mockSADifferentStoredKey) GetRegistration(_ context.Context, _ *sapb.RegistrationID, _ ...grpc.CallOption) (*corepb.Registration, error) {
 | 
						|
	return &corepb.Registration{
 | 
						|
		Key:    []byte(test2KeyPublicJSON),
 | 
						|
		Status: string(core.StatusValid),
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func TestValidPOSTForAccountSwappedKey(t *testing.T) {
 | 
						|
	wfe, fc, signer := setupWFE(t)
 | 
						|
	wfe.sa = &mockSADifferentStoredKey{mocks.NewStorageAuthorityReadOnly(fc)}
 | 
						|
	wfe.accountGetter = wfe.sa
 | 
						|
	event := newRequestEvent()
 | 
						|
 | 
						|
	payload := `{"resource":"ima-payload"}`
 | 
						|
	// Sign a request using test1key
 | 
						|
	_, _, body := signer.byKeyID(1, nil, "http://localhost:4001/test", payload)
 | 
						|
	request := makePostRequestWithPath("test", body)
 | 
						|
 | 
						|
	// Ensure that ValidPOSTForAccount produces an error since the
 | 
						|
	// mockSADifferentStoredKey will return a different key than the one we used to
 | 
						|
	// sign the request
 | 
						|
	_, _, _, prob := wfe.validPOSTForAccount(request, ctx, event)
 | 
						|
	test.Assert(t, prob != nil, "No error returned for request signed by wrong key")
 | 
						|
	test.AssertEquals(t, prob.Type, probs.MalformedProblem)
 | 
						|
	test.AssertEquals(t, prob.Detail, "JWS verification error")
 | 
						|
}
 | 
						|
 | 
						|
func TestValidSelfAuthenticatedPOSTGoodKeyErrors(t *testing.T) {
 | 
						|
	wfe, _, signer := setupWFE(t)
 | 
						|
 | 
						|
	timeoutErrCheckFunc := func(ctx context.Context, keyHash []byte) (bool, error) {
 | 
						|
		return false, context.DeadlineExceeded
 | 
						|
	}
 | 
						|
 | 
						|
	kp, err := goodkey.NewKeyPolicy(&goodkey.Config{}, timeoutErrCheckFunc)
 | 
						|
	test.AssertNotError(t, err, "making key policy")
 | 
						|
 | 
						|
	wfe.keyPolicy = kp
 | 
						|
 | 
						|
	_, _, validJWSBody := signer.embeddedJWK(nil, "http://localhost/test", `{"test":"passed"}`)
 | 
						|
	request := makePostRequestWithPath("test", validJWSBody)
 | 
						|
 | 
						|
	_, _, prob := wfe.validSelfAuthenticatedPOST(context.Background(), request)
 | 
						|
	test.AssertEquals(t, prob.Type, probs.ServerInternalProblem)
 | 
						|
 | 
						|
	badKeyCheckFunc := func(ctx context.Context, keyHash []byte) (bool, error) {
 | 
						|
		return false, fmt.Errorf("oh no: %w", goodkey.ErrBadKey)
 | 
						|
	}
 | 
						|
 | 
						|
	kp, err = goodkey.NewKeyPolicy(&goodkey.Config{}, badKeyCheckFunc)
 | 
						|
	test.AssertNotError(t, err, "making key policy")
 | 
						|
 | 
						|
	wfe.keyPolicy = kp
 | 
						|
 | 
						|
	_, _, validJWSBody = signer.embeddedJWK(nil, "http://localhost/test", `{"test":"passed"}`)
 | 
						|
	request = makePostRequestWithPath("test", validJWSBody)
 | 
						|
 | 
						|
	_, _, prob = wfe.validSelfAuthenticatedPOST(context.Background(), request)
 | 
						|
	test.AssertEquals(t, prob.Type, probs.BadPublicKeyProblem)
 | 
						|
}
 | 
						|
 | 
						|
func TestValidSelfAuthenticatedPOST(t *testing.T) {
 | 
						|
	wfe, _, signer := setupWFE(t)
 | 
						|
 | 
						|
	_, validKey, validJWSBody := signer.embeddedJWK(nil, "http://localhost/test", `{"test":"passed"}`)
 | 
						|
 | 
						|
	_, _, keyIDJWSBody := signer.byKeyID(1, nil, "http://localhost/test", `{"test":"passed"}`)
 | 
						|
 | 
						|
	testCases := []struct {
 | 
						|
		Name            string
 | 
						|
		Request         *http.Request
 | 
						|
		ExpectedProblem *probs.ProblemDetails
 | 
						|
		ExpectedPayload string
 | 
						|
		ExpectedJWK     *jose.JSONWebKey
 | 
						|
		ErrorStatType   string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			Name:    "Invalid JWS",
 | 
						|
			Request: makePostRequestWithPath("test", "foo"),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "Parse error reading JWS",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSUnmarshalFailed",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:    "JWS with key ID",
 | 
						|
			Request: makePostRequestWithPath("test", keyIDJWSBody),
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "No embedded JWK in JWS header",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "JWSAuthTypeWrong",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:            "Valid JWS",
 | 
						|
			Request:         makePostRequestWithPath("test", validJWSBody),
 | 
						|
			ExpectedPayload: `{"test":"passed"}`,
 | 
						|
			ExpectedJWK:     validKey,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testCases {
 | 
						|
		t.Run(tc.Name, func(t *testing.T) {
 | 
						|
			wfe.stats.joseErrorCount.Reset()
 | 
						|
			outPayload, jwk, prob := wfe.validSelfAuthenticatedPOST(context.Background(), tc.Request)
 | 
						|
			if tc.ExpectedProblem == nil && prob != nil {
 | 
						|
				t.Fatalf("Expected nil problem, got %#v\n", prob)
 | 
						|
			} else if tc.ExpectedProblem == nil {
 | 
						|
				inThumb, _ := tc.ExpectedJWK.Thumbprint(crypto.SHA256)
 | 
						|
				outThumb, _ := jwk.Thumbprint(crypto.SHA256)
 | 
						|
				test.AssertDeepEquals(t, inThumb, outThumb)
 | 
						|
				test.AssertEquals(t, string(outPayload), tc.ExpectedPayload)
 | 
						|
			} else {
 | 
						|
				test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem)
 | 
						|
			}
 | 
						|
			if tc.ErrorStatType != "" {
 | 
						|
				test.AssertMetricWithLabelsEquals(
 | 
						|
					t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestMatchJWSURLs(t *testing.T) {
 | 
						|
	wfe, _, signer := setupWFE(t)
 | 
						|
 | 
						|
	noURLJWS, _, _ := signer.embeddedJWK(nil, "", "")
 | 
						|
	urlAJWS, _, _ := signer.embeddedJWK(nil, "example.com", "")
 | 
						|
	urlBJWS, _, _ := signer.embeddedJWK(nil, "example.org", "")
 | 
						|
 | 
						|
	testCases := []struct {
 | 
						|
		Name            string
 | 
						|
		Outer           *jose.JSONWebSignature
 | 
						|
		Inner           *jose.JSONWebSignature
 | 
						|
		ExpectedProblem *probs.ProblemDetails
 | 
						|
		ErrorStatType   string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			Name:  "Outer JWS without URL",
 | 
						|
			Outer: noURLJWS,
 | 
						|
			Inner: urlAJWS,
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "Outer JWS header parameter 'url' required",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "KeyRolloverOuterJWSNoURL",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:  "Inner JWS without URL",
 | 
						|
			Outer: urlAJWS,
 | 
						|
			Inner: noURLJWS,
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "Inner JWS header parameter 'url' required",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "KeyRolloverInnerJWSNoURL",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:  "Inner and outer JWS without URL",
 | 
						|
			Outer: noURLJWS,
 | 
						|
			Inner: noURLJWS,
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type: probs.MalformedProblem,
 | 
						|
				// The Outer JWS is validated first
 | 
						|
				Detail:     "Outer JWS header parameter 'url' required",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "KeyRolloverOuterJWSNoURL",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:  "Mismatched inner and outer JWS URLs",
 | 
						|
			Outer: urlAJWS,
 | 
						|
			Inner: urlBJWS,
 | 
						|
			ExpectedProblem: &probs.ProblemDetails{
 | 
						|
				Type:       probs.MalformedProblem,
 | 
						|
				Detail:     "Outer JWS 'url' value \"example.com\" does not match inner JWS 'url' value \"example.org\"",
 | 
						|
				HTTPStatus: http.StatusBadRequest,
 | 
						|
			},
 | 
						|
			ErrorStatType: "KeyRolloverMismatchedURLs",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name:  "Matching inner and outer JWS URLs",
 | 
						|
			Outer: urlAJWS,
 | 
						|
			Inner: urlAJWS,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testCases {
 | 
						|
		t.Run(tc.Name, func(t *testing.T) {
 | 
						|
			wfe.stats.joseErrorCount.Reset()
 | 
						|
			prob := wfe.matchJWSURLs(tc.Outer.Signatures[0].Header, tc.Inner.Signatures[0].Header)
 | 
						|
			if prob != nil && tc.ExpectedProblem == nil {
 | 
						|
				t.Errorf("matchJWSURLs failed. Expected no problem, got %#v", prob)
 | 
						|
			} else {
 | 
						|
				test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem)
 | 
						|
			}
 | 
						|
			if tc.ErrorStatType != "" {
 | 
						|
				test.AssertMetricWithLabelsEquals(
 | 
						|
					t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 |