Skip to content

Commit

Permalink
Test ExportAll with multiple active CAs
Browse files Browse the repository at this point in the history
  • Loading branch information
codingllama committed Jan 21, 2025
1 parent 930fa71 commit ac7524a
Showing 1 changed file with 198 additions and 2 deletions.
200 changes: 198 additions & 2 deletions lib/client/ca_export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,27 @@ package client

import (
"context"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"

"github.com/gravitational/teleport/api/client/proto"
clientpb "github.com/gravitational/teleport/api/client/proto"
integrationpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1"
"github.com/gravitational/teleport/api/mfa"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/cryptosuites"
"github.com/gravitational/teleport/lib/fixtures"
)

Expand All @@ -56,7 +62,7 @@ func (m *mockAuthClient) GetCertAuthority(ctx context.Context, id types.CertAuth
return m.server.GetCertAuthority(ctx, id, loadKeys)
}

func (m *mockAuthClient) PerformMFACeremony(ctx context.Context, challengeRequest *proto.CreateAuthenticateChallengeRequest, promptOpts ...mfa.PromptOpt) (*proto.MFAAuthenticateResponse, error) {
func (m *mockAuthClient) PerformMFACeremony(ctx context.Context, challengeRequest *clientpb.CreateAuthenticateChallengeRequest, promptOpts ...mfa.PromptOpt) (*clientpb.MFAAuthenticateResponse, error) {
// return MFA not required to gracefully skip the MFA prompt.
return nil, &mfa.ErrMFANotRequired
}
Expand Down Expand Up @@ -353,3 +359,193 @@ func TestExportAuthorities(t *testing.T) {
})
}
}

// Tests a scenario similar to
// https://github.com/gravitational/teleport/issues/35444.
func TestExportAllAuthorities_mutipleActiveKeys(t *testing.T) {
t.Parallel()

softwareKey, err := cryptosuites.GeneratePrivateKeyWithAlgorithm(cryptosuites.ECDSAP256)
require.NoError(t, err, "GeneratePrivateKeyWithAlgorithm errored")
// Typically the HSM key would be RSA2048, but this is fine for testing
// purposes.
hsmKey, err := cryptosuites.GeneratePrivateKeyWithAlgorithm(cryptosuites.ECDSAP256)
require.NoError(t, err, "GeneratePrivateKeyWithAlgorithm errored")

makeSerialNumber := func() func() *big.Int {
lastSerialNumber := int64(0)
return func() *big.Int {
lastSerialNumber++
return big.NewInt(lastSerialNumber)
}
}()

const clusterName = "zarq" // fake, doesn't matter for this test.
makeKeyPairs := func(t *testing.T, key *keys.PrivateKey, keyType types.PrivateKeyType) (sshKP *types.SSHKeyPair, tlsPEM, tlsDER *types.TLSKeyPair) {
sshPriv, err := key.MarshalSSHPrivateKey()
require.NoError(t, err, "MarshalSSHPrivateKey errored")
sshKP = &types.SSHKeyPair{
PublicKey: key.MarshalSSHPublicKey(),
PrivateKey: sshPriv,
PrivateKeyType: keyType,
}

serialNumber := makeSerialNumber()
subject := pkix.Name{
Organization: []string{clusterName},
SerialNumber: serialNumber.String(),
CommonName: clusterName,
}
now := time.Now()
// template mimics an actual user CA certificate.
template := &x509.Certificate{
SerialNumber: serialNumber,
Issuer: subject,
Subject: subject,
NotBefore: now.Add(-1 * time.Second),
NotAfter: now.Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
}
x509CertDER, err := x509.CreateCertificate(rand.Reader, template, template /* parent */, key.Public(), key.Signer)
require.NoError(t, err, "CreateCertificate errored")
x509CertPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: x509CertDER,
})
tlsPEM = &types.TLSKeyPair{
Cert: x509CertPEM,
Key: key.PrivateKeyPEM(),
KeyType: keyType,
}

