From 6bc0a86207c409db571637c77e2f1f3fe0fb91db Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 18 Apr 2024 16:12:30 +0200 Subject: [PATCH 1/6] Fix CA startup with Vault RA configuration --- authority/authority.go | 80 ++++++++++++++++++++++++++++------------ ca/ca.go | 1 + cas/apiv1/requests.go | 3 +- cas/apiv1/services.go | 17 +++++++++ cas/vaultcas/vaultcas.go | 3 +- scep/options.go | 20 +++++----- 6 files changed, 89 insertions(+), 35 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index f2118eac..e3da7f93 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -8,6 +8,7 @@ import ( "crypto/sha256" "crypto/x509" "encoding/hex" + "fmt" "log" "net/http" "strings" @@ -447,6 +448,7 @@ func (a *Authority) init() error { return err } a.rootX509Certs = append(a.rootX509Certs, resp.RootCertificate) + a.intermediateX509Certs = append(a.intermediateX509Certs, resp.IntermediateCertificates...) } } @@ -695,32 +697,42 @@ func (a *Authority) init() error { options := &scep.Options{ Roots: a.rootX509Certs, Intermediates: a.intermediateX509Certs, - SignerCert: a.intermediateX509Certs[0], } - if options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ - SigningKey: a.config.IntermediateKey, - Password: a.password, - }); err != nil { - return err + + // intermediate certificates can be empty in RA mode + if len(a.intermediateX509Certs) > 0 { + options.SignerCert = a.intermediateX509Certs[0] } - // TODO(hs): instead of creating the decrypter here, pass the - // intermediate key + chain down to the SCEP authority, - // and only instantiate it when required there. Is that possible? - // Also with entering passwords? - // TODO(hs): if moving the logic, try improving the logic for the - // decrypter password too? Right now it needs to be entered multiple - // times; I've observed it to be three times maximum, every time - // the intermediate key is read. - _, isRSA := options.Signer.Public().(*rsa.PublicKey) - if km, ok := a.keyManager.(kmsapi.Decrypter); ok && isRSA { - if decrypter, err := km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{ - DecryptionKey: a.config.IntermediateKey, - Password: a.password, - }); err == nil { - // only pass the decrypter down when it was successfully created, - // meaning it's an RSA key, and `CreateDecrypter` did not fail. - options.Decrypter = decrypter - options.DecrypterCert = options.Intermediates[0] + + // attempt to create the (default) SCEP signer if the intermediate + // key is configured. + if a.config.IntermediateKey != "" { + if options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ + SigningKey: a.config.IntermediateKey, + Password: a.password, + }); err != nil { + return err + } + + // TODO(hs): instead of creating the decrypter here, pass the + // intermediate key + chain down to the SCEP authority, + // and only instantiate it when required there. Is that possible? + // Also with entering passwords? + // TODO(hs): if moving the logic, try improving the logic for the + // decrypter password too? Right now it needs to be entered multiple + // times; I've observed it to be three times maximum, every time + // the intermediate key is read. + _, isRSAKey := options.Signer.Public().(*rsa.PublicKey) + if km, ok := a.keyManager.(kmsapi.Decrypter); ok && isRSAKey { + if decrypter, err := km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{ + DecryptionKey: a.config.IntermediateKey, + Password: a.password, + }); err == nil { + // only pass the decrypter down when it was successfully created, + // meaning it's an RSA key, and `CreateDecrypter` did not fail. + options.Decrypter = decrypter + options.DecrypterCert = options.Intermediates[0] + } } } @@ -811,6 +823,26 @@ func (a *Authority) init() error { return nil } +func (a *Authority) Mode() string { + if a.isRA() { + return fmt.Sprintf("RA (%s)", casapi.TypeOf(a.x509CAService).Name()) + } + + return "CA" // TODO(hs): more info? I.e. KMS type? +} + +func (a *Authority) isRA() bool { + if a.x509CAService == nil { + return false + } + switch casapi.TypeOf(a.x509CAService) { + case casapi.StepCAS, casapi.CloudCAS, casapi.VaultCAS, casapi.ExternalCAS: + return true + default: + return false + } +} + // initLogf is used to log initialization information. The output // can be disabled by starting the CA with the `--quiet` flag. func (a *Authority) initLogf(format string, v ...any) { diff --git a/ca/ca.go b/ca/ca.go index 0b426ded..d19f88d1 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -425,6 +425,7 @@ func (ca *CA) Run() error { log.Printf("Current context: %s", step.Contexts().GetCurrent().Name) } log.Printf("Config file: %s", ca.getConfigFileOutput()) + log.Printf("Mode: %s", ca.auth.Mode()) baseURL := fmt.Sprintf("https://%s%s", authorityInfo.DNSNames[0], ca.config.Address[strings.LastIndex(ca.config.Address, ":"):]) diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go index fdbb285e..35cf9251 100644 --- a/cas/apiv1/requests.go +++ b/cas/apiv1/requests.go @@ -116,7 +116,8 @@ type GetCertificateAuthorityRequest struct { // GetCertificateAuthorityResponse is the response that contains // the root certificate. type GetCertificateAuthorityResponse struct { - RootCertificate *x509.Certificate + RootCertificate *x509.Certificate + IntermediateCertificates []*x509.Certificate } // CreateKeyRequest is the request used to generate a new key using a KMS. diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index 00ecc2a8..bee58869 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -67,6 +67,23 @@ func (t Type) String() string { return strings.ToLower(string(t)) } +// Name returns the name of the CAS implementation. +func (t Type) Name() (name string) { + switch t { + case CloudCAS: + name = "GCP CAS" + case StepCAS: + name = "Step CAS" + case VaultCAS: + name = "Vault" + case ExternalCAS: + name = "External" + default: + name = "SoftCAS" // TODO(hs): different name? It's not a "CAS" CAS, really + } + return +} + // TypeOf returns the type of the given CertificateAuthorityService. func TypeOf(c CertificateAuthorityService) Type { if ct, ok := c.(interface{ Type() Type }); ok { diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index 73d1b926..16adccc4 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -165,7 +165,8 @@ func (v *VaultCAS) GetCertificateAuthority(*apiv1.GetCertificateAuthorityRequest } return &apiv1.GetCertificateAuthorityResponse{ - RootCertificate: cert.root, + RootCertificate: cert.root, + IntermediateCertificates: cert.intermediates, }, nil } diff --git a/scep/options.go b/scep/options.go index 8bc30a61..d173a76c 100644 --- a/scep/options.go +++ b/scep/options.go @@ -37,19 +37,21 @@ func (o *Options) Validate() error { switch { case len(o.Intermediates) == 0: return errors.New("no intermediate certificate available for SCEP authority") - case o.Signer == nil: - return errors.New("no signer available for SCEP authority") case o.SignerCert == nil: return errors.New("no signer certificate available for SCEP authority") } - // check if the signer (intermediate CA) certificate has the same public key as - // the signer. According to the RFC it seems valid to have different keys for - // the intermediate and the CA signing new certificates, so this might change - // in the future. - signerPublicKey := o.Signer.Public().(comparablePublicKey) - if !signerPublicKey.Equal(o.SignerCert.PublicKey) { - return errors.New("mismatch between signer certificate and public key") + // the signer is optional, but if it's set, its public key must match the signer + // certificate public key. + if o.Signer != nil { + // check if the signer (intermediate CA) certificate has the same public key as + // the signer. According to the RFC it seems valid to have different keys for + // the intermediate and the CA signing new certificates, so this might change + // in the future. + signerPublicKey := o.Signer.Public().(comparablePublicKey) + if !signerPublicKey.Equal(o.SignerCert.PublicKey) { + return errors.New("mismatch between signer certificate and public key") + } } // decrypter can be nil in case a signing only key is used; validation complete. From 113a6dd8abf70ddafe8395a79fb0d266592affa8 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Sat, 20 Apr 2024 00:19:26 +0200 Subject: [PATCH 2/6] Remove reporting the CA mode from startup logs --- authority/authority.go | 21 --------------------- ca/ca.go | 1 - cas/apiv1/services.go | 17 ----------------- 3 files changed, 39 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index e3da7f93..d3b93288 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -8,7 +8,6 @@ import ( "crypto/sha256" "crypto/x509" "encoding/hex" - "fmt" "log" "net/http" "strings" @@ -823,26 +822,6 @@ func (a *Authority) init() error { return nil } -func (a *Authority) Mode() string { - if a.isRA() { - return fmt.Sprintf("RA (%s)", casapi.TypeOf(a.x509CAService).Name()) - } - - return "CA" // TODO(hs): more info? I.e. KMS type? -} - -func (a *Authority) isRA() bool { - if a.x509CAService == nil { - return false - } - switch casapi.TypeOf(a.x509CAService) { - case casapi.StepCAS, casapi.CloudCAS, casapi.VaultCAS, casapi.ExternalCAS: - return true - default: - return false - } -} - // initLogf is used to log initialization information. The output // can be disabled by starting the CA with the `--quiet` flag. func (a *Authority) initLogf(format string, v ...any) { diff --git a/ca/ca.go b/ca/ca.go index d19f88d1..0b426ded 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -425,7 +425,6 @@ func (ca *CA) Run() error { log.Printf("Current context: %s", step.Contexts().GetCurrent().Name) } log.Printf("Config file: %s", ca.getConfigFileOutput()) - log.Printf("Mode: %s", ca.auth.Mode()) baseURL := fmt.Sprintf("https://%s%s", authorityInfo.DNSNames[0], ca.config.Address[strings.LastIndex(ca.config.Address, ":"):]) diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index bee58869..00ecc2a8 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -67,23 +67,6 @@ func (t Type) String() string { return strings.ToLower(string(t)) } -// Name returns the name of the CAS implementation. -func (t Type) Name() (name string) { - switch t { - case CloudCAS: - name = "GCP CAS" - case StepCAS: - name = "Step CAS" - case VaultCAS: - name = "Vault" - case ExternalCAS: - name = "External" - default: - name = "SoftCAS" // TODO(hs): different name? It's not a "CAS" CAS, really - } - return -} - // TypeOf returns the type of the given CertificateAuthorityService. func TypeOf(c CertificateAuthorityService) Type { if ct, ok := c.(interface{ Type() Type }); ok { From b0fabe1346104547dc0dffbb8392fcd96c85619f Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 22 Apr 2024 16:56:55 +0200 Subject: [PATCH 3/6] Add some SCEP integration tests --- test/integration/scep/common_test.go | 242 ++++++++++++++++++++ test/integration/scep/decrypter_cas_test.go | 164 +++++++++++++ test/integration/scep/decrypter_test.go | 155 +++++++++++++ test/integration/scep/regular_cas_test.go | 132 +++++++++++ test/integration/scep/regular_test.go | 123 ++++++++++ 5 files changed, 816 insertions(+) create mode 100644 test/integration/scep/common_test.go create mode 100644 test/integration/scep/decrypter_cas_test.go create mode 100644 test/integration/scep/decrypter_test.go create mode 100644 test/integration/scep/regular_cas_test.go create mode 100644 test/integration/scep/regular_test.go diff --git a/test/integration/scep/common_test.go b/test/integration/scep/common_test.go new file mode 100644 index 00000000..917a2ccc --- /dev/null +++ b/test/integration/scep/common_test.go @@ -0,0 +1,242 @@ +package sceptest + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "errors" + "fmt" + "io" + "math/big" + "net" + "net/http" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/smallstep/pkcs7" + "github.com/smallstep/scep" + "go.step.sm/crypto/minica" + "go.step.sm/crypto/x509util" + + "github.com/smallstep/certificates/cas/apiv1" +) + +// reservePort "reserves" a TCP port by opening a listener on a random +// port and immediately closing it. The port can then be assumed to be +// available for running a server on. +func reservePort(t *testing.T) (host, port string) { + t.Helper() + l, err := net.Listen("tcp", ":0") + require.NoError(t, err) + + address := l.Addr().String() + err = l.Close() + require.NoError(t, err) + + host, port, err = net.SplitHostPort(address) + require.NoError(t, err) + + return +} + +type client struct { + caURL string + caCert *x509.Certificate + httpClient *http.Client +} + +func createSCEPClient(t *testing.T, caURL string) (*client, error) { + t.Helper() + return &client{ + caURL: caURL, + httpClient: http.DefaultClient, + }, nil +} + +func (c *client) getCACert(t *testing.T) error { + // return early if CA certificate already available + if c.caCert != nil { + return nil + } + + resp, err := c.httpClient.Get(fmt.Sprintf("%s?operation=GetCACert&message=test", c.caURL)) + if err != nil { + return fmt.Errorf("failed get request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed reading response body: %w", err) + } + + t.Log(string(body)) + + // SCEP CA/RA certificate selection. If there's only a single certificate, it will + // be used as the CA certificate at all times. If there's multiple, the first certificate + // is assumed to be the certificate of the recipient to encrypt messages to. + switch ct := resp.Header.Get("Content-Type"); ct { + case "application/x-x509-ca-cert": + cert, err := x509.ParseCertificate(body) + if err != nil { + return fmt.Errorf("failed parsing response body: %w", err) + } + if _, ok := cert.PublicKey.(*rsa.PublicKey); !ok { + return fmt.Errorf("certificate has unexpected public key type %T", cert.PublicKey) + } + c.caCert = cert + case "application/x-x509-ca-ra-cert": + certs, err := scep.CACerts(body) + if err != nil { + return fmt.Errorf("failed parsing response body: %w", err) + } + cert := certs[0] + if _, ok := cert.PublicKey.(*rsa.PublicKey); !ok { + return fmt.Errorf("certificate has unexpected public key type %T", cert.PublicKey) + } + c.caCert = cert + default: + return fmt.Errorf("unexpected content-type value %q", ct) + } + + return nil +} + +func (c *client) requestCertificate(t *testing.T, commonName string, sans []string) (*x509.Certificate, error) { + if err := c.getCACert(t); err != nil { + return nil, fmt.Errorf("failed getting CA certificate: %w", err) + } + + signer, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, fmt.Errorf("failed creating SCEP private key: %w", err) + } + + csr, err := x509util.CreateCertificateRequest(commonName, sans, signer) + if err != nil { + return nil, fmt.Errorf("failed creating CSR: %w", err) + } + + tmpl := &x509.Certificate{ + Subject: csr.Subject, + PublicKey: signer.Public(), + SerialNumber: big.NewInt(1), + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(1 * time.Hour), + DNSNames: csr.DNSNames, + IPAddresses: csr.IPAddresses, + EmailAddresses: csr.EmailAddresses, + URIs: csr.URIs, + } + + selfSigned, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, signer.Public(), signer) + if err != nil { + return nil, fmt.Errorf("failed creating self signed certificate: %w", err) + } + selfSignedCertificate, err := x509.ParseCertificate(selfSigned) + if err != nil { + return nil, fmt.Errorf("failed parsing self signed certificate: %w", err) + } + + msgTmpl := &scep.PKIMessage{ + TransactionID: "test-1", + MessageType: scep.PKCSReq, + SenderNonce: []byte("test-nonce-1"), + Recipients: []*x509.Certificate{c.caCert}, + SignerCert: selfSignedCertificate, + SignerKey: signer, + } + + msg, err := scep.NewCSRRequest(csr, msgTmpl) + if err != nil { + return nil, fmt.Errorf("failed creating SCEP PKCSReq message: %w", err) + } + + t.Log(string(msg.Raw)) + + u, err := url.Parse(c.caURL) + if err != nil { + return nil, fmt.Errorf("failed parsing CA URL: %w", err) + } + + opURL := u.ResolveReference(&url.URL{RawQuery: fmt.Sprintf("operation=PKIOperation&message=%s", url.QueryEscape(base64.StdEncoding.EncodeToString(msg.Raw)))}) + resp, err := c.httpClient.Get(opURL.String()) + if err != nil { + return nil, fmt.Errorf("failed get request: %w", err) + } + defer resp.Body.Close() + + if ct := resp.Header.Get("Content-Type"); ct != "application/x-pki-message" { + return nil, fmt.Errorf("received unexpected content type %q", ct) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed reading response body: %w", err) + } + + t.Log(string(body)) + + signedData, err := pkcs7.Parse(body) + if err != nil { + return nil, fmt.Errorf("failed parsing response body: %w", err) + } + + // TODO: verify the signature? + + p7, err := pkcs7.Parse(signedData.Content) + if err != nil { + return nil, fmt.Errorf("failed decrypting inner p7: %w", err) + } + + content, err := p7.Decrypt(selfSignedCertificate, signer) + if err != nil { + return nil, fmt.Errorf("failed decrypting response: %w", err) + } + + p7, err = pkcs7.Parse(content) + if err != nil { + return nil, fmt.Errorf("failed parsing p7 content: %w", err) + } + + cert := p7.Certificates[0] + + return cert, nil +} + +type testCAS struct { + ca *minica.CA +} + +func (c *testCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { + cert, err := c.ca.SignCSR(req.CSR) + if err != nil { + return nil, fmt.Errorf("failed signing CSR: %w", err) + } + + return &apiv1.CreateCertificateResponse{ + Certificate: cert, + CertificateChain: []*x509.Certificate{cert, c.ca.Intermediate}, + }, nil +} +func (c *testCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { + return nil, errors.New("not implemented") +} + +func (c *testCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { + return nil, errors.New("not implemented") +} + +func (c *testCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) { + return &apiv1.GetCertificateAuthorityResponse{ + RootCertificate: c.ca.Root, + IntermediateCertificates: []*x509.Certificate{c.ca.Intermediate}, + }, nil +} + +var _ apiv1.CertificateAuthorityService = (*testCAS)(nil) +var _ apiv1.CertificateAuthorityGetter = (*testCAS)(nil) diff --git a/test/integration/scep/decrypter_cas_test.go b/test/integration/scep/decrypter_cas_test.go new file mode 100644 index 00000000..f69af6f9 --- /dev/null +++ b/test/integration/scep/decrypter_cas_test.go @@ -0,0 +1,164 @@ +package sceptest + +import ( + "context" + "crypto" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "fmt" + "math/big" + "net" + "net/http" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.step.sm/crypto/keyutil" + "go.step.sm/crypto/minica" + "go.step.sm/crypto/pemutil" + + "github.com/smallstep/certificates/authority/config" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/ca" + "github.com/smallstep/certificates/cas/apiv1" +) + +func TestIssuesCertificateUsingSCEPWithDecrypterAndUpstreamCAS(t *testing.T) { + signer, err := keyutil.GenerateSigner("EC", "P-256", 0) + require.NoError(t, err) + + dir := t.TempDir() + m, err := minica.New(minica.WithName("Step E2E | SCEP Decrypter w/ Upstream CAS"), minica.WithGetSignerFunc(func() (crypto.Signer, error) { + return signer, nil + })) + require.NoError(t, err) + + rootFilepath := filepath.Join(dir, "root.crt") + _, err = pemutil.Serialize(m.Root, pemutil.WithFilename(rootFilepath)) + require.NoError(t, err) + + intermediateCertFilepath := filepath.Join(dir, "intermediate.crt") + _, err = pemutil.Serialize(m.Intermediate, pemutil.WithFilename(intermediateCertFilepath)) + require.NoError(t, err) + + intermediateKeyFilepath := filepath.Join(dir, "intermediate.key") + _, err = pemutil.Serialize(m.Signer, pemutil.WithFilename(intermediateKeyFilepath)) + require.NoError(t, err) + + decrypterKey, err := keyutil.GenerateKey("RSA", "", 2048) + require.NoError(t, err) + + decrypter, ok := decrypterKey.(crypto.Decrypter) + require.True(t, ok) + + decrypterCertifiate, err := m.Sign(&x509.Certificate{ + Subject: pkix.Name{CommonName: "decrypter"}, + PublicKey: decrypter.Public(), + SerialNumber: big.NewInt(1), + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(1 * time.Hour), + DNSNames: []string{"decrypter"}, + }) + require.NoError(t, err) + + b, err := pemutil.Serialize(decrypterCertifiate) + require.NoError(t, err) + decrypterCertificatePEMBytes := pem.EncodeToMemory(b) + + b, err = pemutil.Serialize(decrypter, pemutil.WithPassword([]byte("1234"))) + require.NoError(t, err) + decrypterKeyPEMBytes := pem.EncodeToMemory(b) + + // get a random address to listen on and connect to; currently no nicer way to get one before starting the server + // TODO(hs): find/implement a nicer way to expose the CA URL, similar to how e.g. httptest.Server exposes it? + host, port := reservePort(t) + insecureHost, insecurePort := reservePort(t) + + prov := &provisioner.SCEP{ + ID: "scep", + Name: "scep", + Type: "SCEP", + ForceCN: false, + ChallengePassword: "", + EncryptionAlgorithmIdentifier: 2, + MinimumPublicKeyLength: 2048, + Claims: &config.GlobalProvisionerClaims, + DecrypterCertificate: decrypterCertificatePEMBytes, + DecrypterKeyPEM: decrypterKeyPEMBytes, + DecrypterKeyPassword: "1234", + } + + err = prov.Init(provisioner.Config{}) + require.NoError(t, err) + + apiv1.Register("test-scep-cas", func(_ context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) { + return &testCAS{ + ca: m, + }, nil + }) + + cfg := &config.Config{ + Address: net.JoinHostPort(host, port), // reuse the address that was just "reserved" + InsecureAddress: net.JoinHostPort(insecureHost, insecurePort), // reuse the address that was just "reserved" + DNSNames: []string{"127.0.0.1", "[::1]", "localhost"}, + AuthorityConfig: &config.AuthConfig{ + Options: &apiv1.Options{ + AuthorityID: "stepca-test-scep", + Type: "test-scep-cas", + CertificateAuthority: "test-cas", + }, + AuthorityID: "stepca-test-scep", + DeploymentType: "standalone-test", + Provisioners: provisioner.List{prov}, + }, + Logger: json.RawMessage(`{"format": "text"}`), + } + c, err := ca.New(cfg) + require.NoError(t, err) + + // instantiate a client for the CA running at the random address + caClient, err := ca.NewClient( + fmt.Sprintf("https://localhost:%s", port), + ca.WithRootFile(rootFilepath), + ) + require.NoError(t, err) + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + err = c.Run() + require.ErrorIs(t, err, http.ErrServerClosed) + }() + + // require OK health response as the baseline + ctx := context.Background() + healthResponse, err := caClient.HealthWithContext(ctx) + require.NoError(t, err) + if assert.NotNil(t, healthResponse) { + require.Equal(t, "ok", healthResponse.Status) + } + + scepClient, err := createSCEPClient(t, fmt.Sprintf("http://localhost:%s/scep/scep", insecurePort)) + require.NoError(t, err) + + cert, err := scepClient.requestCertificate(t, "test.localhost", []string{"test.localhost"}) + assert.NoError(t, err) + require.NotNil(t, cert) + + assert.Equal(t, "test.localhost", cert.Subject.CommonName) + assert.Equal(t, "Step E2E | SCEP Decrypter w/ Upstream CAS Intermediate CA", cert.Issuer.CommonName) + + // done testing; stop and wait for the server to quit + err = c.Stop() + require.NoError(t, err) + + wg.Wait() +} diff --git a/test/integration/scep/decrypter_test.go b/test/integration/scep/decrypter_test.go new file mode 100644 index 00000000..9af1b921 --- /dev/null +++ b/test/integration/scep/decrypter_test.go @@ -0,0 +1,155 @@ +package sceptest + +import ( + "context" + "crypto" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "fmt" + "math/big" + "net" + "net/http" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.step.sm/crypto/keyutil" + "go.step.sm/crypto/minica" + "go.step.sm/crypto/pemutil" + + "github.com/smallstep/certificates/authority/config" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/ca" +) + +func TestIssuesCertificateUsingSCEPWithDecrypter(t *testing.T) { + signer, err := keyutil.GenerateSigner("EC", "P-256", 0) + require.NoError(t, err) + + dir := t.TempDir() + m, err := minica.New(minica.WithName("Step E2E | SCEP Decrypter"), minica.WithGetSignerFunc(func() (crypto.Signer, error) { + return signer, nil + })) + require.NoError(t, err) + + rootFilepath := filepath.Join(dir, "root.crt") + _, err = pemutil.Serialize(m.Root, pemutil.WithFilename(rootFilepath)) + require.NoError(t, err) + + intermediateCertFilepath := filepath.Join(dir, "intermediate.crt") + _, err = pemutil.Serialize(m.Intermediate, pemutil.WithFilename(intermediateCertFilepath)) + require.NoError(t, err) + + intermediateKeyFilepath := filepath.Join(dir, "intermediate.key") + _, err = pemutil.Serialize(m.Signer, pemutil.WithFilename(intermediateKeyFilepath)) + require.NoError(t, err) + + decrypterKey, err := keyutil.GenerateKey("RSA", "", 2048) + require.NoError(t, err) + + decrypter, ok := decrypterKey.(crypto.Decrypter) + require.True(t, ok) + + decrypterCertifiate, err := m.Sign(&x509.Certificate{ + Subject: pkix.Name{CommonName: "decrypter"}, + PublicKey: decrypter.Public(), + SerialNumber: big.NewInt(1), + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(1 * time.Hour), + DNSNames: []string{"decrypter"}, + }) + require.NoError(t, err) + + b, err := pemutil.Serialize(decrypterCertifiate) + require.NoError(t, err) + decrypterCertificatePEMBytes := pem.EncodeToMemory(b) + + b, err = pemutil.Serialize(decrypter, pemutil.WithPassword([]byte("1234"))) + require.NoError(t, err) + decrypterKeyPEMBytes := pem.EncodeToMemory(b) + + // get a random address to listen on and connect to; currently no nicer way to get one before starting the server + // TODO(hs): find/implement a nicer way to expose the CA URL, similar to how e.g. httptest.Server exposes it? + host, port := reservePort(t) + insecureHost, insecurePort := reservePort(t) + + prov := &provisioner.SCEP{ + ID: "scep", + Name: "scep", + Type: "SCEP", + ForceCN: false, + ChallengePassword: "", + EncryptionAlgorithmIdentifier: 2, + MinimumPublicKeyLength: 2048, + Claims: &config.GlobalProvisionerClaims, + DecrypterCertificate: decrypterCertificatePEMBytes, + DecrypterKeyPEM: decrypterKeyPEMBytes, + DecrypterKeyPassword: "1234", + } + + err = prov.Init(provisioner.Config{}) + require.NoError(t, err) + + cfg := &config.Config{ + Root: []string{rootFilepath}, + IntermediateCert: intermediateCertFilepath, + IntermediateKey: intermediateKeyFilepath, + Address: net.JoinHostPort(host, port), // reuse the address that was just "reserved" + InsecureAddress: net.JoinHostPort(insecureHost, insecurePort), // reuse the address that was just "reserved" + DNSNames: []string{"127.0.0.1", "[::1]", "localhost"}, + AuthorityConfig: &config.AuthConfig{ + AuthorityID: "stepca-test-scep", + DeploymentType: "standalone-test", + Provisioners: provisioner.List{prov}, + }, + Logger: json.RawMessage(`{"format": "text"}`), + } + c, err := ca.New(cfg) + require.NoError(t, err) + + // instantiate a client for the CA running at the random address + caClient, err := ca.NewClient( + fmt.Sprintf("https://localhost:%s", port), + ca.WithRootFile(rootFilepath), + ) + require.NoError(t, err) + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + err = c.Run() + require.ErrorIs(t, err, http.ErrServerClosed) + }() + + // require OK health response as the baseline + ctx := context.Background() + healthResponse, err := caClient.HealthWithContext(ctx) + require.NoError(t, err) + if assert.NotNil(t, healthResponse) { + require.Equal(t, "ok", healthResponse.Status) + } + + scepClient, err := createSCEPClient(t, fmt.Sprintf("http://localhost:%s/scep/scep", insecurePort)) + require.NoError(t, err) + + cert, err := scepClient.requestCertificate(t, "test.localhost", []string{"test.localhost"}) + assert.NoError(t, err) + require.NotNil(t, cert) + + assert.Equal(t, "test.localhost", cert.Subject.CommonName) + assert.Equal(t, "Step E2E | SCEP Decrypter Intermediate CA", cert.Issuer.CommonName) + + // done testing; stop and wait for the server to quit + err = c.Stop() + require.NoError(t, err) + + wg.Wait() +} diff --git a/test/integration/scep/regular_cas_test.go b/test/integration/scep/regular_cas_test.go new file mode 100644 index 00000000..5ab653b3 --- /dev/null +++ b/test/integration/scep/regular_cas_test.go @@ -0,0 +1,132 @@ +package sceptest + +import ( + "context" + "crypto" + "encoding/json" + "fmt" + "net" + "net/http" + "path/filepath" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.step.sm/crypto/keyutil" + "go.step.sm/crypto/minica" + "go.step.sm/crypto/pemutil" + + "github.com/smallstep/certificates/authority/config" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/ca" + "github.com/smallstep/certificates/cas/apiv1" +) + +func TestIssuesCertificateUsingRegularSCEPConfiguration(t *testing.T) { + signer, err := keyutil.GenerateSigner("RSA", "", 2048) + require.NoError(t, err) + + dir := t.TempDir() + m, err := minica.New(minica.WithName("Step E2E | SCEP Regular w/ Upstream CAS"), minica.WithGetSignerFunc(func() (crypto.Signer, error) { + return signer, nil + })) + require.NoError(t, err) + + rootFilepath := filepath.Join(dir, "root.crt") + _, err = pemutil.Serialize(m.Root, pemutil.WithFilename(rootFilepath)) + require.NoError(t, err) + + intermediateCertFilepath := filepath.Join(dir, "intermediate.crt") + _, err = pemutil.Serialize(m.Intermediate, pemutil.WithFilename(intermediateCertFilepath)) + require.NoError(t, err) + + intermediateKeyFilepath := filepath.Join(dir, "intermediate.key") + _, err = pemutil.Serialize(m.Signer, pemutil.WithFilename(intermediateKeyFilepath)) + require.NoError(t, err) + + // get a random address to listen on and connect to; currently no nicer way to get one before starting the server + // TODO(hs): find/implement a nicer way to expose the CA URL, similar to how e.g. httptest.Server exposes it? + host, port := reservePort(t) + insecureHost, insecurePort := reservePort(t) + + prov := &provisioner.SCEP{ + ID: "scep", + Name: "scep", + Type: "SCEP", + ForceCN: false, + ChallengePassword: "", + EncryptionAlgorithmIdentifier: 2, + MinimumPublicKeyLength: 2048, + Claims: &config.GlobalProvisionerClaims, + } + + err = prov.Init(provisioner.Config{}) + require.NoError(t, err) + + apiv1.Register("test-scep-cas", func(_ context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) { + return &testCAS{ + ca: m, + }, nil + }) + + cfg := &config.Config{ + Address: net.JoinHostPort(host, port), // reuse the address that was just "reserved" + InsecureAddress: net.JoinHostPort(insecureHost, insecurePort), // reuse the address that was just "reserved" + DNSNames: []string{"127.0.0.1", "[::1]", "localhost"}, + AuthorityConfig: &config.AuthConfig{ + Options: &apiv1.Options{ + AuthorityID: "stepca-test-scep", + Type: "test-scep-cas", + CertificateAuthority: "test-cas", + }, + AuthorityID: "stepca-test-scep", + DeploymentType: "standalone-test", + Provisioners: provisioner.List{prov}, + }, + Logger: json.RawMessage(`{"format": "text"}`), + } + c, err := ca.New(cfg) + require.NoError(t, err) + + // instantiate a client for the CA running at the random address + caClient, err := ca.NewClient( + fmt.Sprintf("https://localhost:%s", port), + ca.WithRootFile(rootFilepath), + ) + require.NoError(t, err) + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + err = c.Run() + require.ErrorIs(t, err, http.ErrServerClosed) + }() + + // require OK health response as the baseline + ctx := context.Background() + healthResponse, err := caClient.HealthWithContext(ctx) + require.NoError(t, err) + if assert.NotNil(t, healthResponse) { + require.Equal(t, "ok", healthResponse.Status) + } + + scepClient, err := createSCEPClient(t, fmt.Sprintf("http://localhost:%s/scep/scep", insecurePort)) + require.NoError(t, err) + + cert, err := scepClient.requestCertificate(t, "test.localhost", []string{"test.localhost"}) + assert.NoError(t, err) + require.NotNil(t, cert) + + assert.Equal(t, "test.localhost", cert.Subject.CommonName) + assert.Equal(t, "Step E2E | SCEP Regular w/ Upstream CAS Intermediate CA", cert.Issuer.CommonName) + + // done testing; stop and wait for the server to quit + err = c.Stop() + require.NoError(t, err) + + wg.Wait() +} diff --git a/test/integration/scep/regular_test.go b/test/integration/scep/regular_test.go new file mode 100644 index 00000000..b99c6679 --- /dev/null +++ b/test/integration/scep/regular_test.go @@ -0,0 +1,123 @@ +package sceptest + +import ( + "context" + "crypto" + "encoding/json" + "fmt" + "net" + "net/http" + "path/filepath" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.step.sm/crypto/keyutil" + "go.step.sm/crypto/minica" + "go.step.sm/crypto/pemutil" + + "github.com/smallstep/certificates/authority/config" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/ca" +) + +func TestIssuesCertificateUsingRegularSCEPWithUpstreamCAS(t *testing.T) { + signer, err := keyutil.GenerateSigner("RSA", "", 2048) + require.NoError(t, err) + + dir := t.TempDir() + m, err := minica.New(minica.WithName("Step E2E | SCEP Regular w/ Upstream CAS"), minica.WithGetSignerFunc(func() (crypto.Signer, error) { + return signer, nil + })) + require.NoError(t, err) + + rootFilepath := filepath.Join(dir, "root.crt") + _, err = pemutil.Serialize(m.Root, pemutil.WithFilename(rootFilepath)) + require.NoError(t, err) + + intermediateCertFilepath := filepath.Join(dir, "intermediate.crt") + _, err = pemutil.Serialize(m.Intermediate, pemutil.WithFilename(intermediateCertFilepath)) + require.NoError(t, err) + + intermediateKeyFilepath := filepath.Join(dir, "intermediate.key") + _, err = pemutil.Serialize(m.Signer, pemutil.WithFilename(intermediateKeyFilepath)) + require.NoError(t, err) + + // get a random address to listen on and connect to; currently no nicer way to get one before starting the server + // TODO(hs): find/implement a nicer way to expose the CA URL, similar to how e.g. httptest.Server exposes it? + host, port := reservePort(t) + insecureHost, insecurePort := reservePort(t) + + prov := &provisioner.SCEP{ + ID: "scep", + Name: "scep", + Type: "SCEP", + ForceCN: false, + ChallengePassword: "", + EncryptionAlgorithmIdentifier: 2, + MinimumPublicKeyLength: 2048, + Claims: &config.GlobalProvisionerClaims, + } + + err = prov.Init(provisioner.Config{}) + require.NoError(t, err) + + cfg := &config.Config{ + Root: []string{rootFilepath}, + IntermediateCert: intermediateCertFilepath, + IntermediateKey: intermediateKeyFilepath, + Address: net.JoinHostPort(host, port), // reuse the address that was just "reserved" + InsecureAddress: net.JoinHostPort(insecureHost, insecurePort), // reuse the address that was just "reserved" + DNSNames: []string{"127.0.0.1", "[::1]", "localhost"}, + AuthorityConfig: &config.AuthConfig{ + AuthorityID: "stepca-test-scep", + DeploymentType: "standalone-test", + Provisioners: provisioner.List{prov}, + }, + Logger: json.RawMessage(`{"format": "text"}`), + } + c, err := ca.New(cfg) + require.NoError(t, err) + + // instantiate a client for the CA running at the random address + caClient, err := ca.NewClient( + fmt.Sprintf("https://localhost:%s", port), + ca.WithRootFile(rootFilepath), + ) + require.NoError(t, err) + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + err = c.Run() + require.ErrorIs(t, err, http.ErrServerClosed) + }() + + // require OK health response as the baseline + ctx := context.Background() + healthResponse, err := caClient.HealthWithContext(ctx) + require.NoError(t, err) + if assert.NotNil(t, healthResponse) { + require.Equal(t, "ok", healthResponse.Status) + } + + scepClient, err := createSCEPClient(t, fmt.Sprintf("http://localhost:%s/scep/scep", insecurePort)) + require.NoError(t, err) + + cert, err := scepClient.requestCertificate(t, "test.localhost", []string{"test.localhost"}) + assert.NoError(t, err) + require.NotNil(t, cert) + + assert.Equal(t, "test.localhost", cert.Subject.CommonName) + assert.Equal(t, "Step E2E | SCEP Regular Intermediate CA", cert.Issuer.CommonName) + + // done testing; stop and wait for the server to quit + err = c.Stop() + require.NoError(t, err) + + wg.Wait() +} From 87202001a883bb56bae7b381483123f7c082937b Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 22 Apr 2024 17:14:54 +0200 Subject: [PATCH 4/6] Rewrite SCEP integration tests to only use the HTTPS endpoint --- test/integration/scep/common_test.go | 16 ++++++++++++++-- test/integration/scep/decrypter_cas_test.go | 8 +++----- test/integration/scep/decrypter_test.go | 6 ++---- test/integration/scep/regular_cas_test.go | 19 ++++++++----------- test/integration/scep/regular_test.go | 10 ++++------ 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/test/integration/scep/common_test.go b/test/integration/scep/common_test.go index 917a2ccc..40ac17b7 100644 --- a/test/integration/scep/common_test.go +++ b/test/integration/scep/common_test.go @@ -3,6 +3,7 @@ package sceptest import ( "crypto/rand" "crypto/rsa" + "crypto/tls" "crypto/x509" "encoding/base64" "errors" @@ -49,11 +50,20 @@ type client struct { httpClient *http.Client } -func createSCEPClient(t *testing.T, caURL string) (*client, error) { +func createSCEPClient(t *testing.T, caURL string, root *x509.Certificate) (*client, error) { t.Helper() + trustedRoots := x509.NewCertPool() + trustedRoots.AddCert(root) + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{ + RootCAs: trustedRoots, + } + httpClient := &http.Client{ + Transport: transport, + } return &client{ caURL: caURL, - httpClient: http.DefaultClient, + httpClient: httpClient, }, nil } @@ -100,6 +110,8 @@ func (c *client) getCACert(t *testing.T) error { } c.caCert = cert default: + fmt.Println("body", string(body)) + return fmt.Errorf("unexpected content-type value %q", ct) } diff --git a/test/integration/scep/decrypter_cas_test.go b/test/integration/scep/decrypter_cas_test.go index f69af6f9..cdfdb61d 100644 --- a/test/integration/scep/decrypter_cas_test.go +++ b/test/integration/scep/decrypter_cas_test.go @@ -78,7 +78,6 @@ func TestIssuesCertificateUsingSCEPWithDecrypterAndUpstreamCAS(t *testing.T) { // get a random address to listen on and connect to; currently no nicer way to get one before starting the server // TODO(hs): find/implement a nicer way to expose the CA URL, similar to how e.g. httptest.Server exposes it? host, port := reservePort(t) - insecureHost, insecurePort := reservePort(t) prov := &provisioner.SCEP{ ID: "scep", @@ -104,9 +103,8 @@ func TestIssuesCertificateUsingSCEPWithDecrypterAndUpstreamCAS(t *testing.T) { }) cfg := &config.Config{ - Address: net.JoinHostPort(host, port), // reuse the address that was just "reserved" - InsecureAddress: net.JoinHostPort(insecureHost, insecurePort), // reuse the address that was just "reserved" - DNSNames: []string{"127.0.0.1", "[::1]", "localhost"}, + Address: net.JoinHostPort(host, port), // reuse the address that was just "reserved" + DNSNames: []string{"127.0.0.1", "[::1]", "localhost"}, AuthorityConfig: &config.AuthConfig{ Options: &apiv1.Options{ AuthorityID: "stepca-test-scep", @@ -146,7 +144,7 @@ func TestIssuesCertificateUsingSCEPWithDecrypterAndUpstreamCAS(t *testing.T) { require.Equal(t, "ok", healthResponse.Status) } - scepClient, err := createSCEPClient(t, fmt.Sprintf("http://localhost:%s/scep/scep", insecurePort)) + scepClient, err := createSCEPClient(t, fmt.Sprintf("https://localhost:%s/scep/scep", port), m.Root) require.NoError(t, err) cert, err := scepClient.requestCertificate(t, "test.localhost", []string{"test.localhost"}) diff --git a/test/integration/scep/decrypter_test.go b/test/integration/scep/decrypter_test.go index 9af1b921..1a2e370a 100644 --- a/test/integration/scep/decrypter_test.go +++ b/test/integration/scep/decrypter_test.go @@ -77,7 +77,6 @@ func TestIssuesCertificateUsingSCEPWithDecrypter(t *testing.T) { // get a random address to listen on and connect to; currently no nicer way to get one before starting the server // TODO(hs): find/implement a nicer way to expose the CA URL, similar to how e.g. httptest.Server exposes it? host, port := reservePort(t) - insecureHost, insecurePort := reservePort(t) prov := &provisioner.SCEP{ ID: "scep", @@ -100,8 +99,7 @@ func TestIssuesCertificateUsingSCEPWithDecrypter(t *testing.T) { Root: []string{rootFilepath}, IntermediateCert: intermediateCertFilepath, IntermediateKey: intermediateKeyFilepath, - Address: net.JoinHostPort(host, port), // reuse the address that was just "reserved" - InsecureAddress: net.JoinHostPort(insecureHost, insecurePort), // reuse the address that was just "reserved" + Address: net.JoinHostPort(host, port), // reuse the address that was just "reserved" DNSNames: []string{"127.0.0.1", "[::1]", "localhost"}, AuthorityConfig: &config.AuthConfig{ AuthorityID: "stepca-test-scep", @@ -137,7 +135,7 @@ func TestIssuesCertificateUsingSCEPWithDecrypter(t *testing.T) { require.Equal(t, "ok", healthResponse.Status) } - scepClient, err := createSCEPClient(t, fmt.Sprintf("http://localhost:%s/scep/scep", insecurePort)) + scepClient, err := createSCEPClient(t, fmt.Sprintf("https://localhost:%s/scep/scep", port), m.Root) require.NoError(t, err) cert, err := scepClient.requestCertificate(t, "test.localhost", []string{"test.localhost"}) diff --git a/test/integration/scep/regular_cas_test.go b/test/integration/scep/regular_cas_test.go index 5ab653b3..0bf9b8b0 100644 --- a/test/integration/scep/regular_cas_test.go +++ b/test/integration/scep/regular_cas_test.go @@ -24,7 +24,7 @@ import ( "github.com/smallstep/certificates/cas/apiv1" ) -func TestIssuesCertificateUsingRegularSCEPConfiguration(t *testing.T) { +func TestFailsIssuingCertificateUsingRegularSCEPWithUpstreamCAS(t *testing.T) { signer, err := keyutil.GenerateSigner("RSA", "", 2048) require.NoError(t, err) @@ -49,7 +49,6 @@ func TestIssuesCertificateUsingRegularSCEPConfiguration(t *testing.T) { // get a random address to listen on and connect to; currently no nicer way to get one before starting the server // TODO(hs): find/implement a nicer way to expose the CA URL, similar to how e.g. httptest.Server exposes it? host, port := reservePort(t) - insecureHost, insecurePort := reservePort(t) prov := &provisioner.SCEP{ ID: "scep", @@ -72,9 +71,8 @@ func TestIssuesCertificateUsingRegularSCEPConfiguration(t *testing.T) { }) cfg := &config.Config{ - Address: net.JoinHostPort(host, port), // reuse the address that was just "reserved" - InsecureAddress: net.JoinHostPort(insecureHost, insecurePort), // reuse the address that was just "reserved" - DNSNames: []string{"127.0.0.1", "[::1]", "localhost"}, + Address: net.JoinHostPort(host, port), // reuse the address that was just "reserved" + DNSNames: []string{"127.0.0.1", "[::1]", "localhost"}, AuthorityConfig: &config.AuthConfig{ Options: &apiv1.Options{ AuthorityID: "stepca-test-scep", @@ -114,15 +112,14 @@ func TestIssuesCertificateUsingRegularSCEPConfiguration(t *testing.T) { require.Equal(t, "ok", healthResponse.Status) } - scepClient, err := createSCEPClient(t, fmt.Sprintf("http://localhost:%s/scep/scep", insecurePort)) + scepClient, err := createSCEPClient(t, fmt.Sprintf("https://localhost:%s/scep/scep", port), m.Root) require.NoError(t, err) + // issuance is expected to fail when an upstream CAS is configured, as the current + // CAS interfaces do not support providing a decrypter. cert, err := scepClient.requestCertificate(t, "test.localhost", []string{"test.localhost"}) - assert.NoError(t, err) - require.NotNil(t, cert) - - assert.Equal(t, "test.localhost", cert.Subject.CommonName) - assert.Equal(t, "Step E2E | SCEP Regular w/ Upstream CAS Intermediate CA", cert.Issuer.CommonName) + assert.Error(t, err) + assert.Nil(t, cert) // done testing; stop and wait for the server to quit err = c.Stop() diff --git a/test/integration/scep/regular_test.go b/test/integration/scep/regular_test.go index b99c6679..500e8370 100644 --- a/test/integration/scep/regular_test.go +++ b/test/integration/scep/regular_test.go @@ -23,12 +23,12 @@ import ( "github.com/smallstep/certificates/ca" ) -func TestIssuesCertificateUsingRegularSCEPWithUpstreamCAS(t *testing.T) { +func TestIssuesCertificateUsingRegularSCEPConfiguration(t *testing.T) { signer, err := keyutil.GenerateSigner("RSA", "", 2048) require.NoError(t, err) dir := t.TempDir() - m, err := minica.New(minica.WithName("Step E2E | SCEP Regular w/ Upstream CAS"), minica.WithGetSignerFunc(func() (crypto.Signer, error) { + m, err := minica.New(minica.WithName("Step E2E | SCEP Regular"), minica.WithGetSignerFunc(func() (crypto.Signer, error) { return signer, nil })) require.NoError(t, err) @@ -48,7 +48,6 @@ func TestIssuesCertificateUsingRegularSCEPWithUpstreamCAS(t *testing.T) { // get a random address to listen on and connect to; currently no nicer way to get one before starting the server // TODO(hs): find/implement a nicer way to expose the CA URL, similar to how e.g. httptest.Server exposes it? host, port := reservePort(t) - insecureHost, insecurePort := reservePort(t) prov := &provisioner.SCEP{ ID: "scep", @@ -68,8 +67,7 @@ func TestIssuesCertificateUsingRegularSCEPWithUpstreamCAS(t *testing.T) { Root: []string{rootFilepath}, IntermediateCert: intermediateCertFilepath, IntermediateKey: intermediateKeyFilepath, - Address: net.JoinHostPort(host, port), // reuse the address that was just "reserved" - InsecureAddress: net.JoinHostPort(insecureHost, insecurePort), // reuse the address that was just "reserved" + Address: net.JoinHostPort(host, port), // reuse the address that was just "reserved" DNSNames: []string{"127.0.0.1", "[::1]", "localhost"}, AuthorityConfig: &config.AuthConfig{ AuthorityID: "stepca-test-scep", @@ -105,7 +103,7 @@ func TestIssuesCertificateUsingRegularSCEPWithUpstreamCAS(t *testing.T) { require.Equal(t, "ok", healthResponse.Status) } - scepClient, err := createSCEPClient(t, fmt.Sprintf("http://localhost:%s/scep/scep", insecurePort)) + scepClient, err := createSCEPClient(t, fmt.Sprintf("https://localhost:%s/scep/scep", port), m.Root) require.NoError(t, err) cert, err := scepClient.requestCertificate(t, "test.localhost", []string{"test.localhost"}) From 2561a7271e3aa2fbd8a427396d1eda787a5082a3 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 22 Apr 2024 19:12:54 +0200 Subject: [PATCH 5/6] Dedupe CA and SCEP client creation logic --- test/integration/scep/common_test.go | 25 +++++++++++++++++++-- test/integration/scep/decrypter_cas_test.go | 21 ++++------------- test/integration/scep/decrypter_test.go | 22 ++++-------------- test/integration/scep/regular_cas_test.go | 21 ++++------------- test/integration/scep/regular_test.go | 22 ++++-------------- 5 files changed, 39 insertions(+), 72 deletions(-) diff --git a/test/integration/scep/common_test.go b/test/integration/scep/common_test.go index 40ac17b7..60581e64 100644 --- a/test/integration/scep/common_test.go +++ b/test/integration/scep/common_test.go @@ -1,6 +1,7 @@ package sceptest import ( + "context" "crypto/rand" "crypto/rsa" "crypto/tls" @@ -16,6 +17,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/smallstep/pkcs7" @@ -23,9 +25,28 @@ import ( "go.step.sm/crypto/minica" "go.step.sm/crypto/x509util" + "github.com/smallstep/certificates/ca" "github.com/smallstep/certificates/cas/apiv1" ) +func newCAClient(t *testing.T, caURL, rootFilepath string) *ca.Client { + caClient, err := ca.NewClient( + caURL, + ca.WithRootFile(rootFilepath), + ) + require.NoError(t, err) + return caClient +} + +func requireHealthyCA(t *testing.T, caClient *ca.Client) { + ctx := context.Background() + healthResponse, err := caClient.HealthWithContext(ctx) + require.NoError(t, err) + if assert.NotNil(t, healthResponse) { + require.Equal(t, "ok", healthResponse.Status) + } +} + // reservePort "reserves" a TCP port by opening a listener on a random // port and immediately closing it. The port can then be assumed to be // available for running a server on. @@ -50,7 +71,7 @@ type client struct { httpClient *http.Client } -func createSCEPClient(t *testing.T, caURL string, root *x509.Certificate) (*client, error) { +func createSCEPClient(t *testing.T, caURL string, root *x509.Certificate) *client { t.Helper() trustedRoots := x509.NewCertPool() trustedRoots.AddCert(root) @@ -64,7 +85,7 @@ func createSCEPClient(t *testing.T, caURL string, root *x509.Certificate) (*clie return &client{ caURL: caURL, httpClient: httpClient, - }, nil + } } func (c *client) getCACert(t *testing.T) error { diff --git a/test/integration/scep/decrypter_cas_test.go b/test/integration/scep/decrypter_cas_test.go index cdfdb61d..f19a2c91 100644 --- a/test/integration/scep/decrypter_cas_test.go +++ b/test/integration/scep/decrypter_cas_test.go @@ -120,13 +120,6 @@ func TestIssuesCertificateUsingSCEPWithDecrypterAndUpstreamCAS(t *testing.T) { c, err := ca.New(cfg) require.NoError(t, err) - // instantiate a client for the CA running at the random address - caClient, err := ca.NewClient( - fmt.Sprintf("https://localhost:%s", port), - ca.WithRootFile(rootFilepath), - ) - require.NoError(t, err) - var wg sync.WaitGroup wg.Add(1) @@ -136,17 +129,11 @@ func TestIssuesCertificateUsingSCEPWithDecrypterAndUpstreamCAS(t *testing.T) { require.ErrorIs(t, err, http.ErrServerClosed) }() - // require OK health response as the baseline - ctx := context.Background() - healthResponse, err := caClient.HealthWithContext(ctx) - require.NoError(t, err) - if assert.NotNil(t, healthResponse) { - require.Equal(t, "ok", healthResponse.Status) - } - - scepClient, err := createSCEPClient(t, fmt.Sprintf("https://localhost:%s/scep/scep", port), m.Root) - require.NoError(t, err) + // instantiate a client for the CA running at the random address + caClient := newCAClient(t, fmt.Sprintf("https://localhost:%s", port), rootFilepath) + requireHealthyCA(t, caClient) + scepClient := createSCEPClient(t, fmt.Sprintf("https://localhost:%s/scep/scep", port), m.Root) cert, err := scepClient.requestCertificate(t, "test.localhost", []string{"test.localhost"}) assert.NoError(t, err) require.NotNil(t, cert) diff --git a/test/integration/scep/decrypter_test.go b/test/integration/scep/decrypter_test.go index 1a2e370a..f59ae8b1 100644 --- a/test/integration/scep/decrypter_test.go +++ b/test/integration/scep/decrypter_test.go @@ -1,7 +1,6 @@ package sceptest import ( - "context" "crypto" "crypto/x509" "crypto/x509/pkix" @@ -111,13 +110,6 @@ func TestIssuesCertificateUsingSCEPWithDecrypter(t *testing.T) { c, err := ca.New(cfg) require.NoError(t, err) - // instantiate a client for the CA running at the random address - caClient, err := ca.NewClient( - fmt.Sprintf("https://localhost:%s", port), - ca.WithRootFile(rootFilepath), - ) - require.NoError(t, err) - var wg sync.WaitGroup wg.Add(1) @@ -127,17 +119,11 @@ func TestIssuesCertificateUsingSCEPWithDecrypter(t *testing.T) { require.ErrorIs(t, err, http.ErrServerClosed) }() - // require OK health response as the baseline - ctx := context.Background() - healthResponse, err := caClient.HealthWithContext(ctx) - require.NoError(t, err) - if assert.NotNil(t, healthResponse) { - require.Equal(t, "ok", healthResponse.Status) - } - - scepClient, err := createSCEPClient(t, fmt.Sprintf("https://localhost:%s/scep/scep", port), m.Root) - require.NoError(t, err) + // instantiate a client for the CA running at the random address + caClient := newCAClient(t, fmt.Sprintf("https://localhost:%s", port), rootFilepath) + requireHealthyCA(t, caClient) + scepClient := createSCEPClient(t, fmt.Sprintf("https://localhost:%s/scep/scep", port), m.Root) cert, err := scepClient.requestCertificate(t, "test.localhost", []string{"test.localhost"}) assert.NoError(t, err) require.NotNil(t, cert) diff --git a/test/integration/scep/regular_cas_test.go b/test/integration/scep/regular_cas_test.go index 0bf9b8b0..ae5ebbfd 100644 --- a/test/integration/scep/regular_cas_test.go +++ b/test/integration/scep/regular_cas_test.go @@ -88,13 +88,6 @@ func TestFailsIssuingCertificateUsingRegularSCEPWithUpstreamCAS(t *testing.T) { c, err := ca.New(cfg) require.NoError(t, err) - // instantiate a client for the CA running at the random address - caClient, err := ca.NewClient( - fmt.Sprintf("https://localhost:%s", port), - ca.WithRootFile(rootFilepath), - ) - require.NoError(t, err) - var wg sync.WaitGroup wg.Add(1) @@ -104,19 +97,13 @@ func TestFailsIssuingCertificateUsingRegularSCEPWithUpstreamCAS(t *testing.T) { require.ErrorIs(t, err, http.ErrServerClosed) }() - // require OK health response as the baseline - ctx := context.Background() - healthResponse, err := caClient.HealthWithContext(ctx) - require.NoError(t, err) - if assert.NotNil(t, healthResponse) { - require.Equal(t, "ok", healthResponse.Status) - } - - scepClient, err := createSCEPClient(t, fmt.Sprintf("https://localhost:%s/scep/scep", port), m.Root) - require.NoError(t, err) + // instantiate a client for the CA running at the random address + caClient := newCAClient(t, fmt.Sprintf("https://localhost:%s", port), rootFilepath) + requireHealthyCA(t, caClient) // issuance is expected to fail when an upstream CAS is configured, as the current // CAS interfaces do not support providing a decrypter. + scepClient := createSCEPClient(t, fmt.Sprintf("https://localhost:%s/scep/scep", port), m.Root) cert, err := scepClient.requestCertificate(t, "test.localhost", []string{"test.localhost"}) assert.Error(t, err) assert.Nil(t, cert) diff --git a/test/integration/scep/regular_test.go b/test/integration/scep/regular_test.go index 500e8370..fc2d4d58 100644 --- a/test/integration/scep/regular_test.go +++ b/test/integration/scep/regular_test.go @@ -1,7 +1,6 @@ package sceptest import ( - "context" "crypto" "encoding/json" "fmt" @@ -79,13 +78,6 @@ func TestIssuesCertificateUsingRegularSCEPConfiguration(t *testing.T) { c, err := ca.New(cfg) require.NoError(t, err) - // instantiate a client for the CA running at the random address - caClient, err := ca.NewClient( - fmt.Sprintf("https://localhost:%s", port), - ca.WithRootFile(rootFilepath), - ) - require.NoError(t, err) - var wg sync.WaitGroup wg.Add(1) @@ -95,17 +87,11 @@ func TestIssuesCertificateUsingRegularSCEPConfiguration(t *testing.T) { require.ErrorIs(t, err, http.ErrServerClosed) }() - // require OK health response as the baseline - ctx := context.Background() - healthResponse, err := caClient.HealthWithContext(ctx) - require.NoError(t, err) - if assert.NotNil(t, healthResponse) { - require.Equal(t, "ok", healthResponse.Status) - } - - scepClient, err := createSCEPClient(t, fmt.Sprintf("https://localhost:%s/scep/scep", port), m.Root) - require.NoError(t, err) + // instantiate a client for the CA running at the random address + caClient := newCAClient(t, fmt.Sprintf("https://localhost:%s", port), rootFilepath) + requireHealthyCA(t, caClient) + scepClient := createSCEPClient(t, fmt.Sprintf("https://localhost:%s/scep/scep", port), m.Root) cert, err := scepClient.requestCertificate(t, "test.localhost", []string{"test.localhost"}) assert.NoError(t, err) require.NotNil(t, cert) From 1e5e267b2beb99beb267a57f42669fe7c8e1139e Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 22 Apr 2024 19:24:04 +0200 Subject: [PATCH 6/6] Remove leftover debug print --- test/integration/scep/common_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration/scep/common_test.go b/test/integration/scep/common_test.go index 60581e64..86f64c3d 100644 --- a/test/integration/scep/common_test.go +++ b/test/integration/scep/common_test.go @@ -131,8 +131,6 @@ func (c *client) getCACert(t *testing.T) error { } c.caCert = cert default: - fmt.Println("body", string(body)) - return fmt.Errorf("unexpected content-type value %q", ct) }