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

CA Export Format #902

Merged
merged 1 commit into from
Apr 6, 2017
Merged
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
7 changes: 7 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ const (
OIDC = "oidc"
)

const (
// AuthorizedKeys are public keys that check against User CAs.
AuthorizedKeys = "authorized_keys"
// KnownHosts are public keys that check against Host CAs.
KnownHosts = "known_hosts"
)

const (
// CertExtensionPermitAgentForwarding allows agent forwarding for certificate
CertExtensionPermitAgentForwarding = "permit-agent-forwarding"
Expand Down
2 changes: 1 addition & 1 deletion lib/auth/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func Init(cfg InitConfig, dynamicConfig bool) (*AuthServer, *Identity, error) {
if err := asrv.Trust.UpsertCertAuthority(ca, backend.Forever); err != nil {
return nil, nil, trace.Wrap(err)
}
log.Infof("[INIT] Created Trusted Certificate Authority: %v", ca)
log.Infof("[INIT] Created Trusted Certificate Authority: %q, type: %q", ca.GetName(), ca.GetType())
}

for _, tunnel := range cfg.ReverseTunnels {
Expand Down
71 changes: 67 additions & 4 deletions lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,9 +413,42 @@ func ApplyFileConfig(fc *FileConfig, cfg *service.Config) error {
return nil
}

// parseCAKey() gets called for every line in a "CA key file" which is
// the same as 'known_hosts' format for openssh
func parseCAKey(bytes []byte, allowedLogins []string) (services.CertAuthority, services.Role, error) {
// parseAuthorizedKeys parses keys in the authorized_keys format and
// returns a services.CertAuthority.
func parseAuthorizedKeys(bytes []byte, allowedLogins []string) (services.CertAuthority, services.Role, error) {
pubkey, comment, _, _, err := ssh.ParseAuthorizedKey(bytes)
if err != nil {
return nil, nil, trace.Wrap(err)
}

comments, err := url.ParseQuery(comment)
if err != nil {
return nil, nil, trace.Wrap(err)
}
clusterName := comments.Get("clustername")
if clusterName == "" {
return nil, nil, trace.BadParameter("not clustername provided")
}

// create a new certificate authority
ca := services.NewCertAuthority(
services.UserCA,
clusterName,
nil,
[][]byte{ssh.MarshalAuthorizedKey(pubkey)},
allowedLogins)

// transform old allowed logins into roles
role := services.RoleForCertAuthority(ca)
role.SetLogins(allowedLogins)
ca.AddRole(role.GetName())

return ca, role, nil
}

// parseKnownHosts parses keys in known_hosts format and returns a
// services.CertAuthority.
func parseKnownHosts(bytes []byte, allowedLogins []string) (services.CertAuthority, services.Role, error) {
marker, options, pubKey, comment, _, err := ssh.ParseKnownHosts(bytes)
if marker != "cert-authority" {
return nil, nil, trace.BadParameter("invalid file format. expected '@cert-authority` marker")
Expand Down Expand Up @@ -447,6 +480,34 @@ func parseCAKey(bytes []byte, allowedLogins []string) (services.CertAuthority, s
return ca, role, nil
}

// certificateAuthorityFormat parses bytes and determines if they are in
// known_hosts format or authorized_keys format.
func certificateAuthorityFormat(bytes []byte) (string, error) {
_, _, _, _, err := ssh.ParseAuthorizedKey(bytes)
if err != nil {
_, _, _, _, _, err := ssh.ParseKnownHosts(bytes)
if err != nil {
return "", trace.BadParameter("unknown ca format")
}
return teleport.KnownHosts, nil
}
return teleport.AuthorizedKeys, nil
}

// parseCAKey parses bytes either in known_hosts or authorized_keys format
// and returns a services.CertAuthority.
func parseCAKey(bytes []byte, allowedLogins []string) (services.CertAuthority, services.Role, error) {
caFormat, err := certificateAuthorityFormat(bytes)
if err != nil {
return nil, nil, trace.Wrap(err)
}

if caFormat == teleport.AuthorizedKeys {
return parseAuthorizedKeys(bytes, allowedLogins)
}
return parseKnownHosts(bytes, allowedLogins)
}

// readTrustedClusters parses the content of "trusted_clusters" YAML structure
// and modifies Teleport 'conf' by adding "authorities" and "reverse tunnels"
// to it
Expand Down Expand Up @@ -489,7 +550,9 @@ func readTrustedClusters(clusters []TrustedCluster, conf *service.Config) error
ca.GetClusterName())
}
authorities = append(authorities, ca)
roles = append(roles, role)
if role != nil {
roles = append(roles, role)
}
}
conf.Auth.Authorities = append(conf.Auth.Authorities, authorities...)
conf.Auth.Roles = append(conf.Auth.Roles, roles...)
Expand Down
41 changes: 41 additions & 0 deletions lib/config/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package config
import (
"bytes"
"encoding/base64"
"fmt"
"io/ioutil"
"net"
"os"
Expand Down Expand Up @@ -48,6 +49,7 @@ type ConfigTestSuite struct {
}

var _ = check.Suite(&ConfigTestSuite{})
var _ = fmt.Printf

func (s *ConfigTestSuite) SetUpSuite(c *check.C) {
var err error
Expand Down Expand Up @@ -376,6 +378,45 @@ func (s *ConfigTestSuite) TestLegacyU2FTransformation(c *check.C) {
c.Assert(cfg.Auth.U2F.GetFacets(), check.DeepEquals, []string{"https://graviton:3080"})
}

// TestParseKey ensures that keys are parsed correctly if they are in
// authorized_keys format or known_hosts format.
func (s *ConfigTestSuite) TestParseKey(c *check.C) {
tests := []struct {
inCABytes []byte
outType services.CertAuthType
outClusterName string
}{
// 0 - host ca in known_hosts format
{
[]byte(`@cert-authority *.foo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCz+PzY6z2Xa1cMeiJqOH5BRpwY+PlS3Q6C4e3Yj8xjLW1zD3Cehm71zjsYmrpuFTmdylbcKB6CcM6Ft4YbKLG3PTLSKvCPTgfSBk8RCYX02PtOV5ixwa7xl5Gfhc1GRIheXgFO9IT+W9w9ube9r002AGpkMnRRtWAWiZHMGeJoaUoCsjDLDbWsQHj06pr7fD98c7PVcVzCKPTQpadXEP6sF8w417DvypHY1bYsvhRqHw9Njx6T3b9BM3bJ4QXgy18XuO5fCpLjKLsngLwSbqe/1IP4Q0zlUaNOTph3WnjeKJZO9yQeVX1cWDwY4Iz5lSHhsJnQD99hBDdw2RklHU0j type=host`),
services.HostCA,
"foo",
},
// 1 - user ca in known_hosts format (legacy)
{
[]byte(`@cert-authority *.bar ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCfhrvzbHAHrukeDhLSzoXtpctiumao1MQElwhOeuzFRYwrGV/1L2gsx4OJk4ztXKOCpon1FB+dy2aJN0WIr/9qXg37D6K/XJhgDaSfW8cjpl72Lw8kknDpmgSSA3cTvzFNmXfw4DNT/klRwEw6MMrDmfT9QvaV2d35lSoMMeTZ1ilFeJqXdUkY+bgijLBQU5MUjZUfQfS3jpSxVD0DD9D1VbAE1nGSNyFqf34JxJmqJ3R5hfZqNfb9CWouv+uFF99tzOr7tnKM/sQMPGmJ5G+zjTaErNSSLiIU1iCwVKUpNFcGiR1lpOEET+neJVnEeqEqKv2ookkXaIdKjk1UKZEn type=user`),
services.UserCA,
"bar",
},
// 2 - user ca in authorized_keys format
{
[]byte(`cert-authority ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCiIxyz0ctsyQbKLpWVYNF+ZIOrF150Wma2GqkrOWZaOzu5NSnt9Hmp7DaIa2Gn8fh8+8vjP02qp3i43SDOlLyYSn05nJjEXaz7QGysgeppN8ayojl5dkOhA00ROpCl5HhS9cmga7fy1Uwy4jhxenNpfQ5ap0COQi3UrXPepaq8z+I4XQK//qFWnkgyD1VXCnRKXXiajOf3dShYJqLCgwYiViuFmzi2p3lysoYS5eRwTCKiyyBtlkUtpTAse455yGf3QCpe+UOBiJ/4AElxacDndtMkjjctHSPCiztnph1xej64vSy8C2nGsnPIK7RfiOzSEdd5hwva+wPLgNTcKXZz type=user&clustername=baz`),
services.UserCA,
"baz",
},
}

// run tests
for i, tt := range tests {
comment := check.Commentf("Test %v", i)

ca, _, err := parseCAKey(tt.inCABytes, []string{"foo"})
c.Assert(err, check.IsNil, comment)
c.Assert(ca.GetType(), check.Equals, tt.outType)
c.Assert(ca.GetClusterName(), check.Equals, tt.outClusterName)
}
}

func checkStaticConfig(c *check.C, conf *FileConfig) {
c.Assert(conf.AuthToken, check.Equals, "xxxyyy")
c.Assert(conf.SSH.Enabled(), check.Equals, false) // YAML treats 'no' as False
Expand Down
42 changes: 33 additions & 9 deletions tool/tctl/common/tctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type AuthCommand struct {
exportAuthorityFingerprint string
exportPrivateKeys bool
outDir string
compatVersion string
}

type AuthServerCommand struct {
Expand Down Expand Up @@ -217,6 +218,7 @@ func Run() {
authExport := auth.Command("export", "Export CA keys to standard output")
authExport.Flag("keys", "if set, will print private keys").BoolVar(&cmdAuth.exportPrivateKeys)
authExport.Flag("fingerprint", "filter authority by fingerprint").StringVar(&cmdAuth.exportAuthorityFingerprint)
authExport.Flag("compat", "export cerfiticates compatible with specific version of Teleport").StringVar(&cmdAuth.compatVersion)

authGenerate := auth.Command("gen", "Generate a new SSH keypair")
authGenerate.Flag("pub-key", "path to the public key").Required().StringVar(&cmdAuth.genPubPath)
Expand Down Expand Up @@ -613,6 +615,18 @@ func (a *AuthCommand) ExportAuthorities(client *auth.TunClient) error {
continue
}

// export certificates in the old 1.0 format where host and user
// certificate authorities were exported in the known_hosts format.
if a.compatVersion == "1.0" {
castr, err := hostCAFormat(ca, keyBytes, client)
if err != nil {
return trace.Wrap(err)
}

fmt.Println(castr)
continue
}

// export certificate authority in user or host ca format
var castr string
switch ca.GetType() {
Expand All @@ -636,24 +650,34 @@ func (a *AuthCommand) ExportAuthorities(client *auth.TunClient) error {
}

// userCAFormat returns the certificate authority public key exported as a single
// line that can be placed in ~/.ssh/authorized_keys file. For example:
// line that can be placed in ~/.ssh/authorized_keys file. The format adheres to the
// man sshd (8) authorized_keys format, a space-separated list of: options, keytype,
// base64-encoded key, comment.
// For example:
//
// cert-authority AAA... type=user&clustername=cluster-a
//
// cert-authority AAA...
// URL encoding is used to pass the CA type and cluster name into the comment field.
func userCAFormat(ca services.CertAuthority, keyBytes []byte) (string, error) {
return fmt.Sprintf("cert-authority %s", keyBytes), nil
comment := url.Values{
"type": []string{string(services.UserCA)},
"clustername": []string{ca.GetClusterName()},
}

return fmt.Sprintf("cert-authority %s %s", keyBytes, comment.Encode()), nil
}

// hostCAFormat returns the certificate authority public key exported as a single line
// that can be placed in ~/.ssh/authorized_hosts. The format adheres to the man sshd (8)
// authorized_hosts format, a space-separated list of: makrer, hosts, key, and comment.
// authorized_hosts format, a space-separated list of: marker, hosts, key, and comment.
// For example:
//
// @cert-authority *.cluster-a ssh-rsa AAA... type=host
// @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 hostCAFormat(ca services.CertAuthority, keyBytes []byte, client *auth.TunClient) (string, error) {
options := url.Values{
"type": []string{string(services.HostCA)},
comment := url.Values{
"type": []string{string(ca.GetType())},
}

roles, err := services.FetchRoles(ca.GetRoles(), client)
Expand All @@ -662,11 +686,11 @@ func hostCAFormat(ca services.CertAuthority, keyBytes []byte, client *auth.TunCl
}
allowedLogins, _ := roles.CheckLogins(defaults.MinCertDuration + time.Second)
if len(allowedLogins) > 0 {
options["logins"] = allowedLogins
comment["logins"] = allowedLogins
}

return fmt.Sprintf("@cert-authority *.%s %s %s",
ca.GetClusterName(), strings.TrimSpace(string(keyBytes)), options.Encode()), nil
ca.GetClusterName(), strings.TrimSpace(string(keyBytes)), comment.Encode()), nil
}

// GenerateKeys generates a new keypair
Expand Down