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() +}