github.com/letsencrypt/boulder@v0.20251208.0/test/integration/errors_test.go (about)

     1  //go:build integration
     2  
     3  package integration
     4  
     5  import (
     6  	"bytes"
     7  	"crypto"
     8  	"crypto/ecdsa"
     9  	"crypto/elliptic"
    10  	"crypto/rand"
    11  	"encoding/base64"
    12  	"encoding/json"
    13  	"errors"
    14  	"fmt"
    15  	"io"
    16  	"net/http"
    17  	"slices"
    18  	"strings"
    19  	"testing"
    20  
    21  	"github.com/eggsampler/acme/v3"
    22  	"github.com/go-jose/go-jose/v4"
    23  
    24  	"github.com/letsencrypt/boulder/test"
    25  )
    26  
    27  // TestTooBigOrderError tests that submitting an order with more than 100
    28  // identifiers produces the expected problem result.
    29  func TestTooBigOrderError(t *testing.T) {
    30  	t.Parallel()
    31  
    32  	var idents []acme.Identifier
    33  	for i := range 101 {
    34  		idents = append(idents, acme.Identifier{Type: "dns", Value: fmt.Sprintf("%d.example.com", i)})
    35  	}
    36  
    37  	_, err := authAndIssue(nil, nil, idents, true, "")
    38  	test.AssertError(t, err, "authAndIssue failed")
    39  
    40  	var prob acme.Problem
    41  	test.AssertErrorWraps(t, err, &prob)
    42  	test.AssertEquals(t, prob.Type, "urn:ietf:params:acme:error:malformed")
    43  	test.AssertContains(t, prob.Detail, "Order cannot contain more than 100 identifiers")
    44  }
    45  
    46  // TestAccountEmailError tests that registering a new account, or updating an
    47  // account, with invalid contact information produces the expected problem
    48  // result to ACME clients.
    49  func TestAccountEmailError(t *testing.T) {
    50  	t.Parallel()
    51  
    52  	testCases := []struct {
    53  		name               string
    54  		contacts           []string
    55  		expectedProbType   string
    56  		expectedProbDetail string
    57  	}{
    58  		{
    59  			name:               "empty contact",
    60  			contacts:           []string{"mailto:valid@valid.com", ""},
    61  			expectedProbType:   "urn:ietf:params:acme:error:invalidContact",
    62  			expectedProbDetail: `empty contact`,
    63  		},
    64  		{
    65  			name:               "empty proto",
    66  			contacts:           []string{"mailto:valid@valid.com", " "},
    67  			expectedProbType:   "urn:ietf:params:acme:error:unsupportedContact",
    68  			expectedProbDetail: `only contact scheme 'mailto:' is supported`,
    69  		},
    70  		{
    71  			name:               "empty mailto",
    72  			contacts:           []string{"mailto:valid@valid.com", "mailto:"},
    73  			expectedProbType:   "urn:ietf:params:acme:error:invalidContact",
    74  			expectedProbDetail: `unable to parse email address`,
    75  		},
    76  		{
    77  			name:               "non-ascii mailto",
    78  			contacts:           []string{"mailto:valid@valid.com", "mailto:cpu@l̴etsencrypt.org"},
    79  			expectedProbType:   "urn:ietf:params:acme:error:invalidContact",
    80  			expectedProbDetail: `contact email contains non-ASCII characters`,
    81  		},
    82  		{
    83  			name:               "too many contacts",
    84  			contacts:           slices.Repeat([]string{"mailto:lots@valid.com"}, 11),
    85  			expectedProbType:   "urn:ietf:params:acme:error:malformed",
    86  			expectedProbDetail: `too many contacts provided`,
    87  		},
    88  		{
    89  			name:               "invalid contact",
    90  			contacts:           []string{"mailto:valid@valid.com", "mailto:a@"},
    91  			expectedProbType:   "urn:ietf:params:acme:error:invalidContact",
    92  			expectedProbDetail: `unable to parse email address`,
    93  		},
    94  		{
    95  			name:               "forbidden contact domain",
    96  			contacts:           []string{"mailto:valid@valid.com", "mailto:a@example.com"},
    97  			expectedProbType:   "urn:ietf:params:acme:error:invalidContact",
    98  			expectedProbDetail: "contact email has forbidden domain \"example.com\"",
    99  		},
   100  		{
   101  			name:               "contact domain invalid TLD",
   102  			contacts:           []string{"mailto:valid@valid.com", "mailto:a@example.cpu"},
   103  			expectedProbType:   "urn:ietf:params:acme:error:invalidContact",
   104  			expectedProbDetail: `contact email has invalid domain: Domain name does not end with a valid public suffix (TLD)`,
   105  		},
   106  		{
   107  			name:               "contact domain invalid",
   108  			contacts:           []string{"mailto:valid@valid.com", "mailto:a@example./.com"},
   109  			expectedProbType:   "urn:ietf:params:acme:error:invalidContact",
   110  			expectedProbDetail: "contact email has invalid domain: Domain name contains an invalid character",
   111  		},
   112  	}
   113  
   114  	for _, tc := range testCases {
   115  		t.Run(tc.name, func(t *testing.T) {
   116  			var prob acme.Problem
   117  			_, err := makeClient(tc.contacts...)
   118  			if err != nil {
   119  				test.AssertErrorWraps(t, err, &prob)
   120  				test.AssertEquals(t, prob.Type, tc.expectedProbType)
   121  				test.AssertContains(t, prob.Detail, "Error validating contact(s)")
   122  				test.AssertContains(t, prob.Detail, tc.expectedProbDetail)
   123  			} else {
   124  				t.Errorf("expected %s type problem for %q, got nil",
   125  					tc.expectedProbType, strings.Join(tc.contacts, ","))
   126  			}
   127  		})
   128  	}
   129  }
   130  
   131  func TestRejectedIdentifier(t *testing.T) {
   132  	t.Parallel()
   133  
   134  	// When a single malformed name is provided, we correctly reject it.
   135  	idents := []acme.Identifier{
   136  		{Type: "dns", Value: "яџ–Х6яяdь}"},
   137  	}
   138  	_, err := authAndIssue(nil, nil, idents, true, "")
   139  	test.AssertError(t, err, "issuance should fail for one malformed name")
   140  	var prob acme.Problem
   141  	test.AssertErrorWraps(t, err, &prob)
   142  	test.AssertEquals(t, prob.Type, "urn:ietf:params:acme:error:rejectedIdentifier")
   143  	test.AssertContains(t, prob.Detail, "Domain name contains an invalid character")
   144  
   145  	// When multiple malformed names are provided, we correctly reject all of
   146  	// them and reflect this in suberrors. This test ensures that the way we
   147  	// encode these errors across the gRPC boundary is resilient to non-ascii
   148  	// characters.
   149  	idents = []acme.Identifier{
   150  		{Type: "dns", Value: "˜o-"},
   151  		{Type: "dns", Value: "ш№Ў"},
   152  		{Type: "dns", Value: "р±y"},
   153  		{Type: "dns", Value: "яџ–Х6яя"},
   154  		{Type: "dns", Value: "яџ–Х6яя`ь"},
   155  	}
   156  	_, err = authAndIssue(nil, nil, idents, true, "")
   157  	test.AssertError(t, err, "issuance should fail for multiple malformed names")
   158  	test.AssertErrorWraps(t, err, &prob)
   159  	test.AssertEquals(t, prob.Type, "urn:ietf:params:acme:error:rejectedIdentifier")
   160  	test.AssertContains(t, prob.Detail, "Domain name contains an invalid character")
   161  	test.AssertContains(t, prob.Detail, "and 4 more problems")
   162  }
   163  
   164  // TestBadSignatureAlgorithm tests that supplying an unacceptable value for the
   165  // "alg" field of the JWS Protected Header results in a problem document with
   166  // the set of acceptable "alg" values listed in a custom extension field named
   167  // "algorithms". Creating a request with an unacceptable "alg" field requires
   168  // us to do some shenanigans.
   169  func TestBadSignatureAlgorithm(t *testing.T) {
   170  	t.Parallel()
   171  
   172  	client, err := makeClient()
   173  	if err != nil {
   174  		t.Fatal("creating test client")
   175  	}
   176  
   177  	header, err := json.Marshal(&struct {
   178  		Alg   string `json:"alg"`
   179  		KID   string `json:"kid"`
   180  		Nonce string `json:"nonce"`
   181  		URL   string `json:"url"`
   182  	}{
   183  		Alg:   string(jose.RS512), // This is the important bit; RS512 is unacceptable.
   184  		KID:   client.Account.URL,
   185  		Nonce: "deadbeef", // This nonce would fail, but that check comes after the alg check.
   186  		URL:   client.Directory().NewAccount,
   187  	})
   188  	if err != nil {
   189  		t.Fatalf("creating JWS protected header: %s", err)
   190  	}
   191  	protected := base64.RawURLEncoding.EncodeToString(header)
   192  
   193  	payload := base64.RawURLEncoding.EncodeToString([]byte(`{"onlyReturnExisting": true}`))
   194  	hash := crypto.SHA512.New()
   195  	hash.Write([]byte(protected + "." + payload))
   196  	sig, err := client.Account.PrivateKey.Sign(rand.Reader, hash.Sum(nil), crypto.SHA512)
   197  	if err != nil {
   198  		t.Fatalf("creating fake signature: %s", err)
   199  	}
   200  
   201  	data, err := json.Marshal(&struct {
   202  		Protected string `json:"protected"`
   203  		Payload   string `json:"payload"`
   204  		Signature string `json:"signature"`
   205  	}{
   206  		Protected: protected,
   207  		Payload:   payload,
   208  		Signature: base64.RawURLEncoding.EncodeToString(sig),
   209  	})
   210  
   211  	req, err := http.NewRequest(http.MethodPost, client.Directory().NewAccount, bytes.NewReader(data))
   212  	if err != nil {
   213  		t.Fatalf("creating HTTP request: %s", err)
   214  	}
   215  	req.Header.Set("Content-Type", "application/jose+json")
   216  
   217  	resp, err := http.DefaultClient.Do(req)
   218  	if err != nil {
   219  		t.Fatalf("making HTTP request: %s", err)
   220  	}
   221  	defer resp.Body.Close()
   222  
   223  	body, err := io.ReadAll(resp.Body)
   224  	if err != nil {
   225  		t.Fatalf("reading HTTP response: %s", err)
   226  	}
   227  
   228  	var prob struct {
   229  		Type       string                    `json:"type"`
   230  		Detail     string                    `json:"detail"`
   231  		Status     int                       `json:"status"`
   232  		Algorithms []jose.SignatureAlgorithm `json:"algorithms"`
   233  	}
   234  	err = json.Unmarshal(body, &prob)
   235  	if err != nil {
   236  		t.Fatalf("parsing HTTP response: %s", err)
   237  	}
   238  
   239  	if prob.Type != "urn:ietf:params:acme:error:badSignatureAlgorithm" {
   240  		t.Errorf("problem document has wrong type: want badSignatureAlgorithm, got %s", prob.Type)
   241  	}
   242  	if prob.Status != http.StatusBadRequest {
   243  		t.Errorf("problem document has wrong status: want 400, got %d", prob.Status)
   244  	}
   245  	if len(prob.Algorithms) == 0 {
   246  		t.Error("problem document MUST contain acceptable algorithms, got none")
   247  	}
   248  }
   249  
   250  // TestOrderFinalizeEarly tests that finalizing an order before it is fully
   251  // authorized results in an orderNotReady error.
   252  func TestOrderFinalizeEarly(t *testing.T) {
   253  	t.Parallel()
   254  
   255  	client, err := makeClient()
   256  	if err != nil {
   257  		t.Fatalf("creating acme client: %s", err)
   258  	}
   259  
   260  	idents := []acme.Identifier{{Type: "dns", Value: randomDomain(t)}}
   261  
   262  	order, err := client.Client.NewOrder(client.Account, idents)
   263  	if err != nil {
   264  		t.Fatalf("creating order: %s", err)
   265  	}
   266  	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   267  	if err != nil {
   268  		t.Fatalf("generating key: %s", err)
   269  	}
   270  	csr, err := makeCSR(key, idents, false)
   271  	if err != nil {
   272  		t.Fatalf("generating CSR: %s", err)
   273  	}
   274  
   275  	order, err = client.Client.FinalizeOrder(client.Account, order, csr)
   276  	if err == nil {
   277  		t.Fatal("expected finalize to fail, but got success")
   278  	}
   279  	var prob acme.Problem
   280  	ok := errors.As(err, &prob)
   281  	if !ok {
   282  		t.Fatalf("expected error to be of type acme.Problem, got: %T", err)
   283  	}
   284  	if prob.Type != "urn:ietf:params:acme:error:orderNotReady" {
   285  		t.Errorf("expected problem type 'urn:ietf:params:acme:error:orderNotReady', got: %s", prob.Type)
   286  	}
   287  	if order.Status != "pending" {
   288  		t.Errorf("expected order status to be pending, got: %s", order.Status)
   289  	}
   290  }