teleport/lib/client/ca_export_test.go

242 lines
7.1 KiB
Go
Raw Normal View History

Expose `tctl auth export` as a public endpoint (#15491) Public endpoint that exports CA Certificates It supports the same types that we currently support when running `tctl auth export --type=t` Fix https://github.com/gravitational/teleport/issues/7413 Demo ``` marco@lenix ~> curl --insecure "https://127.0.0.1.nip.io:3080/v1/webapi/sites/lenix/auth/export" @cert-authority lenix,*.lenix ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0TxkYYZ+EKHdgA09h/fK0upJzaPe8Sxr63wxPLzqJNDr8O4mxy24UZ08t5tojyaEXHwRLvqOdUBXhbpQpmrUkuN2LLS/k1ijk+V98j+6vWaviL3fDPbZw4PRRl0QtCjAFE/kxH9uLmqvMwnCpeNOQaugq1slVOibDZDrP7FcKmrTzKldk5ohSJSTl0sH6TQzQG2Sj+awXLk7LWcYvE5PN0YVh/S9kzmTpCugaIGd5LOAL3JSuQJ+pwwKmVEg+EPYcbzv7lcLgnew9weCDK0uk4PuNnMXrt1KcCEFttMrceeGnnqOr5JogFLiWL/TP+9kBgleNZhXEaXSz2r1i7pNt type=host cert-authority ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHoDSouxSrXq/X+qZWBM8saHEIb3O3dXOfpRtGrkV6LymPpojhzN1dcYKUqYvWYMw2+WOKIyeAgg9p0zrIQaP7AQPP42sjDhB4keJMbSHh43Hnb7pITRBBpsSCGYMr1U1utQDdz1dbzp96OlvXoNL+ltZKPYNBvG7obqlqhFhHV6ReNvb7FT1OSrtC7UEElPyrYgd3+QMBTTNNYg3Lu+712ySKHqjdM6mLG8XWfSbkmLVbKRTcV3qPj6vEnarXHOnATv+AJElkIlaJjx+RmIxEA9AlE+mjxm2PR+uwsFLV+YD20oPWHaqVyKzT3x7/UNQttBTZD1G0D2fwZg2lDO29 clustername=lenix&type=user marco@lenix ~> curl --insecure "https://127.0.0.1.nip.io:3080/v1/webapi/sites/lenix/auth/export?type=db" -----BEGIN CERTIFICATE----- MIIDcDCCAligAwIBAgIQUppf1u3b6mtJig/5GwAUNjANBgkqhkiG9w0BAQsFADBS MQ4wDAYDVQQKEwVsZW5peDEOMAwGA1UEAxMFbGVuaXgxMDAuBgNVBAUTJzEwOTc5 ODI1MzIyNzkzMDM5OTA4OTE3NjQ1MzMyMDAzMDU1NzIzODAeFw0yMjA4MzAxMDA5 NThaFw0zMjA4MjcxMDA5NThaMFIxDjAMBgNVBAoTBWxlbml4MQ4wDAYDVQQDEwVs ZW5peDEwMC4GA1UEBRMnMTA5Nzk4MjUzMjI3OTMwMzk5MDg5MTc2NDUzMzIwMDMw NTU3MjM4MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4AyI3MnQHLjv hko3qE1F0SqlPMKfCPaBaCHZj5yI7Gm/U6BKx+mm4zl0R7xqDjUIQo751DUVQoJh etuHOpgJ9OyzTYG8BqqhuuGFqWy9RkPcMSup2gQInpcma+iVs6HPYeIZa4MlnYJH nBVjRgXyiURAtdg0wlq12SNh0R0FUBAo4i6bqNLlqQkA9WJkea5RxIk337qgYo/r pRb0QlOWaeUYsVU4yn21h1dnrZCg21iaxLDjV97dV47BYcK5yPFFSRAVVgyR74r2 SF3p6R45qH1Ll+gEwxZf7M0IFPAUbWxaZ3KPz11BE+ISKH1NN7j/Aj5rPqwAULnp e79S94asmQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAaYwDwYDVR0TAQH/BAUwAwEB /zAdBgNVHQ4EFgQUOXJtvj3lcOi/epoPRO32lEHgM8wwDQYJKoZIhvcNAQELBQAD ggEBAMe0zhNLzNNlxYWTgD5uefhP1KPb/44gmh7n1/xF3Wd11QktDraPN9cyy30R qeMdBmwgn6Ek7Dy+E22nPSVLh49DSRPrXATgRetNW4g4iMvQKnZi8k7agnXedxWq irLUsFApAagKwFNJyQQsGpdUxUgZiK43JaZ/dAHGvXYEJA4BYepIQRtEdraa/T5h M9HDWVHEuPCjMH8/10wTWjDiikVHPCwAAab6/r7WJ7Jn+Bv4PBOxYOwHeuboBNgl khF9lGAqonSP6kx3gLSApLW5S3QX0U+4qukmIE970evwIAzc43hwaNaI8gM9nswE dpX8vd+vaed76+ZzxQmBlh4hgQU= -----END CERTIFICATE----- marco@lenix ~> ```
2022-10-27 14:53:29 +00:00
/*
Copyright 2022 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package client
import (
"context"
"crypto/x509"
"encoding/pem"
"strings"
"testing"
2022-10-28 20:20:28 +00:00
"github.com/stretchr/testify/require"
Expose `tctl auth export` as a public endpoint (#15491) Public endpoint that exports CA Certificates It supports the same types that we currently support when running `tctl auth export --type=t` Fix https://github.com/gravitational/teleport/issues/7413 Demo ``` marco@lenix ~> curl --insecure "https://127.0.0.1.nip.io:3080/v1/webapi/sites/lenix/auth/export" @cert-authority lenix,*.lenix ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0TxkYYZ+EKHdgA09h/fK0upJzaPe8Sxr63wxPLzqJNDr8O4mxy24UZ08t5tojyaEXHwRLvqOdUBXhbpQpmrUkuN2LLS/k1ijk+V98j+6vWaviL3fDPbZw4PRRl0QtCjAFE/kxH9uLmqvMwnCpeNOQaugq1slVOibDZDrP7FcKmrTzKldk5ohSJSTl0sH6TQzQG2Sj+awXLk7LWcYvE5PN0YVh/S9kzmTpCugaIGd5LOAL3JSuQJ+pwwKmVEg+EPYcbzv7lcLgnew9weCDK0uk4PuNnMXrt1KcCEFttMrceeGnnqOr5JogFLiWL/TP+9kBgleNZhXEaXSz2r1i7pNt type=host cert-authority ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHoDSouxSrXq/X+qZWBM8saHEIb3O3dXOfpRtGrkV6LymPpojhzN1dcYKUqYvWYMw2+WOKIyeAgg9p0zrIQaP7AQPP42sjDhB4keJMbSHh43Hnb7pITRBBpsSCGYMr1U1utQDdz1dbzp96OlvXoNL+ltZKPYNBvG7obqlqhFhHV6ReNvb7FT1OSrtC7UEElPyrYgd3+QMBTTNNYg3Lu+712ySKHqjdM6mLG8XWfSbkmLVbKRTcV3qPj6vEnarXHOnATv+AJElkIlaJjx+RmIxEA9AlE+mjxm2PR+uwsFLV+YD20oPWHaqVyKzT3x7/UNQttBTZD1G0D2fwZg2lDO29 clustername=lenix&type=user marco@lenix ~> curl --insecure "https://127.0.0.1.nip.io:3080/v1/webapi/sites/lenix/auth/export?type=db" -----BEGIN CERTIFICATE----- MIIDcDCCAligAwIBAgIQUppf1u3b6mtJig/5GwAUNjANBgkqhkiG9w0BAQsFADBS MQ4wDAYDVQQKEwVsZW5peDEOMAwGA1UEAxMFbGVuaXgxMDAuBgNVBAUTJzEwOTc5 ODI1MzIyNzkzMDM5OTA4OTE3NjQ1MzMyMDAzMDU1NzIzODAeFw0yMjA4MzAxMDA5 NThaFw0zMjA4MjcxMDA5NThaMFIxDjAMBgNVBAoTBWxlbml4MQ4wDAYDVQQDEwVs ZW5peDEwMC4GA1UEBRMnMTA5Nzk4MjUzMjI3OTMwMzk5MDg5MTc2NDUzMzIwMDMw NTU3MjM4MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4AyI3MnQHLjv hko3qE1F0SqlPMKfCPaBaCHZj5yI7Gm/U6BKx+mm4zl0R7xqDjUIQo751DUVQoJh etuHOpgJ9OyzTYG8BqqhuuGFqWy9RkPcMSup2gQInpcma+iVs6HPYeIZa4MlnYJH nBVjRgXyiURAtdg0wlq12SNh0R0FUBAo4i6bqNLlqQkA9WJkea5RxIk337qgYo/r pRb0QlOWaeUYsVU4yn21h1dnrZCg21iaxLDjV97dV47BYcK5yPFFSRAVVgyR74r2 SF3p6R45qH1Ll+gEwxZf7M0IFPAUbWxaZ3KPz11BE+ISKH1NN7j/Aj5rPqwAULnp e79S94asmQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAaYwDwYDVR0TAQH/BAUwAwEB /zAdBgNVHQ4EFgQUOXJtvj3lcOi/epoPRO32lEHgM8wwDQYJKoZIhvcNAQELBQAD ggEBAMe0zhNLzNNlxYWTgD5uefhP1KPb/44gmh7n1/xF3Wd11QktDraPN9cyy30R qeMdBmwgn6Ek7Dy+E22nPSVLh49DSRPrXATgRetNW4g4iMvQKnZi8k7agnXedxWq irLUsFApAagKwFNJyQQsGpdUxUgZiK43JaZ/dAHGvXYEJA4BYepIQRtEdraa/T5h M9HDWVHEuPCjMH8/10wTWjDiikVHPCwAAab6/r7WJ7Jn+Bv4PBOxYOwHeuboBNgl khF9lGAqonSP6kx3gLSApLW5S3QX0U+4qukmIE970evwIAzc43hwaNaI8gM9nswE dpX8vd+vaed76+ZzxQmBlh4hgQU= -----END CERTIFICATE----- marco@lenix ~> ```
2022-10-27 14:53:29 +00:00
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/services"
)
type mockAuthClient struct {
auth.ClientI
server *auth.Server
}
func (m *mockAuthClient) GetDomainName(ctx context.Context) (string, error) {
return m.server.GetDomainName()
}
func (m *mockAuthClient) GetCertAuthorities(ctx context.Context, caType types.CertAuthType, loadKeys bool, opts ...services.MarshalOption) ([]types.CertAuthority, error) {
return m.server.GetCertAuthorities(ctx, caType, loadKeys, opts...)
}
func (m *mockAuthClient) GetCertAuthority(ctx context.Context, id types.CertAuthID, loadKeys bool, opts ...services.MarshalOption) (types.CertAuthority, error) {
return m.server.GetCertAuthority(ctx, id, loadKeys, opts...)
}
func TestExportAuthorities(t *testing.T) {
ctx := context.Background()
const localClusterName = "localcluster"
testAuth, err := auth.NewTestAuthServer(auth.TestAuthServerConfig{
ClusterName: localClusterName,
Dir: t.TempDir(),
})
require.NoError(t, err, "failed to create auth.NewTestAuthServer")
validateTLSCertificateDERFunc := func(t *testing.T, s string) {
cert, err := x509.ParseCertificate([]byte(s))
require.NoError(t, err, "failed to x509.ParseCertificate")
require.NotNil(t, cert, "x509.ParseCertificate returned a nil certificate")
require.Equal(t, localClusterName, cert.Subject.CommonName, "unexpected certificate subject CN")
}
validateTLSCertificatePEMFunc := func(t *testing.T, s string) {
pemBlock, _ := pem.Decode([]byte(s))
require.NotNil(t, pemBlock, "pem.Decode failed")
validateTLSCertificateDERFunc(t, string(pemBlock.Bytes))
}
validatePrivateKeyPEMFunc := func(t *testing.T, s string) {
pemBlock, _ := pem.Decode([]byte(s))
require.NotNil(t, pemBlock, "pem.Decode failed")
require.Equal(t, "RSA PRIVATE KEY", pemBlock.Type, "unexpected private key type")
privKey, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes)
require.NoError(t, err, "x509.ParsePKCS1PrivateKey failed")
require.NotNil(t, privKey, "x509.ParsePKCS1PrivateKey returned a nil certificate")
}
validatePrivateKeyDERFunc := func(t *testing.T, s string) {
res := strings.Split(s, "\n\n")
require.Len(t, res, 2, "expected private key and certificate separated by one empty line")
privKey, err := x509.ParsePKCS1PrivateKey([]byte(res[0]))
require.NoError(t, err, "x509.ParsePKCS1PrivateKey failed")
require.NotNil(t, privKey, "x509.ParsePKCS1PrivateKey returned a nil certificate")
validateTLSCertificateDERFunc(t, res[1])
}
for _, exportSecrets := range []bool{false, true} {
for _, tt := range []struct {
name string
req ExportAuthoritiesRequest
errorCheck require.ErrorAssertionFunc
assertOutput func(t *testing.T, output string)
assertSecrets func(t *testing.T, output string)
}{
{
name: "ssh host and user ca",
req: ExportAuthoritiesRequest{
AuthType: "",
},
errorCheck: require.NoError,
assertOutput: func(t *testing.T, output string) {
require.Contains(t, output, "@cert-authority localcluster,*.localcluster ssh-rsa")
require.Contains(t, output, "cert-authority ssh-rsa")
},
assertSecrets: func(t *testing.T, output string) {},
},
{
name: "user",
req: ExportAuthoritiesRequest{
AuthType: "user",
},
errorCheck: require.NoError,
assertOutput: func(t *testing.T, output string) {
require.Contains(t, output, "cert-authority ssh-rsa")
},
assertSecrets: validatePrivateKeyPEMFunc,
},
{
name: "host",
req: ExportAuthoritiesRequest{
AuthType: "host",
},
errorCheck: require.NoError,
assertOutput: func(t *testing.T, output string) {
require.Contains(t, output, "@cert-authority localcluster,*.localcluster ssh-rsa")
},
assertSecrets: validatePrivateKeyPEMFunc,
},
{
name: "tls",
req: ExportAuthoritiesRequest{
AuthType: "tls",
},
errorCheck: require.NoError,
assertOutput: validateTLSCertificatePEMFunc,
assertSecrets: validatePrivateKeyPEMFunc,
},
{
name: "windows",
req: ExportAuthoritiesRequest{
AuthType: "windows",
},
errorCheck: require.NoError,
assertOutput: validateTLSCertificateDERFunc,
assertSecrets: validatePrivateKeyDERFunc,
},
{
name: "invalid",
req: ExportAuthoritiesRequest{
AuthType: "invalid",
},
errorCheck: func(tt require.TestingT, err error, i ...interface{}) {
require.ErrorContains(tt, err, `"invalid" authority type is not supported`)
},
},
{
name: "fingerprint not found",
req: ExportAuthoritiesRequest{
AuthType: "user",
ExportAuthorityFingerprint: "not found fingerprint",
},
errorCheck: require.NoError,
assertOutput: func(t *testing.T, output string) {
require.Empty(t, output)
},
assertSecrets: func(t *testing.T, output string) {
require.Empty(t, output)
},
},
{
name: "fingerprint not found",
req: ExportAuthoritiesRequest{
AuthType: "user",
ExportAuthorityFingerprint: "fake fingerprint",
},
errorCheck: require.NoError,
assertOutput: func(t *testing.T, output string) {
require.Empty(t, output)
},
assertSecrets: func(t *testing.T, output string) {
require.Empty(t, output)
},
},
{
name: "using compat version",
req: ExportAuthoritiesRequest{
AuthType: "user",
UseCompatVersion: true,
},
errorCheck: require.NoError,
assertOutput: func(t *testing.T, output string) {
// compat version (using 1.0) returns cert-authority to be used in the server
// even when asking for ssh authorized hosts / known hosts
require.Contains(t, output, "@cert-authority localcluster,*.localcluster ssh-rsa")
},
assertSecrets: validatePrivateKeyPEMFunc,
},
} {
testName := tt.name
if exportSecrets {
testName = tt.name + "_exportSecrets"
}
t.Run(testName, func(t *testing.T) {
mockedClient := &mockAuthClient{
server: testAuth.AuthServer,
}
var (
err error
exported string
)
if exportSecrets {
exported, err = ExportAuthoritiesWithSecrets(ctx, mockedClient, tt.req)
tt.errorCheck(t, err)
} else {
exported, err = ExportAuthorities(ctx, mockedClient, tt.req)
tt.errorCheck(t, err)
}
if err != nil {
return
}
if exportSecrets {
tt.assertSecrets(t, exported)
} else {
tt.assertOutput(t, exported)
}
})
}
}
}