...

Source file src/github.com/letsencrypt/boulder/test/integration/cert_storage_failed_test.go

Documentation: github.com/letsencrypt/boulder/test/integration

     1  //go:build integration
     2  
     3  package integration
     4  
     5  import (
     6  	"context"
     7  	"crypto/ecdsa"
     8  	"crypto/elliptic"
     9  	"crypto/rand"
    10  	"crypto/x509"
    11  	"database/sql"
    12  	"errors"
    13  	"fmt"
    14  	"os"
    15  	"os/exec"
    16  	"strings"
    17  	"testing"
    18  	"time"
    19  
    20  	_ "github.com/go-sql-driver/mysql"
    21  	"github.com/letsencrypt/boulder/core"
    22  	"github.com/letsencrypt/boulder/sa"
    23  	"github.com/letsencrypt/boulder/test"
    24  	ocsp_helper "github.com/letsencrypt/boulder/test/ocsp/helper"
    25  	"github.com/letsencrypt/boulder/test/vars"
    26  	"golang.org/x/crypto/ocsp"
    27  )
    28  
    29  // getPrecertByName finds and parses a precertificate using the given hostname.
    30  // It returns the most recent one.
    31  func getPrecertByName(db *sql.DB, name string) (*x509.Certificate, error) {
    32  	name = sa.ReverseName(name)
    33  	// Find the certificate from the precertificates table. We don't know the serial so
    34  	// we have to look it up by name.
    35  	var der []byte
    36  	rows, err := db.Query(`
    37  		SELECT der
    38  		FROM issuedNames JOIN precertificates
    39  		USING (serial)
    40  		WHERE reversedName = ?
    41  		ORDER BY issuedNames.id DESC
    42  		LIMIT 1
    43  	`, name)
    44  	for rows.Next() {
    45  		err = rows.Scan(&der)
    46  		if err != nil {
    47  			return nil, err
    48  		}
    49  	}
    50  	if der == nil {
    51  		return nil, fmt.Errorf("no precertificate found for %q", name)
    52  	}
    53  
    54  	cert, err := x509.ParseCertificate(der)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	return cert, nil
    60  }
    61  
    62  // expectOCSP500 queries OCSP for the given certificate and expects a 500 error.
    63  func expectOCSP500(cert *x509.Certificate) error {
    64  	_, err := ocsp_helper.Req(cert, ocsp_helper.DefaultConfig)
    65  	if err == nil {
    66  		return errors.New("Expected error getting OCSP for certificate that failed status storage")
    67  	}
    68  
    69  	var statusCodeError ocsp_helper.StatusCodeError
    70  	if !errors.As(err, &statusCodeError) {
    71  		return fmt.Errorf("Got wrong kind of error for OCSP. Expected status code error, got %s", err)
    72  	} else if statusCodeError.Code != 500 {
    73  		return fmt.Errorf("Got wrong error status for OCSP. Expected 500, got %d", statusCodeError.Code)
    74  	}
    75  	return nil
    76  }
    77  
    78  // TestIssuanceCertStorageFailed tests what happens when a storage RPC fails
    79  // during issuance. Specifically, it tests that case where we successfully
    80  // prepared and stored a linting certificate plus metadata, but after
    81  // issuing the precertificate we failed to mark the certificate as "ready"
    82  // to serve an OCSP "good" response.
    83  //
    84  // To do this, we need to mess with the database, because we want to cause
    85  // a failure in one specific query, without control ever returning to the
    86  // client. Fortunately we can do this with MySQL triggers.
    87  //
    88  // We also want to make sure we can revoke the precertificate, which we will
    89  // assume exists (note that this different from the root program assumption
    90  // that a final certificate exists for any precertificate, though it is
    91  // similar in spirit).
    92  func TestIssuanceCertStorageFailed(t *testing.T) {
    93  	t.Parallel()
    94  	os.Setenv("DIRECTORY", "http://boulder.service.consul:4001/directory")
    95  
    96  	ctx := context.Background()
    97  
    98  	// This test is gated on the StoreLintingCertificateInsteadOfPrecertificate
    99  	// feature flag.
   100  	if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" {
   101  		t.Skip("Skipping test because it requires the StoreLintingCertificateInsteadOfPrecertificate feature flag")
   102  	}
   103  
   104  	db, err := sql.Open("mysql", vars.DBConnSAIntegrationFullPerms)
   105  	test.AssertNotError(t, err, "failed to open db connection")
   106  
   107  	_, err = db.ExecContext(ctx, `DROP TRIGGER IF EXISTS fail_ready`)
   108  	test.AssertNotError(t, err, "failed to drop trigger")
   109  
   110  	// Make a specific update to certificateStatus fail, for this test but not others.
   111  	// To limit the effect to this one test, we make the trigger aware of a specific
   112  	// hostname used in this test. Since the UPDATE to the certificateStatus table
   113  	// doesn't include the hostname, we look it up in the issuedNames table, keyed
   114  	// off of the serial being updated.
   115  	// We limit this to UPDATEs that set the status to "good" because otherwise we
   116  	// would fail to revoke the certificate later.
   117  	// NOTE: CREATE and DROP TRIGGER do not work in prepared statements. Go's
   118  	// database/sql will automatically try to use a prepared statement if you pass
   119  	// any arguments to Exec besides the query itself, so don't do that.
   120  	_, err = db.ExecContext(ctx, `
   121  		CREATE TRIGGER fail_ready
   122  		BEFORE UPDATE ON certificateStatus
   123  		FOR EACH ROW BEGIN
   124  		DECLARE reversedName1 VARCHAR(255);
   125  		SELECT reversedName
   126  		    INTO reversedName1
   127  			FROM issuedNames
   128  			WHERE serial = NEW.serial
   129  			    AND reversedName LIKE "com.wantserror.%";
   130  		IF NEW.status = "good" AND reversedName1 != "" THEN
   131  			SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Pretend there was an error updating the certificateStatus';
   132  		END IF;
   133  		END
   134  	`)
   135  	test.AssertNotError(t, err, "failed to create trigger")
   136  
   137  	defer db.ExecContext(ctx, `DROP TRIGGER IF EXISTS fail_ready`)
   138  
   139  	certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   140  	test.AssertNotError(t, err, "creating random cert key")
   141  
   142  	// ---- Test revocation by serial ----
   143  	revokeMeDomain := "revokeme.wantserror.com"
   144  	// This should fail because the trigger prevented setting the certificate status to "ready"
   145  	_, err = authAndIssue(nil, certKey, []string{revokeMeDomain}, true)
   146  	test.AssertError(t, err, "expected authAndIssue to fail")
   147  
   148  	cert, err := getPrecertByName(db, revokeMeDomain)
   149  	test.AssertNotError(t, err, "failed to get certificate by name")
   150  
   151  	err = expectOCSP500(cert)
   152  	test.AssertNotError(t, err, "expected 500 error from OCSP")
   153  
   154  	// Revoke by invoking admin-revoker
   155  	config := fmt.Sprintf("%s/%s", os.Getenv("BOULDER_CONFIG_DIR"), "admin-revoker.json")
   156  	output, err := exec.Command("./bin/admin-revoker", "serial-revoke",
   157  		"-config", config,
   158  		core.SerialToString(cert.SerialNumber),
   159  		"0").CombinedOutput()
   160  	test.AssertNotError(t, err, fmt.Sprintf("revoking via admin-revoker: %s", string(output)))
   161  
   162  	_, err = ocsp_helper.Req(cert,
   163  		ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked).WithExpectReason(ocsp.Unspecified))
   164  
   165  	// ---- Test revocation by key ----
   166  	blockMyKeyDomain := "blockmykey.wantserror.com"
   167  	// This should fail because the trigger prevented setting the certificate status to "ready"
   168  	_, err = authAndIssue(nil, certKey, []string{blockMyKeyDomain}, true)
   169  	test.AssertError(t, err, "expected authAndIssue to fail")
   170  
   171  	cert, err = getPrecertByName(db, blockMyKeyDomain)
   172  	test.AssertNotError(t, err, "failed to get certificate by name")
   173  
   174  	err = expectOCSP500(cert)
   175  	test.AssertNotError(t, err, "expected 500 error from OCSP")
   176  
   177  	// Time to revoke! We'll do it by creating a different, successful certificate
   178  	// with the same key, then revoking that certificate for keyCompromise.
   179  	revokeClient, err := makeClient()
   180  	test.AssertNotError(t, err, "creating second acme client")
   181  	res, err := authAndIssue(nil, certKey, []string{random_domain()}, true)
   182  	test.AssertNotError(t, err, "issuing second cert")
   183  
   184  	successfulCert := res.certs[0]
   185  	err = revokeClient.RevokeCertificate(
   186  		revokeClient.Account,
   187  		successfulCert,
   188  		certKey,
   189  		1,
   190  	)
   191  	test.AssertNotError(t, err, "revoking second certificate")
   192  
   193  	for i := 0; i < 300; i++ {
   194  		_, err = ocsp_helper.Req(successfulCert,
   195  			ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked).WithExpectReason(ocsp.KeyCompromise))
   196  		if err == nil {
   197  			break
   198  		}
   199  		time.Sleep(15 * time.Millisecond)
   200  	}
   201  	test.AssertNotError(t, err, "expected status to eventually become revoked")
   202  
   203  	// Try to issue again with the same key, expecting an error because of the key is blocked.
   204  	_, err = authAndIssue(nil, certKey, []string{"123.example.com"}, true)
   205  	test.AssertError(t, err, "expected authAndIssue to fail")
   206  	if !strings.Contains(err.Error(), "public key is forbidden") {
   207  		t.Errorf("expected issuance to be rejected with a bad pubkey")
   208  	}
   209  }
   210  

View as plain text