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  }