Skip to content

Commit

Permalink
cert: Add CLI to create tenant scope client cert
Browse files Browse the repository at this point in the history
This PR adds a command to generate a new type of cert -
a tenant scoped client cert. This is similar to a client
cert but can only be used to connect to a specific tenant ID
as indicated in the certificate. This certificate will be used
to authenticate a client for a specific tenant server.
Subsequent PRs stacked on top of this one will implement the
authentication component for this certificate. The first use
case for this certificate will be the debug zip command.

Informs #77958

Release note (cli change): A new command to create a tenant scoped
client certificate has been added. This command will be used to
create certs that will authenticate a client for a specific tenant.
  • Loading branch information
rimadeodhar committed Mar 30, 2022
1 parent a2d43c8 commit 0f79e38
Show file tree
Hide file tree
Showing 10 changed files with 102 additions and 10 deletions.
4 changes: 2 additions & 2 deletions pkg/acceptance/cluster/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ func GenerateCerts(ctx context.Context) func() {
// Root user.
maybePanic(security.CreateClientPair(
certsDir, filepath.Join(certsDir, security.EmbeddedCAKey),
2048, 48*time.Hour, false, security.RootUserName(), true /* generate pk8 key */))
2048, 48*time.Hour, false, security.RootUserName(), "" /* tenantID */, true /* generate pk8 key */))

// Test user.
maybePanic(security.CreateClientPair(
certsDir, filepath.Join(certsDir, security.EmbeddedCAKey),
1024, 48*time.Hour, false, security.TestUserName(), true /* generate pk8 key */))
1024, 48*time.Hour, false, security.TestUserName(), "" /* tenantID */, true /* generate pk8 key */))

// Certs for starting a cockroach server. Key size is from cli/cert.go:defaultKeySize.
maybePanic(security.CreateNodePair(
Expand Down
2 changes: 2 additions & 0 deletions pkg/cli/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ func runCreateClientCert(cmd *cobra.Command, args []string) error {
certCtx.certificateLifetime,
certCtx.overwriteFiles,
username,
"",
certCtx.generatePKCS8Key),
"failed to generate client certificate and key")
}
Expand Down Expand Up @@ -295,6 +296,7 @@ var certCmds = []*cobra.Command{
createClientCertCmd,
mtCreateTenantCertCmd,
mtCreateTenantSigningCertCmd,
mtCreateClientForTenantCert,
listCertsCmd,
}

Expand Down
1 change: 1 addition & 0 deletions pkg/cli/democluster/demo_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,7 @@ func (demoCtx *Context) generateCerts(certsDir string) (err error) {
demoCtx.DefaultCertLifetime,
false, /* overwrite */
security.RootUserName(),
"", /* tenantID */
false, /* generatePKCS8Key */
); err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions pkg/cli/mt.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func init() {
mtCreateTenantCACertCmd,
mtCreateTenantCertCmd,
mtCreateTenantSigningCertCmd,
mtCreateClientForTenantCert,
)

mtCmd.AddCommand(mtCertsCmd)
Expand Down
46 changes: 46 additions & 0 deletions pkg/cli/mt_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,49 @@ If --overwrite is true, any existing files are overwritten.
"failed to generate tenant signing cert and key")
}),
}

// A mtCreateClientForTenantCert command generates a tenant scoped client certificate and stores it
// in the cert directory under client.<username>.tenant-<tenant_id>.crt and key under client.<username>.tenant-<tenant_id>.key.
var mtCreateClientForTenantCert = &cobra.Command{
Use: "create-client-for-tenant --certs-dir=<path to cockroach certs dir> --ca-key=<path-to-ca-key> <username> <tenant_id>",
Short: "create tenant scoped client certificate and key",
Long: `
Generate a tenant scoped client certificate "<certs-dir>/client.<username>.tenant-<tenant_id>.crt" and key
"<certs-dir>/client.<username>.tenant-<tenant_id>.key".
If --overwrite is true, any existing files are overwritten.
Requires a CA cert in "<certs-dir>/ca.crt" and matching key in "--ca-key".
If "ca.crt" contains more than one certificate, the first is used.
Creation fails if the CA expiration time is before the desired certificate expiration.
`,
Args: cobra.ExactArgs(2),
RunE: clierrorplus.MaybeDecorateError(runCreateClientForTenantCrt),
}

