github.com/letsencrypt/boulder@v0.20251208.0/test/integration/cert_storage_failed_test.go (about) 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 "fmt" 13 "os" 14 "os/exec" 15 "strings" 16 "testing" 17 18 "github.com/eggsampler/acme/v3" 19 _ "github.com/go-sql-driver/mysql" 20 21 "github.com/letsencrypt/boulder/core" 22 "github.com/letsencrypt/boulder/revocation" 23 "github.com/letsencrypt/boulder/sa" 24 "github.com/letsencrypt/boulder/test" 25 "github.com/letsencrypt/boulder/test/vars" 26 ) 27 28 // getPrecertByName finds and parses a precertificate using the given hostname. 29 // It returns the most recent one. 30 func getPrecertByName(db *sql.DB, reversedName string) (*x509.Certificate, error) { 31 reversedName = sa.EncodeIssuedName(reversedName) 32 // Find the certificate from the precertificates table. We don't know the serial so 33 // we have to look it up by name. 34 var der []byte 35 rows, err := db.Query(` 36 SELECT der 37 FROM issuedNames JOIN precertificates 38 USING (serial) 39 WHERE reversedName = ? 40 ORDER BY issuedNames.id DESC 41 LIMIT 1 42 `, reversedName) 43 for rows.Next() { 44 err = rows.Scan(&der) 45 if err != nil { 46 return nil, err 47 } 48 } 49 if der == nil { 50 return nil, fmt.Errorf("no precertificate found for %q", reversedName) 51 } 52 53 cert, err := x509.ParseCertificate(der) 54 if err != nil { 55 return nil, err 56 } 57 58 return cert, nil 59 } 60 61 // TestIssuanceCertStorageFailed tests what happens when a storage RPC fails 62 // during issuance. Specifically, it tests that case where we successfully 63 // prepared and stored a linting certificate plus metadata, but failed to store 64 // the corresponding final certificate after issuance completed. 65 // 66 // To do this, we need to mess with the database, because we want to cause 67 // a failure in one specific query, without control ever returning to the 68 // client. Fortunately we can do this with MySQL triggers. 69 // 70 // We also want to make sure we can revoke the precertificate, which we will 71 // assume exists (note that this different from the root program assumption 72 // that a final certificate exists for any precertificate, though it is 73 // similar in spirit). 74 // 75 // Note: For this test to support Vitess, it depends on a trigger being 76 // installed via test/vtcomboserver/install_trigger.sh, since Vitess does not 77 // support creating triggers via normal SQL commands. 78 func TestIssuanceCertStorageFailed(t *testing.T) { 79 os.Setenv("DIRECTORY", "http://boulder.service.consul:4001/directory") 80 81 db, err := sql.Open("mysql", vars.DBConnSAIntegrationFullPerms) 82 test.AssertNotError(t, err, "failed to open db connection") 83 84 if os.Getenv("USE_VITESS") == "false" { 85 // This block is only necessary for ProxySQL + MariaDB and can be 86 // deleted once we're fully migrated to Vitess + MySQL 8, where the 87 // trigger is installed via test/vtcomboserver/install_trigger.sh. 88 89 ctx := context.Background() 90 _, err = db.ExecContext(ctx, `DROP TRIGGER IF EXISTS fail_ready`) 91 test.AssertNotError(t, err, "failed to drop trigger") 92 93 // Make a specific insert into certificates fail, for this test but not others. 94 // To limit the effect to this one test, we make the trigger aware of a specific 95 // hostname used in this test. Since the INSERT to the certificates table 96 // doesn't include the hostname, we look it up in the issuedNames table, keyed 97 // off of the serial. 98 // NOTE: CREATE and DROP TRIGGER do not work in prepared statements. Go's 99 // database/sql will automatically try to use a prepared statement if you pass 100 // any arguments to Exec besides the query itself, so don't do that. 101 _, err = db.ExecContext(ctx, ` 102 CREATE TRIGGER fail_ready 103 BEFORE INSERT ON certificates 104 FOR EACH ROW BEGIN 105 DECLARE reversedName1 VARCHAR(255); 106 SELECT reversedName 107 INTO reversedName1 108 FROM issuedNames 109 WHERE serial = NEW.serial 110 AND reversedName LIKE "com.wantserror.%"; 111 IF reversedName1 != "" THEN 112 SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Pretend there was an error inserting into certificates'; 113 END IF; 114 END 115 `) 116 test.AssertNotError(t, err, "failed to create trigger") 117 118 defer db.ExecContext(ctx, `DROP TRIGGER IF EXISTS fail_ready`) 119 } 120 121 certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 122 test.AssertNotError(t, err, "creating random cert key") 123 124 // ---- Test revocation by serial ---- 125 revokeMeDomain := "revokeme.wantserror.com" 126 // This should fail because the trigger prevented storing the final certificate. 127 _, err = authAndIssue(nil, certKey, []acme.Identifier{{Type: "dns", Value: revokeMeDomain}}, true, "") 128 test.AssertError(t, err, "expected authAndIssue to fail") 129 130 cert, err := getPrecertByName(db, revokeMeDomain) 131 test.AssertNotError(t, err, "failed to get certificate by name") 132 133 // Revoke by invoking admin-revoker 134 config := fmt.Sprintf("%s/%s", os.Getenv("BOULDER_CONFIG_DIR"), "admin.json") 135 output, err := exec.Command( 136 "./bin/admin", 137 "-config", config, 138 "-dry-run=false", 139 "revoke-cert", 140 "-serial", core.SerialToString(cert.SerialNumber), 141 "-reason", "unspecified", 142 ).CombinedOutput() 143 test.AssertNotError(t, err, fmt.Sprintf("revoking via admin-revoker: %s", string(output))) 144 145 waitAndCheckRevoked(t, cert, nil, revocation.Unspecified) 146 147 // ---- Test revocation by key ---- 148 blockMyKeyDomain := "blockmykey.wantserror.com" 149 // This should fail because the trigger prevented storing the final certificate. 150 _, err = authAndIssue(nil, certKey, []acme.Identifier{{Type: "dns", Value: blockMyKeyDomain}}, true, "") 151 test.AssertError(t, err, "expected authAndIssue to fail") 152 153 cert, err = getPrecertByName(db, blockMyKeyDomain) 154 test.AssertNotError(t, err, "failed to get certificate by name") 155 156 // Time to revoke! We'll do it by creating a different, successful certificate 157 // with the same key, then revoking that certificate for keyCompromise. 158 revokeClient, err := makeClient() 159 test.AssertNotError(t, err, "creating second acme client") 160 res, err := authAndIssue(nil, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "") 161 test.AssertNotError(t, err, "issuing second cert") 162 163 err = revokeClient.RevokeCertificate( 164 revokeClient.Account, 165 res.certs[0], 166 certKey, 167 1, 168 ) 169 test.AssertNotError(t, err, "revoking second certificate") 170 171 waitAndCheckRevoked(t, res.certs[0], res.certs[1], revocation.KeyCompromise) 172 waitAndCheckRevoked(t, cert, nil, revocation.KeyCompromise) 173 174 // Try to issue again with the same key, expecting an error because of the key is blocked. 175 _, err = authAndIssue(nil, certKey, []acme.Identifier{{Type: "dns", Value: "123.example.com"}}, true, "") 176 test.AssertError(t, err, "expected authAndIssue to fail") 177 if !strings.Contains(err.Error(), "public key is forbidden") { 178 t.Errorf("expected issuance to be rejected with a bad pubkey") 179 } 180 }