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

Merged
merged 6 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from 5 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
177 changes: 130 additions & 47 deletions lib/client/ca_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,17 @@ 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 all authorities of a particular type in OpenSSH
codingllama marked this conversation as resolved.
Show resolved Hide resolved
// 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 +105,100 @@ 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 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(
codingllama marked this conversation as resolved.
Show resolved Hide resolved
ctx context.Context,
client authclient.ClientI,
req ExportAuthoritiesRequest,
exportSecrets bool,
) ([]*ExportedAuthority, error) {
var authorities []*ExportedAuthority
switch isIntegration, err := req.shouldExportIntegration(ctx); {
codingllama marked this conversation as resolved.
Show resolved Hide resolved
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)
}
hugoShaka marked this conversation as resolved.
Show resolved Hide resolved

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 +280,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 +295,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 +311,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 +329,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 +342,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 +357,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 +379,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 +391,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 +456,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