// runCreateClientForTenantCrt generates key pair and CA certificate and writes them
// to their corresponding files.
// TODO(marc): there is currently no way to specify which CA cert to use if more
// than one if present.
func runCreateClientForTenantCrt(cmd *cobra.Command, args []string) error {
username, err := security.MakeSQLUsernameFromUserInput(args[0], security.UsernameCreation)
if err != nil {
return errors.Wrap(err, "failed to generate client certificate and key")
}
tenantID := args[1]
// Confirm tenantID is valid.
if _, err := strconv.ParseUint(tenantID, 10, 64); err != nil {
return errors.Wrapf(err, "%s is invalid uint64", tenantID)
}
return errors.Wrap(
security.CreateClientPair(
certCtx.certsDir,
certCtx.caKey,
certCtx.keySize,
certCtx.certificateLifetime,
certCtx.overwriteFiles,
username,
tenantID,
certCtx.generatePKCS8Key),
"failed to generate tenant scoped client certificate and key")
}
20 changes: 20 additions & 0 deletions pkg/security/certificate_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,16 @@ func ClientCertFilename(user SQLUsername) string {
return "client." + user.Normalized() + certExtension
}

// ClientForTenantCertPath returns the expected file path for the user's tenant scoped certificate.
func (cl CertsLocator) ClientForTenantCertPath(user SQLUsername, tenantID string) string {
return filepath.Join(cl.certsDir, ClientForTenantCertFilename(user, tenantID))
}

// ClientForTenantCertFilename returns the expected file name for the user's tenant scoped certificate.
func ClientForTenantCertFilename(user SQLUsername, tenantID string) string {
return "client." + user.Normalized() + ".tenant-" + tenantID + certExtension
}

