github.com/letsencrypt/boulder@v0.20251208.0/cmd/admin/key_test.go (about) 1 package main 2 3 import ( 4 "context" 5 "crypto/ecdsa" 6 "crypto/elliptic" 7 "crypto/rand" 8 "crypto/sha256" 9 "crypto/x509" 10 "encoding/hex" 11 "encoding/pem" 12 "os" 13 "os/user" 14 "path" 15 "strconv" 16 "strings" 17 "testing" 18 "time" 19 20 "github.com/jmhodges/clock" 21 "google.golang.org/grpc" 22 "google.golang.org/protobuf/types/known/emptypb" 23 24 "github.com/letsencrypt/boulder/core" 25 blog "github.com/letsencrypt/boulder/log" 26 "github.com/letsencrypt/boulder/mocks" 27 sapb "github.com/letsencrypt/boulder/sa/proto" 28 "github.com/letsencrypt/boulder/test" 29 ) 30 31 func TestSPKIHashFromPrivateKey(t *testing.T) { 32 privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 33 test.AssertNotError(t, err, "creating test private key") 34 keyHash, err := core.KeyDigest(privKey.Public()) 35 test.AssertNotError(t, err, "computing test SPKI hash") 36 37 keyBytes, err := x509.MarshalPKCS8PrivateKey(privKey) 38 test.AssertNotError(t, err, "marshalling test private key bytes") 39 keyFile := path.Join(t.TempDir(), "key.pem") 40 keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyBytes}) 41 err = os.WriteFile(keyFile, keyPEM, os.ModeAppend) 42 test.AssertNotError(t, err, "writing test private key file") 43 44 a := admin{} 45 46 res, err := a.spkiHashFromPrivateKey(keyFile) 47 test.AssertNotError(t, err, "") 48 test.AssertByteEquals(t, res, keyHash[:]) 49 } 50 51 func TestSPKIHashesFromFile(t *testing.T) { 52 var spkiHexes []string 53 for i := range 10 { 54 h := sha256.Sum256([]byte(strconv.Itoa(i))) 55 spkiHexes = append(spkiHexes, hex.EncodeToString(h[:])) 56 } 57 58 spkiFile := path.Join(t.TempDir(), "spkis.txt") 59 err := os.WriteFile(spkiFile, []byte(strings.Join(spkiHexes, "\n")), os.ModeAppend) 60 test.AssertNotError(t, err, "writing test spki file") 61 62 a := admin{} 63 64 res, err := a.spkiHashesFromFile(spkiFile) 65 test.AssertNotError(t, err, "") 66 for i, spkiHash := range res { 67 test.AssertEquals(t, hex.EncodeToString(spkiHash), spkiHexes[i]) 68 } 69 } 70 71 // The key is the p256 test key from RFC9500 72 const goodCSR = ` 73 -----BEGIN CERTIFICATE REQUEST----- 74 MIG6MGICAQAwADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEIlSPiPt4L/teyj 75 dERSxyoeVY+9b3O+XkjpMjLMRcWxbEzRDEy41bihcTnpSILImSVymTQl9BQZq36Q 76 pCpJQnKgADAKBggqhkjOPQQDAgNIADBFAiBadw3gvL9IjUfASUTa7MvmkbC4ZCvl 77 21m1KMwkIx/+CQIhAKvuyfCcdZ0cWJYOXCOb1OavolWHIUzgEpNGUWul6O0s 78 -----END CERTIFICATE REQUEST----- 79 ` 80 81 // TestCSR checks that we get the correct SPKI from a CSR, even if its signature is invalid 82 func TestCSR(t *testing.T) { 83 expectedSPKIHash := "b2b04340cfaee616ec9c2c62d261b208e54bb197498df52e8cadede23ac0ba5e" 84 85 goodCSRFile := path.Join(t.TempDir(), "good.csr") 86 err := os.WriteFile(goodCSRFile, []byte(goodCSR), 0600) 87 test.AssertNotError(t, err, "writing good csr") 88 89 a := admin{log: blog.NewMock()} 90 91 goodHash, err := a.spkiHashFromCSRPEM(goodCSRFile, true, "") 92 test.AssertNotError(t, err, "expected to read CSR") 93 94 if len(goodHash) != 1 { 95 t.Fatalf("expected to read 1 SPKI from CSR, read %d", len(goodHash)) 96 } 97 test.AssertEquals(t, hex.EncodeToString(goodHash[0]), expectedSPKIHash) 98 99 // Flip a bit, in the signature, to make a bad CSR: 100 badCSR := strings.Replace(goodCSR, "Wul6", "Wul7", 1) 101 102 csrFile := path.Join(t.TempDir(), "bad.csr") 103 err = os.WriteFile(csrFile, []byte(badCSR), 0600) 104 test.AssertNotError(t, err, "writing bad csr") 105 106 _, err = a.spkiHashFromCSRPEM(csrFile, true, "") 107 test.AssertError(t, err, "expected invalid signature") 108 109 badHash, err := a.spkiHashFromCSRPEM(csrFile, false, "") 110 test.AssertNotError(t, err, "expected to read CSR with bad signature") 111 112 if len(badHash) != 1 { 113 t.Fatalf("expected to read 1 SPKI from CSR, read %d", len(badHash)) 114 } 115 test.AssertEquals(t, hex.EncodeToString(badHash[0]), expectedSPKIHash) 116 } 117 118 // mockSARecordingBlocks is a mock which only implements the AddBlockedKey gRPC 119 // method. 120 type mockSARecordingBlocks struct { 121 sapb.StorageAuthorityClient 122 blockRequests []*sapb.AddBlockedKeyRequest 123 } 124 125 // AddBlockedKey is a mock which always succeeds and records the request it 126 // received. 127 func (msa *mockSARecordingBlocks) AddBlockedKey(ctx context.Context, req *sapb.AddBlockedKeyRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { 128 msa.blockRequests = append(msa.blockRequests, req) 129 return &emptypb.Empty{}, nil 130 } 131 132 func (msa *mockSARecordingBlocks) reset() { 133 msa.blockRequests = nil 134 } 135 136 type mockSARO struct { 137 sapb.StorageAuthorityReadOnlyClient 138 } 139 140 func (sa *mockSARO) GetSerialsByKey(ctx context.Context, _ *sapb.SPKIHash, _ ...grpc.CallOption) (grpc.ServerStreamingClient[sapb.Serial], error) { 141 return &mocks.ServerStreamClient[sapb.Serial]{}, nil 142 } 143 144 func (sa *mockSARO) KeyBlocked(ctx context.Context, req *sapb.SPKIHash, _ ...grpc.CallOption) (*sapb.Exists, error) { 145 return &sapb.Exists{Exists: false}, nil 146 } 147 148 func TestBlockSPKIHash(t *testing.T) { 149 fc := clock.NewFake() 150 fc.Set(time.Now()) 151 log := blog.NewMock() 152 msa := mockSARecordingBlocks{} 153 154 privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 155 test.AssertNotError(t, err, "creating test private key") 156 keyHash, err := core.KeyDigest(privKey.Public()) 157 test.AssertNotError(t, err, "computing test SPKI hash") 158 159 a := admin{saroc: &mockSARO{}, sac: &msa, clk: fc, log: log} 160 u := &user.User{} 161 162 // A full run should result in one request with the right fields. 163 msa.reset() 164 log.Clear() 165 a.dryRun = false 166 err = a.blockSPKIHash(context.Background(), keyHash[:], u, "hello world") 167 test.AssertNotError(t, err, "") 168 test.AssertEquals(t, len(log.GetAllMatching("Found 0 unexpired certificates")), 1) 169 test.AssertEquals(t, len(msa.blockRequests), 1) 170 test.AssertByteEquals(t, msa.blockRequests[0].KeyHash, keyHash[:]) 171 test.AssertContains(t, msa.blockRequests[0].Comment, "hello world") 172 173 // A dry-run should result in zero requests and two log lines. 174 msa.reset() 175 log.Clear() 176 a.dryRun = true 177 a.sac = dryRunSAC{log: log} 178 err = a.blockSPKIHash(context.Background(), keyHash[:], u, "") 179 test.AssertNotError(t, err, "") 180 test.AssertEquals(t, len(log.GetAllMatching("Found 0 unexpired certificates")), 1) 181 test.AssertEquals(t, len(log.GetAllMatching("dry-run: Block SPKI hash "+hex.EncodeToString(keyHash[:]))), 1) 182 test.AssertEquals(t, len(msa.blockRequests), 0) 183 }