github.com/letsencrypt/boulder@v0.20251208.0/issuance/issuer_test.go (about)

     1  package issuance
     2  
     3  import (
     4  	"crypto/ecdsa"
     5  	"crypto/ed25519"
     6  	"crypto/elliptic"
     7  	"crypto/rand"
     8  	"crypto/x509"
     9  	"crypto/x509/pkix"
    10  	"fmt"
    11  	"math/big"
    12  	"os"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/jmhodges/clock"
    18  
    19  	"github.com/letsencrypt/boulder/cmd"
    20  	"github.com/letsencrypt/boulder/config"
    21  	"github.com/letsencrypt/boulder/core"
    22  	"github.com/letsencrypt/boulder/test"
    23  )
    24  
    25  func defaultProfileConfig() ProfileConfig {
    26  	return ProfileConfig{
    27  		MaxValidityPeriod:   config.Duration{Duration: time.Hour},
    28  		MaxValidityBackdate: config.Duration{Duration: time.Hour},
    29  		IgnoredLints: []string{
    30  			// Ignore the two SCT lints because these tests don't get SCTs.
    31  			"w_ct_sct_policy_count_unsatisfied",
    32  			"e_scts_from_same_operator",
    33  			// Ignore the warning about including the SubjectKeyIdentifier extension:
    34  			// we include it on purpose, but plan to remove it soon.
    35  			"w_ext_subject_key_identifier_not_recommended_subscriber",
    36  		},
    37  	}
    38  }
    39  
    40  func defaultIssuerConfig() IssuerConfig {
    41  	return IssuerConfig{
    42  		Active:     true,
    43  		IssuerURL:  "http://issuer-url.example.org",
    44  		CRLURLBase: "http://crl-url.example.org/",
    45  		CRLShards:  10,
    46  		Profiles:   []string{"modern"},
    47  	}
    48  }
    49  
    50  var issuerCert *Certificate
    51  var issuerSigner *ecdsa.PrivateKey
    52  
    53  func TestMain(m *testing.M) {
    54  	tk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    55  	cmd.FailOnError(err, "failed to generate test key")
    56  	issuerSigner = tk
    57  	template := &x509.Certificate{
    58  		SerialNumber:          big.NewInt(123),
    59  		BasicConstraintsValid: true,
    60  		IsCA:                  true,
    61  		Subject: pkix.Name{
    62  			CommonName: "big ca",
    63  		},
    64  		KeyUsage: x509.KeyUsageCRLSign | x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
    65  	}
    66  	issuer, err := x509.CreateCertificate(rand.Reader, template, template, tk.Public(), tk)
    67  	cmd.FailOnError(err, "failed to generate test issuer")
    68  	cert, err := x509.ParseCertificate(issuer)
    69  	cmd.FailOnError(err, "failed to parse test issuer")
    70  	issuerCert = &Certificate{Certificate: cert}
    71  	os.Exit(m.Run())
    72  }
    73  
    74  func TestLoadCertificate(t *testing.T) {
    75  	t.Parallel()
    76  	tests := []struct {
    77  		name    string
    78  		path    string
    79  		wantErr string
    80  	}{
    81  		{"invalid cert file", "../test/hierarchy/int-e1.crl.pem", "loading issuer certificate"},
    82  		{"non-CA cert file", "../test/hierarchy/ee-e1.cert.pem", "not a CA certificate"},
    83  		{"happy path", "../test/hierarchy/int-e1.cert.pem", ""},
    84  	}
    85  	for _, tc := range tests {
    86  		t.Run(tc.name, func(t *testing.T) {
    87  			t.Parallel()
    88  			_, err := LoadCertificate(tc.path)
    89  			if err != nil {
    90  				if tc.wantErr != "" {
    91  					test.AssertContains(t, err.Error(), tc.wantErr)
    92  				} else {
    93  					t.Errorf("expected no error but got %v", err)
    94  				}
    95  			} else {
    96  				if tc.wantErr != "" {
    97  					t.Errorf("expected error %q but got none", tc.wantErr)
    98  				}
    99  			}
   100  		})
   101  	}
   102  }
   103  
   104  func TestLoadSigner(t *testing.T) {
   105  	t.Parallel()
   106  
   107  	// We're using this for its pubkey. This definitely doesn't match the private
   108  	// key loaded in any of the tests below, but that's okay because it still gets
   109  	// us through all the logic in loadSigner.
   110  	fakeKey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
   111  	test.AssertNotError(t, err, "generating test key")
   112  
   113  	tests := []struct {
   114  		name    string
   115  		loc     IssuerLoc
   116  		wantErr string
   117  	}{
   118  		{"empty IssuerLoc", IssuerLoc{}, "must supply"},
   119  		{"invalid key file", IssuerLoc{File: "../test/hierarchy/int-e1.crl.pem"}, "unable to parse"},
   120  		{"ECDSA key file", IssuerLoc{File: "../test/hierarchy/int-e1.key.pem"}, ""},
   121  		{"RSA key file", IssuerLoc{File: "../test/hierarchy/int-r3.key.pem"}, ""},
   122  		{"invalid config file", IssuerLoc{ConfigFile: "../test/ident-policy.yaml"}, "invalid character"},
   123  		// Note that we don't have a test for "valid config file" because it would
   124  		// always fail -- in CI, the softhsm hasn't been initialized, so there's no
   125  		// key to look up; locally even if the softhsm has been initialized, the
   126  		// keys in it don't match the fakeKey we generated above.
   127  	}
   128  	for _, tc := range tests {
   129  		t.Run(tc.name, func(t *testing.T) {
   130  			t.Parallel()
   131  			_, err := loadSigner(tc.loc, fakeKey.Public())
   132  			if err != nil {
   133  				if tc.wantErr != "" {
   134  					test.AssertContains(t, err.Error(), tc.wantErr)
   135  				} else {
   136  					t.Errorf("expected no error but got %v", err)
   137  				}
   138  			} else {
   139  				if tc.wantErr != "" {
   140  					t.Errorf("expected error %q but got none", tc.wantErr)
   141  				}
   142  			}
   143  		})
   144  	}
   145  }
   146  
   147  func TestLoadIssuer(t *testing.T) {
   148  	_, err := newIssuer(
   149  		defaultIssuerConfig(),
   150  		issuerCert,
   151  		issuerSigner,
   152  		clock.NewFake(),
   153  	)
   154  	test.AssertNotError(t, err, "newIssuer failed")
   155  }
   156  
   157  func TestNewIssuerUnsupportedKeyType(t *testing.T) {
   158  	_, err := newIssuer(
   159  		defaultIssuerConfig(),
   160  		&Certificate{
   161  			Certificate: &x509.Certificate{
   162  				PublicKey: &ed25519.PublicKey{},
   163  			},
   164  		},
   165  		&ed25519.PrivateKey{},
   166  		clock.NewFake(),
   167  	)
   168  	test.AssertError(t, err, "newIssuer didn't fail")
   169  	test.AssertEquals(t, err.Error(), "unsupported issuer key type")
   170  }
   171  
   172  func TestNewIssuerKeyUsage(t *testing.T) {
   173  	t.Parallel()
   174  
   175  	tests := []struct {
   176  		name    string
   177  		ku      x509.KeyUsage
   178  		wantErr string
   179  	}{
   180  		{"missing certSign", x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature, "does not have keyUsage certSign"},
   181  		{"missing crlSign", x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, "does not have keyUsage crlSign"},
   182  		{"missing digitalSignature", x509.KeyUsageCertSign | x509.KeyUsageCRLSign, "does not have keyUsage digitalSignature"},
   183  		{"all three", x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature, ""},
   184  	}
   185  	for _, tc := range tests {
   186  		t.Run(tc.name, func(t *testing.T) {
   187  			t.Parallel()
   188  			_, err := newIssuer(
   189  				defaultIssuerConfig(),
   190  				&Certificate{
   191  					Certificate: &x509.Certificate{
   192  						SerialNumber: big.NewInt(123),
   193  						PublicKey: &ecdsa.PublicKey{
   194  							Curve: elliptic.P256(),
   195  						},
   196  						KeyUsage: tc.ku,
   197  					},
   198  				},
   199  				issuerSigner,
   200  				clock.NewFake(),
   201  			)
   202  			if err != nil {
   203  				if tc.wantErr != "" {
   204  					test.AssertContains(t, err.Error(), tc.wantErr)
   205  				} else {
   206  					t.Errorf("expected no error but got %v", err)
   207  				}
   208  			} else {
   209  				if tc.wantErr != "" {
   210  					t.Errorf("expected error %q but got none", tc.wantErr)
   211  				}
   212  			}
   213  		})
   214  	}
   215  }
   216  
   217  func TestLoadChain_Valid(t *testing.T) {
   218  	chain, err := LoadChain([]string{
   219  		"../test/hierarchy/int-e1.cert.pem",
   220  		"../test/hierarchy/root-x2.cert.pem",
   221  	})
   222  	test.AssertNotError(t, err, "Should load valid chain")
   223  
   224  	expectedIssuer, err := core.LoadCert("../test/hierarchy/int-e1.cert.pem")
   225  	test.AssertNotError(t, err, "Failed to load test issuer")
   226  
   227  	chainIssuer := chain[0]
   228  	test.AssertNotNil(t, chainIssuer, "Failed to decode chain PEM")
   229  
   230  	test.AssertByteEquals(t, chainIssuer.Raw, expectedIssuer.Raw)
   231  }
   232  
   233  func TestLoadChain_TooShort(t *testing.T) {
   234  	_, err := LoadChain([]string{"/path/to/one/cert.pem"})
   235  	test.AssertError(t, err, "Should reject too-short chain")
   236  }
   237  
   238  func TestLoadChain_Unloadable(t *testing.T) {
   239  	_, err := LoadChain([]string{
   240  		"does-not-exist.pem",
   241  		"../test/hierarchy/root-x2.cert.pem",
   242  	})
   243  	test.AssertError(t, err, "Should reject unloadable chain")
   244  
   245  	_, err = LoadChain([]string{
   246  		"../test/hierarchy/int-e1.cert.pem",
   247  		"does-not-exist.pem",
   248  	})
   249  	test.AssertError(t, err, "Should reject unloadable chain")
   250  
   251  	invalidPEMFile, _ := os.CreateTemp("", "invalid.pem")
   252  	err = os.WriteFile(invalidPEMFile.Name(), []byte(""), 0640)
   253  	test.AssertNotError(t, err, "Error writing invalid PEM tmp file")
   254  	_, err = LoadChain([]string{
   255  		invalidPEMFile.Name(),
   256  		"../test/hierarchy/root-x2.cert.pem",
   257  	})
   258  	test.AssertError(t, err, "Should reject unloadable chain")
   259  }
   260  
   261  func TestLoadChain_InvalidSig(t *testing.T) {
   262  	_, err := LoadChain([]string{
   263  		"../test/hierarchy/int-e1.cert.pem",
   264  		"../test/hierarchy/root-x1.cert.pem",
   265  	})
   266  	test.AssertError(t, err, "Should reject invalid signature")
   267  	test.Assert(t, strings.Contains(err.Error(), "root-x1.cert.pem"),
   268  		fmt.Sprintf("Expected error to mention filename, got: %s", err))
   269  	test.Assert(t, strings.Contains(err.Error(), "signature from \"CN=(TEST) Ineffable Ice X1"),
   270  		fmt.Sprintf("Expected error to mention subject, got: %s", err))
   271  }