diff --git a/cli/internal/certcache/cert.go b/cli/internal/certcache/cert.go index 03f50f10..b483e5d0 100644 --- a/cli/internal/certcache/cert.go +++ b/cli/internal/certcache/cert.go @@ -13,6 +13,7 @@ import ( "errors" "github.com/edgelesssys/marblerun/cli/internal/file" + "github.com/edgelesssys/marblerun/cli/internal/pkcs11" "github.com/edgelesssys/marblerun/util" "github.com/spf13/afero" "github.com/spf13/pflag" @@ -43,21 +44,50 @@ func LoadCoordinatorCachedCert(flags *pflag.FlagSet, fs afero.Fs) (root, interme } // LoadClientCert parses the command line flags to load a TLS client certificate. -func LoadClientCert(flags *pflag.FlagSet) (*tls.Certificate, error) { +// The returned cancel function must be called only after the certificate is no longer needed. +func LoadClientCert(flags *pflag.FlagSet) (crt *tls.Certificate, cancel func() error, err error) { certFile, err := flags.GetString("cert") if err != nil { - return nil, err + return nil, nil, err } keyFile, err := flags.GetString("key") if err != nil { - return nil, err + return nil, nil, err + } + + pkcs11ConfigFile, err := flags.GetString("pkcs11-config") + if err != nil { + return nil, nil, err + } + pkcs11KeyID, err := flags.GetString("pkcs11-key-id") + if err != nil { + return nil, nil, err + } + pkcs11KeyLabel, err := flags.GetString("pkcs11-key-label") + if err != nil { + return nil, nil, err } - clientCert, err := tls.LoadX509KeyPair(certFile, keyFile) + pkcs11CertID, err := flags.GetString("pkcs11-cert-id") if err != nil { - return nil, err + return nil, nil, err + } + pkcs11CertLabel, err := flags.GetString("pkcs11-cert-label") + if err != nil { + return nil, nil, err + } + + var clientCert tls.Certificate + switch { + case pkcs11ConfigFile != "": + clientCert, cancel, err = pkcs11.LoadX509KeyPair(pkcs11ConfigFile, pkcs11KeyID, pkcs11KeyLabel, pkcs11CertID, pkcs11CertLabel) + case certFile != "" && keyFile != "": + clientCert, err = tls.LoadX509KeyPair(certFile, keyFile) + cancel = func() error { return nil } + default: + err = errors.New("neither PKCS#11 nor file-based client certificate can be loaded with the provided flags") } - return &clientCert, nil + return &clientCert, cancel, err } func saveCert(fh *file.Handler, root, intermediate *x509.Certificate) error { diff --git a/cli/internal/cmd/cmd.go b/cli/internal/cmd/cmd.go index 4ab7796c..c2397839 100644 --- a/cli/internal/cmd/cmd.go +++ b/cli/internal/cmd/cmd.go @@ -21,6 +21,7 @@ import ( "github.com/edgelesssys/marblerun/cli/internal/kube" "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/util/version" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" @@ -32,6 +33,26 @@ func webhookDNSName(namespace string) string { return "marble-injector." + namespace } +func addClientAuthFlags(cmd *cobra.Command, flags *pflag.FlagSet) { + flags.StringP("cert", "c", "", "PEM encoded admin certificate file") + flags.StringP("key", "k", "", "PEM encoded admin key file") + cmd.MarkFlagsRequiredTogether("key", "cert") + + flags.String("pkcs11-config", "", "Path to a PKCS#11 configuration file to load the client certificate with") + flags.String("pkcs11-key-id", "", "ID of the private key in the PKCS#11 token") + flags.String("pkcs11-key-label", "", "Label of the private key in the PKCS#11 token") + flags.String("pkcs11-cert-id", "", "ID of the certificate in the PKCS#11 token") + flags.String("pkcs11-cert-label", "", "Label of the certificate in the PKCS#11 token") + must(cobra.MarkFlagFilename(flags, "pkcs11-config", "json")) + cmd.MarkFlagsOneRequired("pkcs11-key-id", "pkcs11-key-label", "cert") + cmd.MarkFlagsOneRequired("pkcs11-cert-id", "pkcs11-cert-label", "cert") + + cmd.MarkFlagsMutuallyExclusive("pkcs11-config", "cert") + cmd.MarkFlagsMutuallyExclusive("pkcs11-config", "key") + cmd.MarkFlagsOneRequired("pkcs11-config", "cert") + cmd.MarkFlagsOneRequired("pkcs11-config", "key") +} + // parseRestFlags parses the command line flags used to configure the REST client. func parseRestFlags(cmd *cobra.Command) (api.VerifyOptions, string, error) { eraConfig, err := cmd.Flags().GetString("era-config") diff --git a/cli/internal/cmd/manifestUpdate.go b/cli/internal/cmd/manifestUpdate.go index 33670a41..3bd4a3b2 100644 --- a/cli/internal/cmd/manifestUpdate.go +++ b/cli/internal/cmd/manifestUpdate.go @@ -44,11 +44,7 @@ An admin certificate specified in the original manifest is needed to verify the Args: cobra.ExactArgs(2), RunE: runUpdateApply, } - - cmd.Flags().StringP("cert", "c", "", "PEM encoded admin certificate file (required)") - must(cmd.MarkFlagRequired("cert")) - cmd.Flags().StringP("key", "k", "", "PEM encoded admin key file (required)") - must(cmd.MarkFlagRequired("key")) + addClientAuthFlags(cmd, cmd.Flags()) return cmd } @@ -66,11 +62,8 @@ All participants must use the same manifest to acknowledge the pending update. Args: cobra.ExactArgs(2), RunE: runUpdateAcknowledge, } + addClientAuthFlags(cmd, cmd.Flags()) - cmd.Flags().StringP("cert", "c", "", "PEM encoded admin certificate file (required)") - must(cmd.MarkFlagRequired("cert")) - cmd.Flags().StringP("key", "k", "", "PEM encoded admin key file (required)") - must(cmd.MarkFlagRequired("key")) return cmd } @@ -83,11 +76,8 @@ func newUpdateCancel() *cobra.Command { Args: cobra.ExactArgs(1), RunE: runUpdateCancel, } + addClientAuthFlags(cmd, cmd.Flags()) - cmd.Flags().StringP("cert", "c", "", "PEM encoded admin certificate file (required)") - must(cmd.MarkFlagRequired("cert")) - cmd.Flags().StringP("key", "k", "", "PEM encoded admin key file (required)") - must(cmd.MarkFlagRequired("key")) return cmd } @@ -116,10 +106,15 @@ func runUpdateApply(cmd *cobra.Command, args []string) error { if err != nil { return err } - keyPair, err := certcache.LoadClientCert(cmd.Flags()) + keyPair, cancel, err := certcache.LoadClientCert(cmd.Flags()) if err != nil { return err } + defer func() { + if err := cancel(); err != nil { + cmd.PrintErrf("Failed to close PKCS #11 session: %s\n", err) + } + }() manifest, err := loadManifestFile(file.New(manifestFile, fs)) if err != nil { @@ -142,10 +137,15 @@ func runUpdateAcknowledge(cmd *cobra.Command, args []string) error { if err != nil { return err } - keyPair, err := certcache.LoadClientCert(cmd.Flags()) + keyPair, cancel, err := certcache.LoadClientCert(cmd.Flags()) if err != nil { return err } + defer func() { + if err := cancel(); err != nil { + cmd.PrintErrf("Failed to close PKCS #11 session: %s\n", err) + } + }() manifest, err := loadManifestFile(file.New(manifestFile, fs)) if err != nil { @@ -177,10 +177,15 @@ func runUpdateCancel(cmd *cobra.Command, args []string) error { if err != nil { return err } - keyPair, err := certcache.LoadClientCert(cmd.Flags()) + keyPair, cancel, err := certcache.LoadClientCert(cmd.Flags()) if err != nil { return err } + defer func() { + if err := cancel(); err != nil { + cmd.PrintErrf("Failed to close PKCS #11 session: %s\n", err) + } + }() if err := api.ManifestUpdateCancel(cmd.Context(), hostname, root, keyPair); err != nil { return fmt.Errorf("canceling update: %w", err) diff --git a/cli/internal/cmd/secret.go b/cli/internal/cmd/secret.go index 66eca20f..d038cc0c 100644 --- a/cli/internal/cmd/secret.go +++ b/cli/internal/cmd/secret.go @@ -19,11 +19,7 @@ func NewSecretCmd() *cobra.Command { Manage secrets for the MarbleRun Coordinator. Set or retrieve a secret defined in the manifest.`, } - - cmd.PersistentFlags().StringP("cert", "c", "", "PEM encoded MarbleRun user certificate file (required)") - cmd.PersistentFlags().StringP("key", "k", "", "PEM encoded MarbleRun user key file (required)") - must(cmd.MarkPersistentFlagRequired("key")) - must(cmd.MarkPersistentFlagRequired("cert")) + addClientAuthFlags(cmd, cmd.PersistentFlags()) cmd.AddCommand(newSecretSet()) cmd.AddCommand(newSecretGet()) diff --git a/cli/internal/cmd/secretGet.go b/cli/internal/cmd/secretGet.go index c706ff19..9e7250d1 100644 --- a/cli/internal/cmd/secretGet.go +++ b/cli/internal/cmd/secretGet.go @@ -53,10 +53,15 @@ func runSecretGet(cmd *cobra.Command, args []string) error { if err != nil { return err } - keyPair, err := certcache.LoadClientCert(cmd.Flags()) + keyPair, cancel, err := certcache.LoadClientCert(cmd.Flags()) if err != nil { return err } + defer func() { + if err := cancel(); err != nil { + cmd.PrintErrf("Failed to close PKCS #11 session: %s\n", err) + } + }() getSecrets := func(ctx context.Context) (map[string]manifest.Secret, error) { return api.SecretGet(ctx, hostname, root, keyPair, secretIDs) diff --git a/cli/internal/cmd/secretSet.go b/cli/internal/cmd/secretSet.go index 17ad6a99..0f7139cc 100644 --- a/cli/internal/cmd/secretSet.go +++ b/cli/internal/cmd/secretSet.go @@ -78,10 +78,15 @@ func runSecretSet(cmd *cobra.Command, args []string) error { if err != nil { return err } - keyPair, err := certcache.LoadClientCert(cmd.Flags()) + keyPair, cancel, err := certcache.LoadClientCert(cmd.Flags()) if err != nil { return err } + defer func() { + if err := cancel(); err != nil { + cmd.PrintErrf("Failed to close PKCS #11 session: %s\n", err) + } + }() if err := api.SecretSet(cmd.Context(), hostname, root, keyPair, newSecrets); err != nil { return err diff --git a/cli/internal/pkcs11/pkcs11.go b/cli/internal/pkcs11/pkcs11.go new file mode 100644 index 00000000..b997003c --- /dev/null +++ b/cli/internal/pkcs11/pkcs11.go @@ -0,0 +1,82 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: BUSL-1.1 +*/ + +package pkcs11 + +import ( + "crypto" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + + "github.com/ThalesGroup/crypto11" +) + +// LoadX509KeyPair loads a [tls.Certificate] using the provided PKCS#11 configuration file. +// The returned cancel function must be called to release the PKCS#11 resources only after the certificate is no longer needed. +func LoadX509KeyPair(pkcs11ConfigPath string, keyID, keyLabel, certID, certLabel string) (crt tls.Certificate, cancel func() error, err error) { + pkcs11, err := crypto11.ConfigureFromFile(pkcs11ConfigPath) + if err != nil { + return crt, nil, err + } + defer func() { + if err != nil { + err = errors.Join(err, pkcs11.Close()) + } + }() + + var keyIDBytes, keyLabelBytes, certIDBytes, certLabelBytes []byte + if keyID != "" { + keyIDBytes = []byte(keyID) + } + if keyLabel != "" { + keyLabelBytes = []byte(keyLabel) + } + if certID != "" { + certIDBytes = []byte(certID) + } + if certLabel != "" { + certLabelBytes = []byte(certLabel) + } + + privateKey, err := loadPrivateKey(pkcs11, keyIDBytes, keyLabelBytes) + if err != nil { + return crt, nil, err + } + cert, err := loadCertificate(pkcs11, certIDBytes, certLabelBytes) + if err != nil { + return crt, nil, err + } + + return tls.Certificate{ + Certificate: [][]byte{cert.Raw}, + PrivateKey: privateKey, + Leaf: cert, + }, pkcs11.Close, nil +} + +func loadPrivateKey(pkcs11 *crypto11.Context, id, label []byte) (crypto.Signer, error) { + priv, err := pkcs11.FindKeyPair(id, label) + if err != nil { + return nil, err + } + if priv == nil { + return nil, fmt.Errorf("no key pair found for id \"%s\" and label \"%s\"", id, label) + } + return priv, nil +} + +func loadCertificate(pkcs11 *crypto11.Context, id, label []byte) (*x509.Certificate, error) { + cert, err := pkcs11.FindCertificate(id, label, nil) + if err != nil { + return nil, err + } + if cert == nil { + return nil, fmt.Errorf("no certificate found for id \"%s\" and label \"%s\"", id, label) + } + return cert, nil +} diff --git a/cli/internal/pkcs11/pkcs11_integration_test.go b/cli/internal/pkcs11/pkcs11_integration_test.go new file mode 100644 index 00000000..2d8745c0 --- /dev/null +++ b/cli/internal/pkcs11/pkcs11_integration_test.go @@ -0,0 +1,175 @@ +//go:build integration && pkcs11 + +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: BUSL-1.1 +*/ + +package pkcs11 + +import ( + "crypto" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "flag" + "testing" + "time" + + "github.com/ThalesGroup/crypto11" + "github.com/edgelesssys/marblerun/util" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var configPath = flag.String("pkcs11-config", "", "path to PKCS#11 configuration file") + +func TestLoadX509KeyPair(t *testing.T) { + // Ensure we can load the PKCS#11 configuration file + pkcs11, err := crypto11.ConfigureFromFile(*configPath) + require.NoError(t, err) + require.NoError(t, pkcs11.Close()) + + testCases := map[string]struct { + init func(t *testing.T) (keyID, keyLabel, certID, certLabel string) + wantErr bool + }{ + "identified by ID and label": { + init: func(t *testing.T) (keyID, keyLabel, certID, certLabel string) { + t.Helper() + require := require.New(t) + prefix := uuid.New().String() + keyID, keyLabel, certID, certLabel = prefix+"keyID", prefix+"keyLabel", prefix+"certID", prefix+"certLabel" + pkcs11, err := crypto11.ConfigureFromFile(*configPath) + require.NoError(err) + defer pkcs11.Close() + privK, err := pkcs11.GenerateECDSAKeyPairWithLabel([]byte(keyID), []byte(keyLabel), elliptic.P256()) + require.NoError(err) + cert, err := createSelfSignedCertificate(privK) + require.NoError(err) + require.NoError(pkcs11.ImportCertificateWithLabel([]byte(certID), []byte(certLabel), cert)) + return keyID, keyLabel, certID, certLabel + }, + }, + "identified by ID only": { + init: func(t *testing.T) (keyID, keyLabel, certID, certLabel string) { + t.Helper() + require := require.New(t) + prefix := uuid.New().String() + keyID, keyLabel, certID, certLabel = prefix+"keyID", prefix+"keyLabel", prefix+"certID", prefix+"certLabel" + pkcs11, err := crypto11.ConfigureFromFile(*configPath) + require.NoError(err) + defer pkcs11.Close() + privK, err := pkcs11.GenerateECDSAKeyPair([]byte(keyID), elliptic.P256()) + require.NoError(err) + cert, err := createSelfSignedCertificate(privK) + require.NoError(err) + require.NoError(pkcs11.ImportCertificate([]byte(certID), cert)) + return keyID, "", certID, "" + }, + }, + "identified by label only": { + init: func(t *testing.T) (keyID, keyLabel, certID, certLabel string) { + t.Helper() + require := require.New(t) + prefix := uuid.New().String() + keyID, keyLabel, certID, certLabel = prefix+"keyID", prefix+"keyLabel", prefix+"certID", prefix+"certLabel" + pkcs11, err := crypto11.ConfigureFromFile(*configPath) + require.NoError(err) + defer pkcs11.Close() + privK, err := pkcs11.GenerateECDSAKeyPairWithLabel([]byte(keyID), []byte(keyLabel), elliptic.P256()) + require.NoError(err) + cert, err := createSelfSignedCertificate(privK) + require.NoError(err) + require.NoError(pkcs11.ImportCertificateWithLabel([]byte(certID), []byte(certLabel), cert)) + return "", keyLabel, "", certLabel + }, + }, + "key not found": { + init: func(t *testing.T) (keyID, keyLabel, certID, certLabel string) { + t.Helper() + require := require.New(t) + prefix := uuid.New().String() + keyID, keyLabel, certID, certLabel = prefix+"keyID", prefix+"keyLabel", prefix+"certID", prefix+"certLabel" + pkcs11, err := crypto11.ConfigureFromFile(*configPath) + require.NoError(err) + defer pkcs11.Close() + privK, err := pkcs11.GenerateECDSAKeyPairWithLabel([]byte(keyID), []byte(keyLabel), elliptic.P256()) + require.NoError(err) + cert, err := createSelfSignedCertificate(privK) + require.NoError(err) + require.NoError(pkcs11.ImportCertificateWithLabel([]byte(certID), []byte(certLabel), cert)) + return "not-found", "not-found", certID, certLabel + }, + wantErr: true, + }, + "cert not found": { + init: func(t *testing.T) (keyID, keyLabel, certID, certLabel string) { + t.Helper() + require := require.New(t) + prefix := uuid.New().String() + keyID, keyLabel, certID, certLabel = prefix+"keyID", prefix+"keyLabel", prefix+"certID", prefix+"certLabel" + pkcs11, err := crypto11.ConfigureFromFile(*configPath) + require.NoError(err) + defer pkcs11.Close() + _, err = pkcs11.GenerateECDSAKeyPairWithLabel([]byte(keyID), []byte(keyLabel), elliptic.P256()) + require.NoError(err) + return keyID, keyLabel, certID, certLabel + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + keyID, keyLabel, certID, certLabel := tc.init(t) + crt, cancel, err := LoadX509KeyPair(*configPath, keyID, keyLabel, certID, certLabel) + if tc.wantErr { + assert.Error(err) + return + } + require.NoError(err) + defer func() { + _ = cancel() + }() + + assert.NotNil(crt.PrivateKey) + assert.NotNil(crt.Certificate) + assert.NotNil(crt.Leaf) + + require.Len(crt.Certificate, 1) + leafCrt, err := x509.ParseCertificate(crt.Certificate[0]) + require.NoError(err) + assert.True(crt.Leaf.Equal(leafCrt)) + + privK, ok := crt.PrivateKey.(crypto.Signer) + require.True(ok) + pubK, ok := privK.Public().(interface{ Equal(crypto.PublicKey) bool }) + require.True(ok) + assert.True(pubK.Equal(crt.Leaf.PublicKey)) + }) + } +} + +func createSelfSignedCertificate(priv crypto.Signer) (*x509.Certificate, error) { + serialNumber, err := util.GenerateCertificateSerialNumber() + if err != nil { + return nil, err + } + now := time.Now() + template := &x509.Certificate{ + SerialNumber: serialNumber, + NotBefore: now.Add(-2 * time.Hour), + NotAfter: now.Add(2 * time.Hour), + } + cert, err := x509.CreateCertificate(rand.Reader, template, template, priv.Public(), priv) + if err != nil { + return nil, err + } + return x509.ParseCertificate(cert) +} diff --git a/docs/docs/reference/cli.md b/docs/docs/reference/cli.md index 33888827..c4926bb7 100644 --- a/docs/docs/reference/cli.md +++ b/docs/docs/reference/cli.md @@ -424,9 +424,14 @@ marblerun manifest update apply update-manifest.json $MARBLERUN --cert=admin-cer ### Options ``` - -c, --cert string PEM encoded admin certificate file (required) - -h, --help help for apply - -k, --key string PEM encoded admin key file (required) + -c, --cert string PEM encoded admin certificate file + -h, --help help for apply + -k, --key string PEM encoded admin key file + --pkcs11-cert-id string ID of the certificate in the PKCS#11 token + --pkcs11-cert-label string Label of the certificate in the PKCS#11 token + --pkcs11-config string Path to a PKCS#11 configuration file to load the client certificate with + --pkcs11-key-id string ID of the private key in the PKCS#11 token + --pkcs11-key-label string Label of the private key in the PKCS#11 token ``` ### Options inherited from parent commands @@ -467,9 +472,14 @@ marblerun manifest update acknowledge update-manifest.json $MARBLERUN --cert=adm ### Options ``` - -c, --cert string PEM encoded admin certificate file (required) - -h, --help help for acknowledge - -k, --key string PEM encoded admin key file (required) + -c, --cert string PEM encoded admin certificate file + -h, --help help for acknowledge + -k, --key string PEM encoded admin key file + --pkcs11-cert-id string ID of the certificate in the PKCS#11 token + --pkcs11-cert-label string Label of the certificate in the PKCS#11 token + --pkcs11-config string Path to a PKCS#11 configuration file to load the client certificate with + --pkcs11-key-id string ID of the private key in the PKCS#11 token + --pkcs11-key-label string Label of the private key in the PKCS#11 token ``` ### Options inherited from parent commands @@ -506,9 +516,14 @@ marblerun manifest update cancel $MARBLERUN --cert=admin-cert.pem --key=admin-ke ### Options ``` - -c, --cert string PEM encoded admin certificate file (required) - -h, --help help for cancel - -k, --key string PEM encoded admin key file (required) + -c, --cert string PEM encoded admin certificate file + -h, --help help for cancel + -k, --key string PEM encoded admin key file + --pkcs11-cert-id string ID of the certificate in the PKCS#11 token + --pkcs11-cert-label string Label of the certificate in the PKCS#11 token + --pkcs11-config string Path to a PKCS#11 configuration file to load the client certificate with + --pkcs11-key-id string ID of the private key in the PKCS#11 token + --pkcs11-key-label string Label of the private key in the PKCS#11 token ``` ### Options inherited from parent commands @@ -736,9 +751,14 @@ Set or retrieve a secret defined in the manifest. ### Options ``` - -c, --cert string PEM encoded MarbleRun user certificate file (required) - -h, --help help for secret - -k, --key string PEM encoded MarbleRun user key file (required) + -c, --cert string PEM encoded MarbleRun user certificate file + -h, --help help for secret + -k, --key string PEM encoded MarbleRun user key file + --pkcs11-cert-id string ID of the certificate in the PKCS#11 token + --pkcs11-cert-label string Label of the certificate in the PKCS#11 token + --pkcs11-config string Path to a PKCS#11 configuration file to load the client certificate with + --pkcs11-key-id string ID of the private key in the PKCS#11 token + --pkcs11-key-label string Label of the private key in the PKCS#11 token ``` ### Options inherited from parent commands @@ -795,13 +815,18 @@ marblerun secret set certificate.pem $MARBLERUN -c admin.crt -k admin.key --from ``` --accepted-advisories strings Comma-separated list of user accepted Intel Security Advisories for SWHardeningNeeded TCB status. If empty, all advisories are accepted --accepted-tcb-statuses strings Comma-separated list of user accepted TCB statuses (default [UpToDate,SWHardeningNeeded]) - -c, --cert string PEM encoded MarbleRun user certificate file (required) + -c, --cert string PEM encoded MarbleRun user certificate file --coordinator-cert string Path to MarbleRun Coordinator's root certificate to use for TLS connections (default "$HOME/.config/marblerun/coordinator-cert.pem") --era-config string Path to a remote-attestation config file in JSON format. If none is provided, the command attempts to use './coordinator-era.json'. If that does not exist, the command will attempt to load a matching config file from the MarbleRun GitHub repository -i, --insecure Set to skip quote verification, needed when running in simulation mode - -k, --key string PEM encoded MarbleRun user key file (required) + -k, --key string PEM encoded MarbleRun user key file -n, --namespace string Kubernetes namespace of the MarbleRun installation (default "marblerun") --nonce string (Optional) nonce to use for quote verification. If set, the Coordinator will generate a quote over sha256(CoordinatorCert + nonce) + --pkcs11-cert-id string ID of the certificate in the PKCS#11 token + --pkcs11-cert-label string Label of the certificate in the PKCS#11 token + --pkcs11-config string Path to a PKCS#11 configuration file to load the client certificate with + --pkcs11-key-id string ID of the private key in the PKCS#11 token + --pkcs11-key-label string Label of the private key in the PKCS#11 token --save-sgx-quote string If set, save the Coordinator's SGX quote to the specified file ``` @@ -839,13 +864,18 @@ marblerun secret get genericSecret symmetricKeyShared $MARBLERUN -c admin.crt -k ``` --accepted-advisories strings Comma-separated list of user accepted Intel Security Advisories for SWHardeningNeeded TCB status. If empty, all advisories are accepted --accepted-tcb-statuses strings Comma-separated list of user accepted TCB statuses (default [UpToDate,SWHardeningNeeded]) - -c, --cert string PEM encoded MarbleRun user certificate file (required) + -c, --cert string PEM encoded MarbleRun user certificate file --coordinator-cert string Path to MarbleRun Coordinator's root certificate to use for TLS connections (default "$HOME/.config/marblerun/coordinator-cert.pem") --era-config string Path to a remote-attestation config file in JSON format. If none is provided, the command attempts to use './coordinator-era.json'. If that does not exist, the command will attempt to load a matching config file from the MarbleRun GitHub repository -i, --insecure Set to skip quote verification, needed when running in simulation mode - -k, --key string PEM encoded MarbleRun user key file (required) + -k, --key string PEM encoded MarbleRun user key file -n, --namespace string Kubernetes namespace of the MarbleRun installation (default "marblerun") --nonce string (Optional) nonce to use for quote verification. If set, the Coordinator will generate a quote over sha256(CoordinatorCert + nonce) + --pkcs11-cert-id string ID of the certificate in the PKCS#11 token + --pkcs11-cert-label string Label of the certificate in the PKCS#11 token + --pkcs11-config string Path to a PKCS#11 configuration file to load the client certificate with + --pkcs11-key-id string ID of the private key in the PKCS#11 token + --pkcs11-key-label string Label of the private key in the PKCS#11 token --save-sgx-quote string If set, save the Coordinator's SGX quote to the specified file ``` diff --git a/docs/docs/workflows/user-authentication.md b/docs/docs/workflows/user-authentication.md new file mode 100644 index 00000000..d12f059e --- /dev/null +++ b/docs/docs/workflows/user-authentication.md @@ -0,0 +1,50 @@ +# User authentication + +Privileged workflows require a user to authenticate themselves to the MarbleRun Coordinator. +This is done by first [adding a user to the manifest](./define-manifest.md#users) and then using the certificate and matching private key as TLS client credentials when connecting to the Coordinator. + +## File-based authentication + +With file-based authentication, you need to have access to a private key and certificate as PEM-formatted files. +For example, you can generate your credentials using the following `openssl` command: + +```bash +openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout admin_private.pem -out admin_certificate.pem +``` + +You can then use these files to authenticate with the MarbleRun Coordinator: + +```bash +marblerun --cert admin_certificate.pem --key admin_private.pem [COMMAND] +``` + +## PKCS #11-based authentication + +The MarbleRun CLI supports authentication with private keys stored in a PKCS #11-compatible device. +To load client credentials using PKCS #11, you must provide a [PKCS #11 config file](https://pkg.go.dev/github.com/ThalesGroup/crypto11@v1.2.6#Config), which specifies how to access the PKCS #11 token. +Additionally, you must provide the ID and/or label of the private key and certificate to the CLI. + +The PKCS #11 config file is a JSON file that should specify the following fields: + +- `Path` (string) - The full path to the PKCS #11 shared library. +- `Pin` (string) - The PIN (password) to access the token. +- One of: + - `TokenSerial` (string) - Serial number of the token to use. + - `TokenLabel` (string) - Label of the token to use. + - `SlotNumber` (int) - Number of the slot containing the token to use. + +The following shows an example for authenticating with a key and certificate stored in a token with the label `marblerun-token`: + +```json +{ + "Path": "/usr/lib/softhsm/libsofthsm2.so", # Replace with path to your PKCS #11 shared library + "TokenLabel": "marblerun-token", + "Pin": "1234" # Replace with your token's actual password +} +``` + +Assuming the key and certificate have the label `marblerun-key` and `marblerun-cert` respectively, invoke the CLI as follows: + +```bash +marblerun --pkcs11-config /path/to/pkcs11-config.json --pkcs11-key-label marblerun-key --pkcs11-cert-label marblerun-cert [COMMAND] +``` diff --git a/docs/sidebars.js b/docs/sidebars.js index ec164185..112d2686 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -160,6 +160,11 @@ const sidebars = { label: 'Backup and restore', id: 'workflows/backup', }, + { + type: 'doc', + label: 'User authentication', + id: 'workflows/user-authentication', + }, { type: 'doc', label: 'Update a manifest', diff --git a/go.mod b/go.mod index be4eda67..53991f4f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/edgelesssys/marblerun go 1.23.3 require ( + github.com/ThalesGroup/crypto11 v1.2.6 github.com/cert-manager/cert-manager v1.16.2 github.com/edgelesssys/ego v1.6.0 github.com/gofrs/flock v0.12.1 @@ -109,6 +110,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/miekg/pkcs11 v1.1.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -133,6 +135,7 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cast v1.7.0 // indirect + github.com/thales-e-security/pool v0.0.2 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/x448/float16 v0.8.4 // indirect diff --git a/go.sum b/go.sum index 0d4f85cb..8122070b 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZ github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/ThalesGroup/crypto11 v1.2.6 h1:KixeJpVw3Y9gLSsz393XHh/Pez7q+KBXit4TQebmOz4= +github.com/ThalesGroup/crypto11 v1.2.6/go.mod h1:Grol7G+6zQdI94hGq+j702L1QFHSlJA5lBLl8uWAhG0= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= @@ -270,6 +272,8 @@ github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxU github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= @@ -378,6 +382,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= +github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=