// ClientKeyPath returns the expected file path for the user's key.
func (cl CertsLocator) ClientKeyPath(user SQLUsername) string {
return filepath.Join(cl.certsDir, ClientKeyFilename(user))
Expand All @@ -441,6 +451,16 @@ func ClientKeyFilename(user SQLUsername) string {
return "client." + user.Normalized() + keyExtension
}

// ClientForTenantKeyPath returns the expected file path for the user's tenant scoped key
func (cl CertsLocator) ClientForTenantKeyPath(user SQLUsername, tenantID string) string {
return filepath.Join(cl.certsDir, ClientForTenantKeyFilename(user, tenantID))
}

// ClientForTenantKeyFilename returns the expected file name for the user's key.
func ClientForTenantKeyFilename(user SQLUsername, tenantID string) string {
return "client." + user.Normalized() + ".tenant-" + tenantID + keyExtension
}

// SQLServiceCertPath returns the expected file path for the
// SQL service certificate
func (cl CertsLocator) SQLServiceCertPath() string {
Expand Down
2 changes: 1 addition & 1 deletion pkg/security/certificate_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func TestManagerWithPrincipalMap(t *testing.T) {
certsDir, caKey, testKeySize, time.Hour*96, true, true,
))
require.NoError(t, security.CreateClientPair(
certsDir, caKey, testKeySize, time.Hour*48, true, security.TestUserName(), false,
certsDir, caKey, testKeySize, time.Hour*48, true, security.TestUserName(), "", false,
))
require.NoError(t, security.CreateNodePair(
certsDir, caKey, testKeySize, time.Hour*48, true, []string{"127.0.0.1", "foo"},
Expand Down
15 changes: 12 additions & 3 deletions pkg/security/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ func CreateClientPair(
lifetime time.Duration,
overwrite bool,
user SQLUsername,
tenantID string,
wantPKCS8Key bool,
) error {
if len(caKeyPath) == 0 {
Expand Down Expand Up @@ -423,18 +424,26 @@ func CreateClientPair(
return errors.Wrap(err, "could not generate new client key")
}

clientCert, err := GenerateClientCert(caCert, caPrivateKey, clientKey.Public(), lifetime, user)
clientCert, err := GenerateClientCert(caCert, caPrivateKey, clientKey.Public(), lifetime, user, tenantID)
if err != nil {
return errors.Wrap(err, "error creating client certificate and key")
}

certPath := cm.ClientCertPath(user)
var certPath string
var keyPath string

if tenantID != "" {
certPath = cm.ClientForTenantCertPath(user, tenantID)
keyPath = cm.ClientForTenantKeyPath(user, tenantID)
} else {
certPath = cm.ClientCertPath(user)
keyPath = cm.ClientKeyPath(user)
}
if err := writeCertificateToFile(certPath, clientCert, overwrite); err != nil {
return errors.Wrapf(err, "error writing client certificate to %s", certPath)
}
log.Infof(context.Background(), "generated client certificate: %s", certPath)

keyPath := cm.ClientKeyPath(user)
if err := writeKeyToFile(keyPath, clientKey, overwrite); err != nil {
return errors.Wrapf(err, "error writing client key to %s", keyPath)
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/security/certs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ func generateBaseCerts(certsDir string) error {

if err := security.CreateClientPair(
certsDir, caKey,
testKeySize, time.Hour*48, true, security.RootUserName(), false,
testKeySize, time.Hour*48, true, security.RootUserName(), "", false,
); err != nil {
return err
}
Expand Down Expand Up @@ -281,14 +281,14 @@ func generateSplitCACerts(certsDir string) error {

if err := security.CreateClientPair(
certsDir, filepath.Join(certsDir, security.EmbeddedClientCAKey),
testKeySize, time.Hour*48, true, security.NodeUserName(), false,
testKeySize, time.Hour*48, true, security.NodeUserName(), "", false,
); err != nil {
return errors.Wrap(err, "could not generate Client pair")
}

if err := security.CreateClientPair(
certsDir, filepath.Join(certsDir, security.EmbeddedClientCAKey),
testKeySize, time.Hour*48, true, security.RootUserName(), false,
testKeySize, time.Hour*48, true, security.RootUserName(), "", false,
); err != nil {
return errors.Wrap(err, "could not generate Client pair")
}
Expand Down
15 changes: 14 additions & 1 deletion pkg/security/x509.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"fmt"
"math/big"
"net"
"net/url"
"time"

"github.com/cockroachdb/cockroach/pkg/util/timeutil"
Expand Down Expand Up @@ -247,6 +248,7 @@ func GenerateClientCert(
clientPublicKey crypto.PublicKey,
lifetime time.Duration,
user SQLUsername,
tenantID string,
) ([]byte, error) {

// TODO(marc): should we add extra checks?
Expand All @@ -268,7 +270,14 @@ func GenerateClientCert(
// Set client-specific fields.
// Client authentication only.
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}

if tenantID != "" {
var url *url.URL
url, err := makeTenantURISAN(tenantID)
if err != nil {
return nil, err
}
template.URIs = append(template.URIs, url)
}
certBytes, err := x509.CreateCertificate(rand.Reader, template, caCert, clientPublicKey, caPrivateKey)
if err != nil {
return nil, err
Expand Down Expand Up @@ -308,3 +317,7 @@ func GenerateTenantSigningCert(

return certBytes, nil
}

func makeTenantURISAN(tenantID string) (*url.URL, error) {
return url.Parse(fmt.Sprintf("crdb://tenant/%s", tenantID))
}

0 comments on commit 0f79e38

Please sign in to comment.