block, _ := pem.Decode(tlsPEM.Key)
require.NotNil(t, block, "pem.Decode returned nil block")
// Note that typically types.TLSKeyPair doesn't hold raw/DER data, this is
// only used for test convenience.
tlsDER = &types.TLSKeyPair{
Cert: x509CertDER,
Key: block.Bytes,
KeyType: keyType,
}

return sshKP, tlsPEM, tlsDER
}

softKeySSH, softKeyPEM, softKeyDER := makeKeyPairs(t, softwareKey, types.PrivateKeyType_RAW)
hsmKeySSH, hsmKeyPEM, hsmKeyDER := makeKeyPairs(t, hsmKey, types.PrivateKeyType_PKCS11)
userCA, err := types.NewCertAuthority(types.CertAuthoritySpecV2{
Type: "user",
ClusterName: clusterName,
ActiveKeys: types.CAKeySet{
SSH: []*types.SSHKeyPair{
softKeySSH,
hsmKeySSH,
},
TLS: []*types.TLSKeyPair{
softKeyPEM,
hsmKeyPEM,
},
},
})
require.NoError(t, err, "NewCertAuthority(user) errored")

authClient := &multiCAAuthClient{
ClientI: nil,
clusterName: clusterName,
certAuthorities: []types.CertAuthority{userCA},
}
ctx := context.Background()

tests := []struct {
name string
req *ExportAuthoritiesRequest
wantPublic, wantPrivate []*ExportedAuthority
}{
{
name: "tls-user",
req: &ExportAuthoritiesRequest{
AuthType: "tls-user",
},
wantPublic: []*ExportedAuthority{
{Data: softKeyPEM.Cert},
{Data: hsmKeyPEM.Cert},
},
wantPrivate: []*ExportedAuthority{
{Data: softKeyPEM.Key},
{Data: hsmKeyPEM.Key},
},
},
{
name: "windows",
req: &ExportAuthoritiesRequest{
AuthType: "windows",
},
wantPublic: []*ExportedAuthority{
{Data: softKeyDER.Cert},
{Data: hsmKeyDER.Cert},
},
wantPrivate: []*ExportedAuthority{
{Data: softKeyDER.Key},
{Data: hsmKeyDER.Key},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()

runTest := func(
t *testing.T,
exportAllFunc func(context.Context, authclient.ClientI, ExportAuthoritiesRequest) ([]*ExportedAuthority, error),
want []*ExportedAuthority,
) {
got, err := exportAllFunc(ctx, authClient, *test.req)
require.NoError(t, err, "exportAllFunc errored")
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("Authorities mismatch (-want +got)\n%s", diff)
}
}

t.Run("ExportAllAuthorities", func(t *testing.T) {
runTest(t, ExportAllAuthorities, test.wantPublic)
})
t.Run("ExportAllAuthoritiesSecrets", func(t *testing.T) {
runTest(t, ExportAllAuthoritiesSecrets, test.wantPrivate)
})
})
}
}

type multiCAAuthClient struct {
authclient.ClientI

clusterName string
certAuthorities []types.CertAuthority
}

func (m *multiCAAuthClient) GetDomainName(context.Context) (string, error) {
return m.clusterName, nil
}

func (m *multiCAAuthClient) GetCertAuthority(_ context.Context, id types.CertAuthID, loadKeys bool) (types.CertAuthority, error) {
for _, ca := range m.certAuthorities {
if ca.GetType() == id.Type && ca.GetClusterName() == id.DomainName {
if !loadKeys {
ca = ca.WithoutSecrets().(types.CertAuthority)
}
return ca, nil
}
}
return nil, nil
}

func (m *multiCAAuthClient) PerformMFACeremony(
context.Context,
*clientpb.CreateAuthenticateChallengeRequest,
...mfa.PromptOpt,
) (*clientpb.MFAAuthenticateResponse, error) {
// Skip MFA ceremonies.
return nil, &mfa.ErrMFANotRequired
}

0 comments on commit ac7524a

Please sign in to comment.