Improve functional coverage of request ID integration test

pull/1743/head
Herman Slatman 2 months ago
parent 7fd524f70b
commit d392c169fc
No known key found for this signature in database
GPG Key ID: F4D8A44EA0A75A4F

@ -17,13 +17,15 @@ import (
"testing"
"time"
"github.com/smallstep/certificates/internal/requestid"
"github.com/smallstep/certificates/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/x509util"
"go.step.sm/linkedca"
"github.com/smallstep/certificates/internal/requestid"
"github.com/smallstep/certificates/webhook"
)
func TestWebhookController_isCertTypeOK(t *testing.T) {
@ -103,7 +105,8 @@ func TestWebhookController_isCertTypeOK(t *testing.T) {
// withRequestID is a helper that calls into [requestid.NewContext] and returns
// a new context with the requestID added.
func withRequestID(ctx context.Context, requestID string) context.Context {
func withRequestID(t *testing.T, ctx context.Context, requestID string) context.Context {
t.Helper()
return requestid.NewContext(ctx, requestID)
}
@ -138,7 +141,7 @@ func TestWebhookController_Enrich(t *testing.T) {
webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}},
TemplateData: x509util.TemplateData{},
},
ctx: withRequestID(context.Background(), "reqID"),
ctx: withRequestID(t, context.Background(), "reqID"),
req: &webhook.RequestBody{},
responses: []*webhook.ResponseBody{{Allow: true, Data: map[string]any{"role": "bar"}}},
expectErr: false,
@ -153,7 +156,7 @@ func TestWebhookController_Enrich(t *testing.T) {
},
TemplateData: x509util.TemplateData{},
},
ctx: withRequestID(context.Background(), "reqID"),
ctx: withRequestID(t, context.Background(), "reqID"),
req: &webhook.RequestBody{},
responses: []*webhook.ResponseBody{
{Allow: true, Data: map[string]any{"role": "bar"}},
@ -177,7 +180,7 @@ func TestWebhookController_Enrich(t *testing.T) {
TemplateData: x509util.TemplateData{},
certType: linkedca.Webhook_X509,
},
ctx: withRequestID(context.Background(), "reqID"),
ctx: withRequestID(t, context.Background(), "reqID"),
req: &webhook.RequestBody{},
responses: []*webhook.ResponseBody{
{Allow: true, Data: map[string]any{"role": "bar"}},
@ -197,7 +200,7 @@ func TestWebhookController_Enrich(t *testing.T) {
TemplateData: x509util.TemplateData{},
options: []webhook.RequestBodyOption{webhook.WithX5CCertificate(cert)},
},
ctx: withRequestID(context.Background(), "reqID"),
ctx: withRequestID(t, context.Background(), "reqID"),
req: &webhook.RequestBody{},
responses: []*webhook.ResponseBody{{Allow: true, Data: map[string]any{"role": "bar"}}},
expectErr: false,
@ -220,7 +223,7 @@ func TestWebhookController_Enrich(t *testing.T) {
webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}},
TemplateData: x509util.TemplateData{},
},
ctx: withRequestID(context.Background(), "reqID"),
ctx: withRequestID(t, context.Background(), "reqID"),
req: &webhook.RequestBody{},
responses: []*webhook.ResponseBody{{Allow: false}},
expectErr: true,
@ -235,7 +238,7 @@ func TestWebhookController_Enrich(t *testing.T) {
PublicKey: []byte("bad"),
})},
},
ctx: withRequestID(context.Background(), "reqID"),
ctx: withRequestID(t, context.Background(), "reqID"),
req: &webhook.RequestBody{},
responses: []*webhook.ResponseBody{{Allow: false}},
expectErr: true,
@ -296,7 +299,7 @@ func TestWebhookController_Authorize(t *testing.T) {
client: http.DefaultClient,
webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}},
},
ctx: withRequestID(context.Background(), "reqID"),
ctx: withRequestID(t, context.Background(), "reqID"),
req: &webhook.RequestBody{},
responses: []*webhook.ResponseBody{{Allow: true}},
expectErr: false,
@ -307,7 +310,7 @@ func TestWebhookController_Authorize(t *testing.T) {
webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING", CertType: linkedca.Webhook_X509.String()}},
certType: linkedca.Webhook_SSH,
},
ctx: withRequestID(context.Background(), "reqID"),
ctx: withRequestID(t, context.Background(), "reqID"),
req: &webhook.RequestBody{},
responses: []*webhook.ResponseBody{{Allow: false}},
expectErr: false,
@ -318,7 +321,7 @@ func TestWebhookController_Authorize(t *testing.T) {
webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}},
options: []webhook.RequestBodyOption{webhook.WithX5CCertificate(cert)},
},
ctx: withRequestID(context.Background(), "reqID"),
ctx: withRequestID(t, context.Background(), "reqID"),
req: &webhook.RequestBody{},
responses: []*webhook.ResponseBody{{Allow: true}},
expectErr: false,
@ -339,7 +342,7 @@ func TestWebhookController_Authorize(t *testing.T) {
client: http.DefaultClient,
webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}},
},
ctx: withRequestID(context.Background(), "reqID"),
ctx: withRequestID(t, context.Background(), "reqID"),
req: &webhook.RequestBody{},
responses: []*webhook.ResponseBody{{Allow: false}},
expectErr: true,
@ -352,7 +355,7 @@ func TestWebhookController_Authorize(t *testing.T) {
PublicKey: []byte("bad"),
})},
},
ctx: withRequestID(context.Background(), "reqID"),
ctx: withRequestID(t, context.Background(), "reqID"),
req: &webhook.RequestBody{},
responses: []*webhook.ResponseBody{{Allow: false}},
expectErr: true,
@ -568,7 +571,7 @@ func TestWebhook_Do(t *testing.T) {
ctx := context.Background()
if tc.requestID != "" {
ctx = withRequestID(context.Background(), tc.requestID)
ctx = withRequestID(t, ctx, tc.requestID)
}
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()

@ -2,17 +2,17 @@ package client
import "context"
type requestIDKey struct{}
type contextKey struct{}
// NewRequestIDContext returns a new context with the given request ID added to the
// context.
func NewRequestIDContext(ctx context.Context, requestID string) context.Context {
return context.WithValue(ctx, requestIDKey{}, requestID)
return context.WithValue(ctx, contextKey{}, requestID)
}
// RequestIDFromContext returns the request ID from the context if it exists.
// and is not empty.
func RequestIDFromContext(ctx context.Context) (string, bool) {
v, ok := ctx.Value(requestIDKey{}).(string)
v, ok := ctx.Value(contextKey{}).(string)
return v, ok && v != ""
}

@ -7,6 +7,8 @@ import (
"testing"
"time"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/x509util"
@ -41,14 +43,12 @@ func getTestProvisioner(t *testing.T, caURL string) *Provisioner {
}
func TestNewProvisioner(t *testing.T) {
ca := startCATestServer()
ca := startCATestServer(t)
defer ca.Close()
want := getTestProvisioner(t, ca.URL)
caBundle, err := os.ReadFile("testdata/secrets/root_ca.crt")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
type args struct {
name string

@ -10,6 +10,8 @@ import (
"sort"
"testing"
"github.com/stretchr/testify/require"
"github.com/smallstep/certificates/api"
)
@ -196,23 +198,17 @@ func TestAddClientCA(t *testing.T) {
//nolint:gosec // test tls config
func TestAddRootsToRootCAs(t *testing.T) {
ca := startCATestServer()
ca := startCATestServer(t)
defer ca.Close()
client, err := NewClient(ca.URL, WithRootFile("testdata/secrets/root_ca.crt"))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
clientFail, err := NewClient(ca.URL, WithTransport(http.DefaultTransport))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
root, err := os.ReadFile("testdata/secrets/root_ca.crt")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
cert := parseCertificate(t, string(root))
pool := x509.NewCertPool()
@ -251,23 +247,17 @@ func TestAddRootsToRootCAs(t *testing.T) {
//nolint:gosec // test tls config
func TestAddRootsToClientCAs(t *testing.T) {
ca := startCATestServer()
ca := startCATestServer(t)
defer ca.Close()
client, err := NewClient(ca.URL, WithRootFile("testdata/secrets/root_ca.crt"))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
clientFail, err := NewClient(ca.URL, WithTransport(http.DefaultTransport))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
root, err := os.ReadFile("testdata/secrets/root_ca.crt")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
cert := parseCertificate(t, string(root))
pool := x509.NewCertPool()
@ -306,28 +296,20 @@ func TestAddRootsToClientCAs(t *testing.T) {
//nolint:gosec // test tls config
func TestAddFederationToRootCAs(t *testing.T) {
ca := startCATestServer()
ca := startCATestServer(t)
defer ca.Close()
client, err := NewClient(ca.URL, WithRootFile("testdata/secrets/root_ca.crt"))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
clientFail, err := NewClient(ca.URL, WithTransport(http.DefaultTransport))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
root, err := os.ReadFile("testdata/secrets/root_ca.crt")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
federated, err := os.ReadFile("testdata/secrets/federated_ca.crt")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
crt1 := parseCertificate(t, string(root))
crt2 := parseCertificate(t, string(federated))
@ -371,28 +353,20 @@ func TestAddFederationToRootCAs(t *testing.T) {
//nolint:gosec // test tls config
func TestAddFederationToClientCAs(t *testing.T) {
ca := startCATestServer()
ca := startCATestServer(t)
defer ca.Close()
client, err := NewClient(ca.URL, WithRootFile("testdata/secrets/root_ca.crt"))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
clientFail, err := NewClient(ca.URL, WithTransport(http.DefaultTransport))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
root, err := os.ReadFile("testdata/secrets/root_ca.crt")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
federated, err := os.ReadFile("testdata/secrets/federated_ca.crt")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
crt1 := parseCertificate(t, string(root))
crt2 := parseCertificate(t, string(federated))
@ -436,23 +410,17 @@ func TestAddFederationToClientCAs(t *testing.T) {
//nolint:gosec // test tls config
func TestAddRootsToCAs(t *testing.T) {
ca := startCATestServer()
ca := startCATestServer(t)
defer ca.Close()
client, err := NewClient(ca.URL, WithRootFile("testdata/secrets/root_ca.crt"))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
clientFail, err := NewClient(ca.URL, WithTransport(http.DefaultTransport))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
root, err := os.ReadFile("testdata/secrets/root_ca.crt")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
cert := parseCertificate(t, string(root))
pool := x509.NewCertPool()
@ -491,28 +459,20 @@ func TestAddRootsToCAs(t *testing.T) {
//nolint:gosec // test tls config
func TestAddFederationToCAs(t *testing.T) {
ca := startCATestServer()
ca := startCATestServer(t)
defer ca.Close()
client, err := NewClient(ca.URL, WithRootFile("testdata/secrets/root_ca.crt"))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
clientFail, err := NewClient(ca.URL, WithTransport(http.DefaultTransport))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
root, err := os.ReadFile("testdata/secrets/root_ca.crt")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
federated, err := os.ReadFile("testdata/secrets/federated_ca.crt")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
crt1 := parseCertificate(t, string(root))
crt2 := parseCertificate(t, string(federated))

@ -17,27 +17,28 @@ import (
"testing"
"time"
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/authority"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/randutil"
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/authority"
)
func generateOTT(subject string) string {
func generateOTT(t *testing.T, subject string) string {
t.Helper()
now := time.Now()
jwk, err := jose.ReadKey("testdata/secrets/ott_mariano_priv.jwk", jose.WithPassword([]byte("password")))
if err != nil {
panic(err)
}
require.NoError(t, err)
opts := new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID)
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, opts)
if err != nil {
panic(err)
}
require.NoError(t, err)
id, err := randutil.ASCII(64)
if err != nil {
panic(err)
}
require.NoError(t, err)
cl := struct {
jose.Claims
SANS []string `json:"sans"`
@ -53,9 +54,8 @@ func generateOTT(subject string) string {
SANS: []string{subject},
}
raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
if err != nil {
panic(err)
}
require.NoError(t, err)
return raw
}
@ -72,32 +72,28 @@ func startTestServer(baseContext context.Context, tlsConfig *tls.Config, handler
return srv
}
func startCATestServer() *httptest.Server {
func startCATestServer(t *testing.T) *httptest.Server {
config, err := authority.LoadConfiguration("testdata/ca.json")
if err != nil {
panic(err)
}
require.NoError(t, err)
ca, err := New(config)
if err != nil {
panic(err)
}
require.NoError(t, err)
// Use a httptest.Server instead
baseContext := buildContext(ca.auth, nil, nil, nil)
srv := startTestServer(baseContext, ca.srv.TLSConfig, ca.srv.Handler)
return srv
}
func sign(domain string) (*Client, *api.SignResponse, crypto.PrivateKey) {
srv := startCATestServer()
func sign(t *testing.T, domain string) (*Client, *api.SignResponse, crypto.PrivateKey) {
t.Helper()
srv := startCATestServer(t)
defer srv.Close()
return signDuration(srv, domain, 0)
return signDuration(t, srv, domain, 0)
}
func signDuration(srv *httptest.Server, domain string, duration time.Duration) (*Client, *api.SignResponse, crypto.PrivateKey) {
req, pk, err := CreateSignRequest(generateOTT(domain))
if err != nil {
panic(err)
}
func signDuration(t *testing.T, srv *httptest.Server, domain string, duration time.Duration) (*Client, *api.SignResponse, crypto.PrivateKey) {
t.Helper()
req, pk, err := CreateSignRequest(generateOTT(t, domain))
require.NoError(t, err)
if duration > 0 {
req.NotBefore = api.NewTimeDuration(time.Now())
@ -105,13 +101,11 @@ func signDuration(srv *httptest.Server, domain string, duration time.Duration) (
}
client, err := NewClient(srv.URL, WithRootFile("testdata/secrets/root_ca.crt"))
if err != nil {
panic(err)
}
require.NoError(t, err)
sr, err := client.Sign(req)
if err != nil {
panic(err)
}
require.NoError(t, err)
return client, sr, pk
}
@ -145,7 +139,7 @@ func serverHandler(t *testing.T, clientDomain string) http.Handler {
func TestClient_GetServerTLSConfig_http(t *testing.T) {
clientDomain := "test.domain"
client, sr, pk := sign("127.0.0.1")
client, sr, pk := sign(t, "127.0.0.1")
// Create mTLS server
ctx, cancel := context.WithCancel(context.Background())
@ -212,7 +206,7 @@ func TestClient_GetServerTLSConfig_http(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, sr, pk := sign(clientDomain)
client, sr, pk := sign(t, clientDomain)
cli := tt.getClient(t, client, sr, pk)
if cli == nil {
return
@ -246,19 +240,18 @@ func TestClient_GetServerTLSConfig_renew(t *testing.T) {
defer reset()
// Start CA
ca := startCATestServer()
ca := startCATestServer(t)
defer ca.Close()
clientDomain := "test.domain"
client, sr, pk := signDuration(ca, "127.0.0.1", 5*time.Second)
client, sr, pk := signDuration(t, ca, "127.0.0.1", 5*time.Second)
// Start mTLS server
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tlsConfig, err := client.GetServerTLSConfig(ctx, sr, pk)
if err != nil {
t.Fatalf("Client.GetServerTLSConfig() error = %v", err)
}
require.NoError(t, err)
srvMTLS := startTestServer(context.Background(), tlsConfig, serverHandler(t, clientDomain))
defer srvMTLS.Close()
@ -266,30 +259,26 @@ func TestClient_GetServerTLSConfig_renew(t *testing.T) {
ctx, cancel = context.WithCancel(context.Background())
defer cancel()
tlsConfig, err = client.GetServerTLSConfig(ctx, sr, pk, VerifyClientCertIfGiven())
if err != nil {
t.Fatalf("Client.GetServerTLSConfig() error = %v", err)
}
require.NoError(t, err)
srvTLS := startTestServer(context.Background(), tlsConfig, serverHandler(t, clientDomain))
defer srvTLS.Close()
// Transport
client, sr, pk = signDuration(ca, clientDomain, 5*time.Second)
client, sr, pk = signDuration(t, ca, clientDomain, 5*time.Second)
tr1, err := client.Transport(context.Background(), sr, pk)
if err != nil {
t.Fatalf("Client.Transport() error = %v", err)
}
require.NoError(t, err)
// Transport with tlsConfig
client, sr, pk = signDuration(ca, clientDomain, 5*time.Second)
client, sr, pk = signDuration(t, ca, clientDomain, 5*time.Second)
tlsConfig, err = client.GetClientTLSConfig(context.Background(), sr, pk)
if err != nil {
t.Fatalf("Client.GetClientTLSConfig() error = %v", err)
}
require.NoError(t, err)
tr2 := getDefaultTransport(tlsConfig)
// No client cert
root, err := RootCertificate(sr)
if err != nil {
t.Fatalf("RootCertificate() error = %v", err)
}
require.NoError(t, err)
tlsConfig = getDefaultTLSConfig(sr)
tlsConfig.RootCAs = x509.NewCertPool()
tlsConfig.RootCAs.AddCert(root)

@ -5,6 +5,7 @@ import (
"net/http"
"github.com/rs/xid"
"go.step.sm/crypto/randutil"
)
@ -74,17 +75,17 @@ func newRequestID() string {
return requestID
}
type requestIDKey struct{}
type contextKey struct{}
// NewContext returns a new context with the given request ID added to the
// context.
func NewContext(ctx context.Context, requestID string) context.Context {
return context.WithValue(ctx, requestIDKey{}, requestID)
return context.WithValue(ctx, contextKey{}, requestID)
}
// FromContext returns the request ID from the context if it exists and
// is not the empty value.
func FromContext(ctx context.Context) (string, bool) {
v, ok := ctx.Value(requestIDKey{}).(string)
v, ok := ctx.Value(contextKey{}).(string)
return v, ok && v != ""
}

@ -19,11 +19,15 @@ func newRequest(t *testing.T) *http.Request {
func Test_Middleware(t *testing.T) {
requestWithID := newRequest(t)
requestWithID.Header.Set("X-Request-Id", "reqID")
requestWithoutID := newRequest(t)
requestWithEmptyHeader := newRequest(t)
requestWithEmptyHeader.Header.Set("X-Request-Id", "")
requestWithSmallstepID := newRequest(t)
requestWithSmallstepID.Header.Set("X-Smallstep-Id", "smallstepID")
tests := []struct {
name string
traceHeader string

@ -2,19 +2,19 @@ package userid
import "context"
type userIDKey struct{}
type contextKey struct{}
// NewContext returns a new context with the given user ID added to the
// context.
// TODO(hs): this doesn't seem to be used / set currently; implement
// when/where it makes sense.
func NewContext(ctx context.Context, userID string) context.Context {
return context.WithValue(ctx, userIDKey{}, userID)
return context.WithValue(ctx, contextKey{}, userID)
}
// FromContext returns the user ID from the context if it exists
// and is not empty.
func FromContext(ctx context.Context) (string, bool) {
v, ok := ctx.Value(userIDKey{}).(string)
v, ok := ctx.Value(contextKey{}).(string)
return v, ok && v != ""
}

@ -9,6 +9,7 @@ import (
"time"
"github.com/sirupsen/logrus"
"github.com/smallstep/certificates/internal/requestid"
"github.com/smallstep/certificates/internal/userid"
)

@ -9,6 +9,7 @@ import (
"github.com/newrelic/go-agent/v3/newrelic"
"github.com/pkg/errors"
"github.com/smallstep/certificates/internal/requestid"
"github.com/smallstep/certificates/logging"
)

@ -1,132 +0,0 @@
package e2e
import (
"context"
"encoding/json"
"fmt"
"net"
"path/filepath"
"sync"
"testing"
"github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/ca"
"github.com/smallstep/certificates/ca/client"
"github.com/smallstep/certificates/errs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/minica"
"go.step.sm/crypto/pemutil"
)
// 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
}
func Test_reflectRequestID(t *testing.T) {
dir := t.TempDir()
m, err := minica.New(minica.WithName("Step E2E"))
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)
cfg := &config.Config{
Root: []string{rootFilepath},
IntermediateCert: intermediateCertFilepath,
IntermediateKey: intermediateKeyFilepath,
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",
DeploymentType: "standalone-test",
},
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.Error(t, err) // expect error when server is stopped
}()
// 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)
}
// expect an error when retrieving an invalid root
rootResponse, err := caClient.RootWithContext(ctx, "invalid")
if assert.Error(t, err) {
apiErr := &errs.Error{}
if assert.ErrorAs(t, err, &apiErr) {
assert.Equal(t, 404, apiErr.StatusCode())
assert.Equal(t, "The requested resource could not be found. Please see the certificate authority logs for more info.", apiErr.Err.Error())
assert.NotEmpty(t, apiErr.RequestID)
// TODO: include the below error in the JSON? It's currently only output to the CA logs. Also see https://github.com/smallstep/certificates/pull/759
//assert.Equal(t, "/root/invalid was not found: certificate with fingerprint invalid was not found", apiErr.Msg)
}
}
assert.Nil(t, rootResponse)
// expect an error when retrieving an invalid root and provided request ID
rootResponse, err = caClient.RootWithContext(client.NewRequestIDContext(ctx, "reqID"), "invalid")
if assert.Error(t, err) {
apiErr := &errs.Error{}
if assert.ErrorAs(t, err, &apiErr) {
assert.Equal(t, 404, apiErr.StatusCode())
assert.Equal(t, "The requested resource could not be found. Please see the certificate authority logs for more info.", apiErr.Err.Error())
assert.Equal(t, "reqID", apiErr.RequestID)
}
}
assert.Nil(t, rootResponse)
// done testing; stop and wait for the server to quit
err = c.Stop()
require.NoError(t, err)
wg.Wait()
}

@ -0,0 +1,289 @@
package integration
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"path/filepath"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/minica"
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/randutil"
"go.step.sm/crypto/x509util"
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/ca"
"github.com/smallstep/certificates/ca/client"
"github.com/smallstep/certificates/errs"
)
// 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
}
func Test_reflectRequestID(t *testing.T) {
dir := t.TempDir()
m, err := minica.New(minica.WithName("Step E2E"))
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)
authorizingSrv := newAuthorizingServer(t, m)
defer authorizingSrv.Close()
authorizingSrv.StartTLS()
password := []byte("1234")
jwk, jwe, err := jose.GenerateDefaultKeyPair(password)
require.NoError(t, err)
encryptedKey, err := jwe.CompactSerialize()
require.NoError(t, err)
prov := &provisioner.JWK{
ID: "jwk",
Name: "jwk",
Type: "JWK",
Key: jwk,
EncryptedKey: encryptedKey,
Claims: &config.GlobalProvisionerClaims,
Options: &provisioner.Options{
Webhooks: []*provisioner.Webhook{
{
ID: "webhook",
Name: "webhook-test",
URL: fmt.Sprintf("%s/authorize", authorizingSrv.URL),
Kind: "AUTHORIZING",
CertType: "X509",
},
},
},
}
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"
DNSNames: []string{"127.0.0.1", "[::1]", "localhost"},
AuthorityConfig: &config.AuthConfig{
AuthorityID: "stepca-test",
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)
}
// expect an error when retrieving an invalid root
rootResponse, err := caClient.RootWithContext(ctx, "invalid")
var firstErr *errs.Error
if assert.ErrorAs(t, err, &firstErr) {
assert.Equal(t, 404, firstErr.StatusCode())
assert.Equal(t, "The requested resource could not be found. Please see the certificate authority logs for more info.", firstErr.Err.Error())
assert.NotEmpty(t, firstErr.RequestID)
// TODO: include the below error in the JSON? It's currently only output to the CA logs. Also see https://github.com/smallstep/certificates/pull/759
//assert.Equal(t, "/root/invalid was not found: certificate with fingerprint invalid was not found", apiErr.Msg)
}
assert.Nil(t, rootResponse)
// expect an error when retrieving an invalid root and provided request ID
rootResponse, err = caClient.RootWithContext(client.NewRequestIDContext(ctx, "reqID"), "invalid")
var secondErr *errs.Error
if assert.ErrorAs(t, err, &secondErr) {
assert.Equal(t, 404, secondErr.StatusCode())
assert.Equal(t, "The requested resource could not be found. Please see the certificate authority logs for more info.", secondErr.Err.Error())
assert.Equal(t, "reqID", secondErr.RequestID)
}
assert.Nil(t, rootResponse)
// prepare a Sign request
subject := "test"
decryptedJWK := decryptPrivateKey(t, jwe, password)
ott := generateOTT(t, decryptedJWK, subject)
signer, err := keyutil.GenerateDefaultSigner()
require.NoError(t, err)
csr, err := x509util.CreateCertificateRequest(subject, []string{subject}, signer)
require.NoError(t, err)
// perform the Sign request using the OTT and CSR
signResponse, err := caClient.SignWithContext(client.NewRequestIDContext(ctx, "signRequestID"), &api.SignRequest{
CsrPEM: api.CertificateRequest{CertificateRequest: csr},
OTT: ott,
NotAfter: api.NewTimeDuration(time.Now().Add(1 * time.Hour)),
NotBefore: api.NewTimeDuration(time.Now().Add(-1 * time.Hour)),
})
assert.NoError(t, err)
// assert a certificate was returned for the subject "test"
if assert.NotNil(t, signResponse) {
assert.Len(t, signResponse.CertChainPEM, 2)
cert, err := x509.ParseCertificate(signResponse.CertChainPEM[0].Raw)
assert.NoError(t, err)
if assert.NotNil(t, cert) {
assert.Equal(t, "test", cert.Subject.CommonName)
assert.Contains(t, cert.DNSNames, "test")
}
}
// done testing; stop and wait for the server to quit
err = c.Stop()
require.NoError(t, err)
wg.Wait()
}
func decryptPrivateKey(t *testing.T, jwe *jose.JSONWebEncryption, pass []byte) *jose.JSONWebKey {
t.Helper()
d, err := jwe.Decrypt(pass)
require.NoError(t, err)
jwk := &jose.JSONWebKey{}
err = json.Unmarshal(d, jwk)
require.NoError(t, err)
return jwk
}
func generateOTT(t *testing.T, jwk *jose.JSONWebKey, subject string) string {
t.Helper()
now := time.Now()
keyID, err := jose.Thumbprint(jwk)
require.NoError(t, err)
opts := new(jose.SignerOptions).WithType("JWT").WithHeader("kid", keyID)
signer, err := jose.NewSigner(jose.SigningKey{Key: jwk.Key}, opts)
require.NoError(t, err)
id, err := randutil.ASCII(64)
require.NoError(t, err)
cl := struct {
jose.Claims
SANS []string `json:"sans"`
}{
Claims: jose.Claims{
ID: id,
Subject: subject,
Issuer: "jwk",
NotBefore: jose.NewNumericDate(now),
Expiry: jose.NewNumericDate(now.Add(time.Minute)),
Audience: []string{"https://127.0.0.1/1.0/sign"},
},
SANS: []string{subject},
}
raw, err := jose.Signed(signer).Claims(cl).CompactSerialize()
require.NoError(t, err)
return raw
}
func newAuthorizingServer(t *testing.T, ca *minica.CA) *httptest.Server {
t.Helper()
key, err := keyutil.GenerateDefaultSigner()
require.NoError(t, err)
csr, err := x509util.CreateCertificateRequest("127.0.0.1", []string{"127.0.0.1"}, key)
require.NoError(t, err)
crt, err := ca.SignCSR(csr)
require.NoError(t, err)
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if assert.Equal(t, "signRequestID", r.Header.Get("X-Request-Id")) {
json.NewEncoder(w).Encode(struct{ Allow bool }{Allow: true})
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusBadRequest)
}))
trustedRoots := x509.NewCertPool()
trustedRoots.AddCert(ca.Root)
srv.TLS = &tls.Config{
Certificates: []tls.Certificate{
{
Certificate: [][]byte{crt.Raw, ca.Intermediate.Raw},
PrivateKey: key,
Leaf: crt,
},
},
ClientCAs: trustedRoots,
ClientAuth: tls.RequireAndVerifyClientCert,
ServerName: "localhost",
}
return srv
}
Loading…
Cancel
Save