diff --git a/e2e/yaml_test.go b/e2e/yaml_test.go index 7f714158..b3d07755 100644 --- a/e2e/yaml_test.go +++ b/e2e/yaml_test.go @@ -58,6 +58,14 @@ pipes: username: "user" ignore_hostkey: true private_key: {{ .PrivateKey }} +- from: + - username: "cert" + trusted_user_ca_keys: {{ .TrustedUserCAKeys }} + to: + host: host-publickey:2222 + username: "user" + ignore_hostkey: true + private_key: {{ .PrivateKey }} ` func TestYaml(t *testing.T) { @@ -125,6 +133,43 @@ func TestYaml(t *testing.T) { ); err != nil { t.Errorf("failed to copy public key: %v", err) } + + // ssh ca + if err := runCmdAndWait( + "ssh-keygen", + "-N", + "", + "-f", + path.Join(yamldir, "ca_key"), + ); err != nil { + t.Errorf("failed to generate ca key: %v", err) + } + + if err := runCmdAndWait( + "ssh-keygen", + "-N", + "", + "-f", + path.Join(yamldir, "user_ca_key"), + ); err != nil { + t.Errorf("failed to generate user ca key: %v", err) + } + + if err := runCmdAndWait( + "ssh-keygen", + "-s", + path.Join(yamldir, "ca_key"), + "-I", + "cert", + "-n", + "cert", + "-V", + "+1w", + path.Join(yamldir, "user_ca_key.pub"), + ); err != nil { + t.Errorf("failed to sign user ca key: %v", err) + } + } knownHostsKeyData, err := runAndGetStdout( @@ -155,6 +200,8 @@ func TestYaml(t *testing.T) { AuthorizedKeys_Simple string AuthorizedKeys_Catchall string + + TrustedUserCAKeys string }{ KnownHostsKey: base64.StdEncoding.EncodeToString(knownHostsKeyData), KnownHostsPass: base64.StdEncoding.EncodeToString(knownHostsPassData), @@ -162,6 +209,8 @@ func TestYaml(t *testing.T) { AuthorizedKeys_Simple: path.Join(yamldir, "id_rsa_simple.pub"), AuthorizedKeys_Catchall: path.Join(yamldir, "id_rsa_catchall.pub"), + + TrustedUserCAKeys: path.Join(yamldir, "ca_key.pub"), }); err != nil { t.Fatalf("Failed to write yaml file %v", err) } @@ -397,4 +446,37 @@ func TestYaml(t *testing.T) { checkSharedFileContent(t, targetfie, randtext) }) + t.Run("ssh_cert", func(t *testing.T) { + randtext := uuid.New().String() + targetfie := uuid.New().String() + + c, _, _, err := runCmd( + "ssh", + "-v", + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-o", + fmt.Sprintf("CertificateFile=%v", path.Join(yamldir, "user_ca_key-cert.pub")), + "-p", + piperport, + "-l", + "cert", + "-i", + path.Join(yamldir, "user_ca_key"), + "127.0.0.1", + fmt.Sprintf(`sh -c "echo -n %v > /shared/%v"`, randtext, targetfie), + ) + + if err != nil { + t.Errorf("failed to ssh to piper, %v", err) + } + + defer killCmd(c) + + time.Sleep(time.Second) // wait for file flush + + checkSharedFileContent(t, targetfie, randtext) + }) } diff --git a/plugin/yaml/schema.json b/plugin/yaml/schema.json index 6829a8ca..d30780c8 100644 --- a/plugin/yaml/schema.json +++ b/plugin/yaml/schema.json @@ -75,7 +75,33 @@ "type": "string" } ] - } + }, + "trusted_user_ca_keys": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "trusted_user_ca_keys_data": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + } }, "required": [ "username" diff --git a/plugin/yaml/yaml.go b/plugin/yaml/yaml.go index 53c5f5c8..cb695346 100644 --- a/plugin/yaml/yaml.go +++ b/plugin/yaml/yaml.go @@ -19,10 +19,12 @@ import ( ) type pipeConfigFrom struct { - Username string `yaml:"username"` - UsernameRegexMatch bool `yaml:"username_regex_match,omitempty"` - AuthorizedKeys listOrString `yaml:"authorized_keys,omitempty"` - AuthorizedKeysData listOrString `yaml:"authorized_keys_data,omitempty"` + Username string `yaml:"username"` + UsernameRegexMatch bool `yaml:"username_regex_match,omitempty"` + AuthorizedKeys listOrString `yaml:"authorized_keys,omitempty"` + AuthorizedKeysData listOrString `yaml:"authorized_keys_data,omitempty"` + TrustedUserCAKeys listOrString `yaml:"trusted_user_ca_keys,omitempty"` + TrustedUserCAKeysData listOrString `yaml:"trusted_user_ca_keys_data,omitempty"` } type pipeConfigTo struct { @@ -200,7 +202,7 @@ func (p *plugin) supportedMethods() ([]string, error) { for _, pipe := range config.Pipes { for _, from := range pipe.From { - if from.AuthorizedKeys.Any() || from.AuthorizedKeysData.Any() { + if from.AuthorizedKeys.Any() || from.AuthorizedKeysData.Any() || from.TrustedUserCAKeys.Any() || from.TrustedUserCAKeysData.Any() { set["publickey"] = true // found authorized_keys, so we support publickey } else { set["password"] = true // no authorized_keys, so we support password @@ -287,6 +289,30 @@ func (p *plugin) findAndCreateUpstream(conn libplugin.ConnMetadata, password str return nil, err } + var isCert bool + var pkcert *ssh.Certificate + + if publicKey != nil { + pubKey, err := ssh.ParsePublicKey(publicKey) + if err != nil { + return nil, err + } + + pkcert, isCert = pubKey.(*ssh.Certificate) + if isCert { + // ensure cert is valid first + + if pkcert.CertType != ssh.UserCert { + return nil, fmt.Errorf("only user certificates are supported, cert type: %v", pkcert.CertType) + } + + certChecker := ssh.CertChecker{} + if err := certChecker.CheckCert(conn.User(), pkcert); err != nil { + return nil, err + } + } + } + for _, pipe := range config.Pipes { for _, from := range pipe.From { matched := from.Username == user @@ -316,24 +342,46 @@ func (p *plugin) findAndCreateUpstream(conn libplugin.ConnMetadata, password str return p.createUpstream(conn, pipe.To, password) } - rest, err := p.loadFileOrDecodeMany(from.AuthorizedKeys, from.AuthorizedKeysData, map[string]string{ - "DOWNSTREAM_USER": user, - }) - if err != nil { - return nil, err - } + if isCert { + rest, err := p.loadFileOrDecodeMany(from.TrustedUserCAKeys, from.TrustedUserCAKeysData, map[string]string{ + "DOWNSTREAM_USER": user, + }) + if err != nil { + return nil, err + } - var authedPubkey ssh.PublicKey - for len(rest) > 0 { - authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest) + var trustedca ssh.PublicKey + for len(rest) > 0 { + trustedca, _, _, rest, err = ssh.ParseAuthorizedKey(rest) + if err != nil { + return nil, err + } + + if subtle.ConstantTimeCompare(trustedca.Marshal(), pkcert.SignatureKey.Marshal()) == 1 { + return p.createUpstream(conn, pipe.To, "") + } + } + } else { + rest, err := p.loadFileOrDecodeMany(from.AuthorizedKeys, from.AuthorizedKeysData, map[string]string{ + "DOWNSTREAM_USER": user, + }) if err != nil { return nil, err } - if subtle.ConstantTimeCompare(authedPubkey.Marshal(), publicKey) == 1 { - return p.createUpstream(conn, pipe.To, "") + var authedPubkey ssh.PublicKey + for len(rest) > 0 { + authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest) + if err != nil { + return nil, err + } + + if subtle.ConstantTimeCompare(authedPubkey.Marshal(), publicKey) == 1 { + return p.createUpstream(conn, pipe.To, "") + } } } + } }