Improve functional coverage of request ID integration test
parent
7fd524f70b
commit
d392c169fc
@ -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…
Reference in New Issue