291 lines
9.6 KiB
Go
291 lines
9.6 KiB
Go
//go:build integration
|
||
|
||
package integration
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto"
|
||
"crypto/ecdsa"
|
||
"crypto/elliptic"
|
||
"crypto/rand"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"slices"
|
||
"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()
|
||
|
||
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: slices.Repeat([]string{"mailto:lots@valid.com"}, 11),
|
||
expectedProbType: "urn:ietf:params:acme:error:malformed",
|
||
expectedProbDetail: `too many contacts provided`,
|
||
},
|
||
{
|
||
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",
|
||
},
|
||
}
|
||
|
||
for _, tc := range testCases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
var prob acme.Problem
|
||
_, err := makeClient(tc.contacts...)
|
||
if err != nil {
|
||
test.AssertErrorWraps(t, err, &prob)
|
||
test.AssertEquals(t, prob.Type, tc.expectedProbType)
|
||
test.AssertContains(t, prob.Detail, "Error validating contact(s)")
|
||
test.AssertContains(t, prob.Detail, tc.expectedProbDetail)
|
||
} else {
|
||
t.Errorf("expected %s type problem for %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)
|
||
}
|
||
}
|