Skip to content

Commit

Permalink
[v15] Add client-side functions to export multiple authorities (#51365)
Browse files Browse the repository at this point in the history
* Introduce ExportAll functions

* Refactor tests

* Test ExportAll in existing test suite

* Test ExportAll with multiple active CAs

* Fix TestExportAllAuthorities_mutipleActiveKeys for backport

* Update godocs

* Update godocs
  • Loading branch information
codingllama authored Jan 22, 2025
1 parent 37c4042 commit 5b3cfb7
Show file tree
Hide file tree
Showing 2 changed files with 507 additions and 193 deletions.
152 changes: 116 additions & 36 deletions lib/client/ca_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,21 @@ type ExportAuthoritiesRequest struct {
UseCompatVersion bool
}

// ExportAuthorities returns the list of authorities in OpenSSH compatible formats as a string.
// If the ExportAuthoritiesRequest.AuthType is present only prints keys for CAs of this type,
// otherwise returns host and user SSH keys.
// ExportedAuthority represents an exported authority certificate, as returned
// by [ExportAllAuthorities] or [ExportAllAuthoritiesSecrets].
type ExportedAuthority struct {
// Data is the output of the exported authority.
// May be an SSH authorized key, an SSH known hosts entry, a DER or a PEM,
// depending on the type of the exported authority.
Data []byte
}

// ExportAllAuthorities exports public keys of all authorities of a particular
// type. The export format depends on the authority type, see below for
// details.
//
// An empty ExportAuthoritiesRequest.AuthType is interpreted as an export for
// host and user SSH keys.
//
// Exporting using "tls*", "database", "windows" AuthType:
// Returns the certificate authority public key to be used by systems that rely on TLS.
Expand All @@ -75,25 +87,87 @@ type ExportAuthoritiesRequest struct {
// For example:
// > @cert-authority *.cluster-a ssh-rsa AAA... type=host
// URL encoding is used to pass the CA type and allowed logins into the comment field.
//
// At least one authority is guaranteed on success.
func ExportAllAuthorities(ctx context.Context, client authclient.ClientI, req ExportAuthoritiesRequest) ([]*ExportedAuthority, error) {
const exportSecrets = false
return exportAllAuthorities(ctx, client, req, exportSecrets)
}

// ExportAllAuthoritiesSecrets exports private keys of all authorities of a
// particular type.
// See [ExportAllAuthorities] for more information.
//
// At least one authority is guaranteed on success.
func ExportAllAuthoritiesSecrets(ctx context.Context, client authclient.ClientI, req ExportAuthoritiesRequest) ([]*ExportedAuthority, error) {
const exportSecrets = true
return exportAllAuthorities(ctx, client, req, exportSecrets)
}

func exportAllAuthorities(
ctx context.Context,
client authclient.ClientI,
req ExportAuthoritiesRequest,
exportSecrets bool,
) ([]*ExportedAuthority, error) {
authorities, err := exportAuth(ctx, client, req, exportSecrets)
if err != nil {
return nil, trace.Wrap(err)
}

// Sanity check that we have at least one authority.
// Not expected to happen in practice.
if len(authorities) == 0 {
return nil, trace.BadParameter("export returned zero authorities")
}

return authorities, nil
}

// ExportAuthorities is the single-authority version of [ExportAllAuthorities].
// Soft-deprecated, prefer using [ExportAllAuthorities] and handling exports
// with more than one authority gracefully.
func ExportAuthorities(ctx context.Context, client authclient.ClientI, req ExportAuthoritiesRequest) (string, error) {
return exportAuth(ctx, client, req, false /* exportSecrets */)
// TODO(codingllama): Remove ExportAuthorities.
return exportAuthorities(ctx, client, req, ExportAllAuthorities)
}

// ExportAuthoritiesSecrets exports the Authority Certificate secrets (private keys).
// See ExportAuthorities for more information.
// ExportAuthoritiesSecrets is the single-authority variant of
// [ExportAllAuthoritiesSecrets].
// Soft-deprecated, prefer using [ExportAllAuthoritiesSecrets] and handling
// exports with more than one authority gracefully.
func ExportAuthoritiesSecrets(ctx context.Context, client authclient.ClientI, req ExportAuthoritiesRequest) (string, error) {
return exportAuth(ctx, client, req, true /* exportSecrets */)
// TODO(codingllama): Remove ExportAuthoritiesSecrets.
return exportAuthorities(ctx, client, req, ExportAllAuthoritiesSecrets)
}

func exportAuth(ctx context.Context, client authclient.ClientI, req ExportAuthoritiesRequest, exportSecrets bool) (string, error) {
func exportAuthorities(
ctx context.Context,
client authclient.ClientI,
req ExportAuthoritiesRequest,
exportAllFunc func(context.Context, authclient.ClientI, ExportAuthoritiesRequest) ([]*ExportedAuthority, error),
) (string, error) {
authorities, err := exportAllFunc(ctx, client, req)
if err != nil {
return "", trace.Wrap(err)
}
// At least one authority is guaranteed on success by both ExportAll methods.
if l := len(authorities); l > 1 {
return "", trace.BadParameter("export returned %d authorities, expected exactly one", l)
}

return string(authorities[0].Data), nil
}

func exportAuth(ctx context.Context, client authclient.ClientI, req ExportAuthoritiesRequest, exportSecrets bool) ([]*ExportedAuthority, error) {
var typesToExport []types.CertAuthType

if exportSecrets {
mfaResponse, err := mfa.PerformAdminActionMFACeremony(ctx, client.PerformMFACeremony, true /*allowReuse*/)
if err == nil {
ctx = mfa.ContextWithMFAResponse(ctx, mfaResponse)
} else if !errors.Is(err, &mfa.ErrMFANotRequired) && !errors.Is(err, &mfa.ErrMFANotSupported) {
return "", trace.Wrap(err)
return nil, trace.Wrap(err)
}
}

Expand Down Expand Up @@ -175,13 +249,13 @@ func exportAuth(ctx context.Context, client authclient.ClientI, req ExportAuthor
} else {
authType := types.CertAuthType(req.AuthType)
if err := authType.Check(); err != nil {
return "", trace.Wrap(err)
return nil, trace.Wrap(err)
}
typesToExport = []types.CertAuthType{authType}
}
localAuthName, err := client.GetDomainName(ctx)
if err != nil {
return "", trace.Wrap(err)
return nil, trace.Wrap(err)
}

// fetch authorities via auth API (and only take local CAs, ignoring
Expand All @@ -190,7 +264,7 @@ func exportAuth(ctx context.Context, client authclient.ClientI, req ExportAuthor
for _, at := range typesToExport {
cas, err := client.GetCertAuthorities(ctx, at, exportSecrets)
if err != nil {
return "", trace.Wrap(err)
return nil, trace.Wrap(err)
}
for _, ca := range cas {
if ca.GetClusterName() == localAuthName {
Expand All @@ -206,7 +280,7 @@ func exportAuth(ctx context.Context, client authclient.ClientI, req ExportAuthor
if req.ExportAuthorityFingerprint != "" {
fingerprint, err := sshutils.PrivateKeyFingerprint(key.PrivateKey)
if err != nil {
return "", trace.Wrap(err)
return nil, trace.Wrap(err)
}

if fingerprint != req.ExportAuthorityFingerprint {
Expand All @@ -224,7 +298,7 @@ func exportAuth(ctx context.Context, client authclient.ClientI, req ExportAuthor
if req.ExportAuthorityFingerprint != "" {
fingerprint, err := sshutils.AuthorizedKeyFingerprint(key.PublicKey)
if err != nil {
return "", trace.Wrap(err)
return nil, trace.Wrap(err)
}

if fingerprint != req.ExportAuthorityFingerprint {
Expand All @@ -237,7 +311,7 @@ func exportAuth(ctx context.Context, client authclient.ClientI, req ExportAuthor
if req.UseCompatVersion {
castr, err := hostCAFormat(ca, key.PublicKey, client)
if err != nil {
return "", trace.Wrap(err)
return nil, trace.Wrap(err)
}

ret.WriteString(castr)
Expand All @@ -252,18 +326,20 @@ func exportAuth(ctx context.Context, client authclient.ClientI, req ExportAuthor
case types.HostCA:
castr, err = hostCAFormat(ca, key.PublicKey, client)
default:
return "", trace.BadParameter("unknown user type: %q", ca.GetType())
return nil, trace.BadParameter("unknown user type: %q", ca.GetType())
}
if err != nil {
return "", trace.Wrap(err)
return nil, trace.Wrap(err)
}

// write the export friendly string
ret.WriteString(castr)
}
}

return ret.String(), nil
return []*ExportedAuthority{
{Data: []byte(ret.String())},
}, nil
}

type exportTLSAuthorityRequest struct {
Expand All @@ -272,10 +348,10 @@ type exportTLSAuthorityRequest struct {
ExportPrivateKeys bool
}

func exportTLSAuthority(ctx context.Context, client authclient.ClientI, req exportTLSAuthorityRequest) (string, error) {
func exportTLSAuthority(ctx context.Context, client authclient.ClientI, req exportTLSAuthorityRequest) ([]*ExportedAuthority, error) {
clusterName, err := client.GetDomainName(ctx)
if err != nil {
return "", trace.Wrap(err)
return nil, trace.Wrap(err)
}

certAuthority, err := client.GetCertAuthority(
Expand All @@ -284,29 +360,33 @@ func exportTLSAuthority(ctx context.Context, client authclient.ClientI, req expo
req.ExportPrivateKeys,
)
if err != nil {
return "", trace.Wrap(err)
return nil, trace.Wrap(err)
}

if l := len(certAuthority.GetActiveKeys().TLS); l != 1 {
return "", trace.BadParameter("expected one TLS key pair, got %v", l)
}
keyPair := certAuthority.GetActiveKeys().TLS[0]
activeKeys := certAuthority.GetActiveKeys().TLS
// TODO(codingllama): Export AdditionalTrustedKeys as well?

bytesToExport := keyPair.Cert
if req.ExportPrivateKeys {
bytesToExport = keyPair.Key
}
authorities := make([]*ExportedAuthority, len(activeKeys))
for i, activeKey := range activeKeys {
bytesToExport := activeKey.Cert
if req.ExportPrivateKeys {
bytesToExport = activeKey.Key
}

if !req.UnpackPEM {
return string(bytesToExport), nil
}
if req.UnpackPEM {
block, _ := pem.Decode(bytesToExport)
if block == nil {
return nil, trace.BadParameter("invalid PEM data")
}
bytesToExport = block.Bytes
}

b, _ := pem.Decode(bytesToExport)
if b == nil {
return "", trace.BadParameter("invalid PEM data")
authorities[i] = &ExportedAuthority{
Data: bytesToExport,
}
}

return string(b.Bytes), nil
return authorities, nil
}

// userCAFormat returns the certificate authority public key exported as a single
Expand Down
Loading

0 comments on commit 5b3cfb7

Please sign in to comment.