//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) } }