boulder/test/integration/errors_test.go

325 lines
11 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build integration
package integration
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"testing"
"github.com/eggsampler/acme/v3"
"github.com/go-jose/go-jose/v4"
"github.com/letsencrypt/boulder/test"
)
// TestTooBigOrderError tests that submitting an order with more than 100
// identifiers produces the expected problem result.
func TestTooBigOrderError(t *testing.T) {
t.Parallel()
var idents []acme.Identifier
for i := range 101 {
idents = append(idents, acme.Identifier{Type: "dns", Value: fmt.Sprintf("%d.example.com", i)})
}
_, err := authAndIssue(nil, nil, idents, true, "")
test.AssertError(t, err, "authAndIssue failed")
var prob acme.Problem
test.AssertErrorWraps(t, err, &prob)
test.AssertEquals(t, prob.Type, "urn:ietf:params:acme:error:malformed")
test.AssertContains(t, prob.Detail, "Order cannot contain more than 100 identifiers")
}
// TestAccountEmailError tests that registering a new account, or updating an
// account, with invalid contact information produces the expected problem
// result to ACME clients.
func TestAccountEmailError(t *testing.T) {
t.Parallel()
// The registrations.contact field is VARCHAR(191). 175 'a' characters plus
// the prefix "mailto:" and the suffix "@a.com" makes exactly 191 bytes of
// encoded JSON. The correct size to hit our maximum DB field length.
var longStringBuf strings.Builder
longStringBuf.WriteString("mailto:")
for range 175 {
longStringBuf.WriteRune('a')
}
longStringBuf.WriteString("@a.com")
createErrorPrefix := "Error creating new account :: "
updateErrorPrefix := "Unable to update account :: invalid contact: "
testCases := []struct {
name string
contacts []string
expectedProbType string
expectedProbDetail string
}{
{
name: "empty contact",
contacts: []string{"mailto:valid@valid.com", ""},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: `empty contact`,
},
{
name: "empty proto",
contacts: []string{"mailto:valid@valid.com", " "},
expectedProbType: "urn:ietf:params:acme:error:unsupportedContact",
expectedProbDetail: `only contact scheme 'mailto:' is supported`,
},
{
name: "empty mailto",
contacts: []string{"mailto:valid@valid.com", "mailto:"},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: `unable to parse email address`,
},
{
name: "non-ascii mailto",
contacts: []string{"mailto:valid@valid.com", "mailto:cpu@l̴etsencrypt.org"},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: `contact email contains non-ASCII characters`,
},
{
name: "too many contacts",
contacts: []string{"a", "b", "c", "d"},
expectedProbType: "urn:ietf:params:acme:error:malformed",
expectedProbDetail: `too many contacts provided: 4 > 3`,
},
{
name: "invalid contact",
contacts: []string{"mailto:valid@valid.com", "mailto:a@"},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: `unable to parse email address`,
},
{
name: "forbidden contact domain",
contacts: []string{"mailto:valid@valid.com", "mailto:a@example.com"},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: "contact email has forbidden domain \"example.com\"",
},
{
name: "contact domain invalid TLD",
contacts: []string{"mailto:valid@valid.com", "mailto:a@example.cpu"},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: `contact email has invalid domain: Domain name does not end with a valid public suffix (TLD)`,
},
{
name: "contact domain invalid",
contacts: []string{"mailto:valid@valid.com", "mailto:a@example./.com"},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: "contact email has invalid domain: Domain name contains an invalid character",
},
{
name: "too long contact",
contacts: []string{
longStringBuf.String(),
},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: `too many/too long contact(s). Please use shorter or fewer email addresses`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// First try registering a new account and ensuring the expected problem occurs
var prob acme.Problem
_, err := makeClient(tc.contacts...)
if err != nil {
test.AssertErrorWraps(t, err, &prob)
test.AssertEquals(t, prob.Type, tc.expectedProbType)
test.AssertEquals(t, prob.Detail, createErrorPrefix+tc.expectedProbDetail)
} else {
t.Errorf("expected %s type problem for %q, got nil",
tc.expectedProbType, strings.Join(tc.contacts, ","))
}
// Next try making a client with a good contact and updating with the test
// case contact info. The same problem should occur.
c, err := makeClient("mailto:valid@valid.com")
test.AssertNotError(t, err, "failed to create account with valid contact")
_, err = c.UpdateAccount(c.Account, tc.contacts...)
if err != nil {
test.AssertErrorWraps(t, err, &prob)
test.AssertEquals(t, prob.Type, tc.expectedProbType)
test.AssertEquals(t, prob.Detail, updateErrorPrefix+tc.expectedProbDetail)
} else {
t.Errorf("expected %s type problem after updating account to %q, got nil",
tc.expectedProbType, strings.Join(tc.contacts, ","))
}
})
}
}
func TestRejectedIdentifier(t *testing.T) {
t.Parallel()
// When a single malformed name is provided, we correctly reject it.
idents := []acme.Identifier{
{Type: "dns", Value: "яџХ6яяdь}"},
}
_, err := authAndIssue(nil, nil, idents, true, "")
test.AssertError(t, err, "issuance should fail for one malformed name")
var prob acme.Problem
test.AssertErrorWraps(t, err, &prob)
test.AssertEquals(t, prob.Type, "urn:ietf:params:acme:error:rejectedIdentifier")
test.AssertContains(t, prob.Detail, "Domain name contains an invalid character")
// When multiple malformed names are provided, we correctly reject all of
// them and reflect this in suberrors. This test ensures that the way we
// encode these errors across the gRPC boundary is resilient to non-ascii
// characters.
idents = []acme.Identifier{
{Type: "dns", Value: "˜o-"},
{Type: "dns", Value: "ш№Ў"},
{Type: "dns", Value: "р±y"},
{Type: "dns", Value: "яџХ6яя"},
{Type: "dns", Value: "яџХ6яя`ь"},
}
_, err = authAndIssue(nil, nil, idents, true, "")
test.AssertError(t, err, "issuance should fail for multiple malformed names")
test.AssertErrorWraps(t, err, &prob)
test.AssertEquals(t, prob.Type, "urn:ietf:params:acme:error:rejectedIdentifier")
test.AssertContains(t, prob.Detail, "Domain name contains an invalid character")
test.AssertContains(t, prob.Detail, "and 4 more problems")
}
// TestBadSignatureAlgorithm tests that supplying an unacceptable value for the
// "alg" field of the JWS Protected Header results in a problem document with
// the set of acceptable "alg" values listed in a custom extension field named
// "algorithms". Creating a request with an unacceptable "alg" field requires
// us to do some shenanigans.
func TestBadSignatureAlgorithm(t *testing.T) {
t.Parallel()
client, err := makeClient()
if err != nil {
t.Fatal("creating test client")
}
header, err := json.Marshal(&struct {
Alg string `json:"alg"`
KID string `json:"kid"`
Nonce string `json:"nonce"`
URL string `json:"url"`
}{
Alg: string(jose.RS512), // This is the important bit; RS512 is unacceptable.
KID: client.Account.URL,
Nonce: "deadbeef", // This nonce would fail, but that check comes after the alg check.
URL: client.Directory().NewAccount,
})
if err != nil {
t.Fatalf("creating JWS protected header: %s", err)
}
protected := base64.RawURLEncoding.EncodeToString(header)
payload := base64.RawURLEncoding.EncodeToString([]byte(`{"onlyReturnExisting": true}`))
hash := crypto.SHA512.New()
hash.Write([]byte(protected + "." + payload))
sig, err := client.Account.PrivateKey.Sign(rand.Reader, hash.Sum(nil), crypto.SHA512)
if err != nil {
t.Fatalf("creating fake signature: %s", err)
}
data, err := json.Marshal(&struct {
Protected string `json:"protected"`
Payload string `json:"payload"`
Signature string `json:"signature"`
}{
Protected: protected,
Payload: payload,
Signature: base64.RawURLEncoding.EncodeToString(sig),
})
req, err := http.NewRequest(http.MethodPost, client.Directory().NewAccount, bytes.NewReader(data))
if err != nil {
t.Fatalf("creating HTTP request: %s", err)
}
req.Header.Set("Content-Type", "application/jose+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("making HTTP request: %s", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("reading HTTP response: %s", err)
}
var prob struct {
Type string `json:"type"`
Detail string `json:"detail"`
Status int `json:"status"`
Algorithms []jose.SignatureAlgorithm `json:"algorithms"`
}
err = json.Unmarshal(body, &prob)
if err != nil {
t.Fatalf("parsing HTTP response: %s", err)
}
if prob.Type != "urn:ietf:params:acme:error:badSignatureAlgorithm" {
t.Errorf("problem document has wrong type: want badSignatureAlgorithm, got %s", prob.Type)
}
if prob.Status != http.StatusBadRequest {
t.Errorf("problem document has wrong status: want 400, got %d", prob.Status)
}
if len(prob.Algorithms) == 0 {
t.Error("problem document MUST contain acceptable algorithms, got none")
}
}
// TestOrderFinalizeEarly tests that finalizing an order before it is fully
// authorized results in an orderNotReady error.
func TestOrderFinalizeEarly(t *testing.T) {
t.Parallel()
client, err := makeClient()
if err != nil {
t.Fatalf("creating acme client: %s", err)
}
idents := []acme.Identifier{{Type: "dns", Value: randomDomain(t)}}
order, err := client.Client.NewOrder(client.Account, idents)
if err != nil {
t.Fatalf("creating order: %s", err)
}
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("generating key: %s", err)
}
csr, err := makeCSR(key, idents, false)
if err != nil {
t.Fatalf("generating CSR: %s", err)
}
order, err = client.Client.FinalizeOrder(client.Account, order, csr)
if err == nil {
t.Fatal("expected finalize to fail, but got success")
}
var prob acme.Problem
ok := errors.As(err, &prob)
if !ok {
t.Fatalf("expected error to be of type acme.Problem, got: %T", err)
}
if prob.Type != "urn:ietf:params:acme:error:orderNotReady" {
t.Errorf("expected problem type 'urn:ietf:params:acme:error:orderNotReady', got: %s", prob.Type)
}
if order.Status != "pending" {
t.Errorf("expected order status to be pending, got: %s", order.Status)
}
}