Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add client-side functions to export multiple authorities #51189

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 128 additions & 47 deletions lib/client/ca_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,16 @@ func (r *ExportAuthoritiesRequest) shouldExportIntegration(ctx context.Context)
}
}

// ExportAuthorities returns the list of authorities in OpenSSH compatible formats as a string.
// 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 returns authorities in OpenSSH compatible formats.
// If the ExportAuthoritiesRequest.AuthType is present only prints keys for CAs of this type,
// otherwise returns host and user SSH keys.
//
Expand All @@ -95,35 +104,99 @@ func (r *ExportAuthoritiesRequest) shouldExportIntegration(ctx context.Context)
// 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.
func ExportAuthorities(ctx context.Context, client authclient.ClientI, req ExportAuthoritiesRequest) (string, error) {
if isIntegration, err := req.shouldExportIntegration(ctx); err != nil {
return "", trace.Wrap(err)
} else if isIntegration {
return exportAuthForIntegration(ctx, client, req)
//
// 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 authority private keys.
// 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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is largely refactored from the old ExportAuthorities and ExportAuthoritiesSecrets bodies.

ctx context.Context,
client authclient.ClientI,
req ExportAuthoritiesRequest,
exportSecrets bool,
) ([]*ExportedAuthority, error) {
var authorities []*ExportedAuthority
switch isIntegration, err := req.shouldExportIntegration(ctx); {
case err != nil:
return nil, trace.Wrap(err)
case isIntegration && exportSecrets:
return nil, trace.NotImplemented("export with secrets is not supported for %q CAs", req.AuthType)
case isIntegration:
authorities, err = exportAuthForIntegration(ctx, client, req)
if err != nil {
return nil, trace.Wrap(err)
}
default:
authorities, err = exportAuth(ctx, client, req, exportSecrets)
if err != nil {
return nil, trace.Wrap(err)
}
}
return exportAuth(ctx, client, req, false /* exportSecrets */)

// 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) {
// 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) {
if isIntegration, err := req.shouldExportIntegration(ctx); err != nil {
// TODO(codingllama): Remove ExportAuthoritiesSecrets.
return exportAuthorities(ctx, client, req, ExportAllAuthoritiesSecrets)
}

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)
} else if isIntegration {
return "", trace.NotImplemented("export with secrets is not supported for %q CAs", req.AuthType)
}
return exportAuth(ctx, client, req, true /* exportSecrets */)
// 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) (string, error) {
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 @@ -205,13 +278,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 @@ -220,7 +293,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 @@ -236,7 +309,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 @@ -254,7 +327,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 @@ -267,7 +340,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 @@ -282,18 +355,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 @@ -302,10 +377,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 @@ -314,29 +389,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 Expand Up @@ -375,21 +454,23 @@ func hostCAFormat(ca types.CertAuthority, keyBytes []byte, client authclient.Cli
})
}

func exportAuthForIntegration(ctx context.Context, client authclient.ClientI, req ExportAuthoritiesRequest) (string, error) {
func exportAuthForIntegration(ctx context.Context, client authclient.ClientI, req ExportAuthoritiesRequest) ([]*ExportedAuthority, error) {
switch req.AuthType {
case "github":
keySet, err := fetchIntegrationCAKeySet(ctx, client, req.Integration)
if err != nil {
return "", trace.Wrap(err)
return nil, trace.Wrap(err)
}
ret, err := exportGitHubCAs(keySet, req)
if err != nil {
return "", trace.Wrap(err)
return nil, trace.Wrap(err)
}
return ret, nil
return []*ExportedAuthority{
{Data: []byte(ret)},
}, nil

default:
return "", trace.BadParameter("unknown integration CA type %q", req.AuthType)
return nil, trace.BadParameter("unknown integration CA type %q", req.AuthType)
}
}

Expand Down
Loading
Loading