Repository: scionproto/scion
Pull request #4617 (2024-09-13 19:11:45)

scion-pki: enable kms support
Enable the scion-pki tool to interact with various cloud KMS and HSMs through the step-kms-plugin. The step-kms-plugin must be installed and available in the PATH.

For more information about step-kms-plugin, please refer to the documentation at https://github.com/smallstep/step-kms-plugin.

To see example usage of step-kms-plugin, please
refer to
https://smallstep.com/docs/step-ca/cryptographic-protection

jiceatscion: 

<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/scionproto/scion/4617)
<!-- Reviewable:end -->

oncilla: FWIW. I think longterm we should just migrate to using step directly and provide scion specific things via plugins, rather than maintain our own scion-pki tool.

Patch:
diff --git a/doc/command/scion-pki/scion-pki.rst b/doc/command/scion-pki/scion-pki.rst
index a46874a65..fc3578253 100644
--- a/doc/command/scion-pki/scion-pki.rst
+++ b/doc/command/scion-pki/scion-pki.rst
@@ -26,6 +26,7 @@ SEE ALSO
 * :ref:`scion-pki certificate <scion-pki_certificate>` 	 - Manage certificates for the SCION control plane PKI.
 * :ref:`scion-pki completion <scion-pki_completion>` 	 - Generate the autocompletion script for the specified shell
 * :ref:`scion-pki key <scion-pki_key>` 	 - Manage private and public keys
+* :ref:`scion-pki kms <scion-pki_kms>` 	 - Run the step-kms-plugin
 * :ref:`scion-pki trc <scion-pki_trc>` 	 - Manage TRCs for the SCION control plane PKI
 * :ref:`scion-pki version <scion-pki_version>` 	 - Show the scion-pki version information
 
diff --git a/doc/command/scion-pki/scion-pki_certificate_create.rst b/doc/command/scion-pki/scion-pki_certificate_create.rst
index fb97bcf46..25aa3bcba 100644
--- a/doc/command/scion-pki/scion-pki_certificate_create.rst
+++ b/doc/command/scion-pki/scion-pki_certificate_create.rst
@@ -104,12 +104,14 @@ Options
       --bundle               Bundle the certificate with the issuer certificate as a certificate chain
       --ca string            The path to the issuer certificate
       --ca-key string        The path to the issuer private key used to sign the new certificate
+      --ca-kms string        The uri to configure a Cloud KMS or an HSM used for signing the certificate.
       --common-name string   The common name that replaces the common name in the subject template
       --csr                  Generate a certificate signign request instead of a certificate
       --curve string         The elliptic curve to use (P-256|P-384|P-521) (default "P-256")
       --force                Force overwritting existing files
   -h, --help                 help for create
       --key string           The path to the existing private key to use instead of creating a new one
+      --kms string           The uri to configure a Cloud KMS or an HSM.
       --not-after time       The NotAfter time of the certificate. Can either be a timestamp or an offset.
                              
                              If the value is a timestamp, it is expected to either be an RFC 3339 formatted
diff --git a/doc/command/scion-pki/scion-pki_certificate_match_private.rst b/doc/command/scion-pki/scion-pki_certificate_match_private.rst
index c9501438e..0dba125af 100644
--- a/doc/command/scion-pki/scion-pki_certificate_match_private.rst
+++ b/doc/command/scion-pki/scion-pki_certificate_match_private.rst
@@ -36,6 +36,7 @@ Options
 ::
 
   -h, --help               help for private
+      --kms string         The uri to configure a Cloud KMS or an HSM.
       --separator string   The separator between file names (default "\n")
 
 SEE ALSO
diff --git a/doc/command/scion-pki/scion-pki_certificate_sign.rst b/doc/command/scion-pki/scion-pki_certificate_sign.rst
index 625ec980c..34f3d726d 100644
--- a/doc/command/scion-pki/scion-pki_certificate_sign.rst
+++ b/doc/command/scion-pki/scion-pki_certificate_sign.rst
@@ -57,6 +57,7 @@ Options
       --bundle            Bundle the certificate with the issuer certificate as a certificate chain
       --ca string         The path to the issuer certificate
       --ca-key string     The path to the issuer private key used to sign the new certificate
+      --ca-kms string     The uri to configure a Cloud KMS or an HSM used for signing the certificate.
   -h, --help              help for sign
       --not-after time    The NotAfter time of the certificate. Can either be a timestamp or an offset.
                           
diff --git a/doc/command/scion-pki/scion-pki_key_match_certificate.rst b/doc/command/scion-pki/scion-pki_key_match_certificate.rst
index 2cc013f11..f86434d51 100644
--- a/doc/command/scion-pki/scion-pki_key_match_certificate.rst
+++ b/doc/command/scion-pki/scion-pki_key_match_certificate.rst
@@ -36,6 +36,7 @@ Options
 ::
 
   -h, --help               help for certificate
+      --kms string         The uri to configure a Cloud KMS or an HSM.
       --separator string   The separator between file names (default "\n")
 
 SEE ALSO
diff --git a/doc/command/scion-pki/scion-pki_key_public.rst b/doc/command/scion-pki/scion-pki_key_public.rst
index 2c5c160f4..a9d58887b 100644
--- a/doc/command/scion-pki/scion-pki_key_public.rst
+++ b/doc/command/scion-pki/scion-pki_key_public.rst
@@ -35,6 +35,7 @@ Options
 
       --force        Force overwritting existing public key
   -h, --help         help for public
+      --kms string   The uri to configure a Cloud KMS or an HSM.
       --out string   Path to write public key
 
 SEE ALSO
diff --git a/doc/command/scion-pki/scion-pki_kms.rst b/doc/command/scion-pki/scion-pki_kms.rst
new file mode 100644
index 000000000..f3fda47f2
--- /dev/null
+++ b/doc/command/scion-pki/scion-pki_kms.rst
@@ -0,0 +1,45 @@
+:orphan:
+
+.. _scion-pki_kms:
+
+scion-pki kms
+-------------
+
+Run the step-kms-plugin
+
+Synopsis
+~~~~~~~~
+
+
+This command leverages the step-kms-plugin to interact with cloud Key Management
+Systems (KMS) and Hardware Security Modules (HSM).
+
+The commands are passed directly to the step-kms-plugin. For more information on
+the available commands and their usage, please refer to the step-kms-plugin
+documentation at https://github.com/smallstep/step-kms-plugin. In order to enable
+KMS support, the step-kms-plugin must be installed and available in the PATH.
+
+Various commands of the scion-pki tool allow the use of KMS. In all cases, the
+private key needs to already exist in the KMS. To instruct the scion-pki tool to
+use the key in the KMS, the --kms flag must be set.
+
+For more information about supported KMSs and uri pattern, please consult
+https://smallstep.com/docs/step-ca/cryptographic-protection.
+
+
+::
+
+  scion-pki kms [command] [flags]
+
+Options
+~~~~~~~
+
+::
+
+  -h, --help   help for kms
+
+SEE ALSO
+~~~~~~~~
+
+* :ref:`scion-pki <scion-pki>` 	 - SCION Control Plane PKI Management Tool
+
diff --git a/doc/command/scion-pki/scion-pki_trc_sign.rst b/doc/command/scion-pki/scion-pki_trc_sign.rst
index db160eddb..0fb394e1c 100644
--- a/doc/command/scion-pki/scion-pki_trc_sign.rst
+++ b/doc/command/scion-pki/scion-pki_trc_sign.rst
@@ -46,6 +46,7 @@ Options
 ::
 
   -h, --help             help for sign
+      --kms string       The uri to configure a Cloud KMS or an HSM.
   -o, --out string       Output file path. If --out is set, --out-dir is ignored.
       --out-dir string   Output directory. If --out is set, --out-dir is ignored. (default ".")
 
diff --git a/scion-pki/BUILD.bazel b/scion-pki/BUILD.bazel
new file mode 100644
index 000000000..196f93c10
--- /dev/null
+++ b/scion-pki/BUILD.bazel
@@ -0,0 +1,12 @@
+load("//tools/lint:go.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "flags.go",
+        "plugin.go",
+    ],
+    importpath = "github.com/scionproto/scion/scion-pki",
+    visibility = ["//visibility:public"],
+    deps = ["@com_github_spf13_pflag//:go_default_library"],
+)
diff --git a/scion-pki/certs/BUILD.bazel b/scion-pki/certs/BUILD.bazel
index 85a32f4d7..8c53576a4 100644
--- a/scion-pki/certs/BUILD.bazel
+++ b/scion-pki/certs/BUILD.bazel
@@ -43,6 +43,7 @@ go_library(
         "//private/svc:go_default_library",
         "//private/tracing:go_default_library",
         "//private/trust:go_default_library",
+        "//scion-pki:go_default_library",
         "//scion-pki/encoding:go_default_library",
         "//scion-pki/file:go_default_library",
         "//scion-pki/key:go_default_library",
diff --git a/scion-pki/certs/create.go b/scion-pki/certs/create.go
index 17beaa61d..8cee7cc99 100644
--- a/scion-pki/certs/create.go
+++ b/scion-pki/certs/create.go
@@ -35,6 +35,7 @@ import (
 	"github.com/scionproto/scion/pkg/scrypto/cppki"
 	"github.com/scionproto/scion/private/app/command"
 	"github.com/scionproto/scion/private/app/flag"
+	scionpki "github.com/scionproto/scion/scion-pki"
 	"github.com/scionproto/scion/scion-pki/file"
 	"github.com/scionproto/scion/scion-pki/key"
 )
@@ -125,7 +126,9 @@ func newCreateCmd(pather command.Pather) *cobra.Command {
 		notAfter    flag.Time
 		ca          string
 		caKey       string
+		caKms       string
 		existingKey string
+		kms         string
 		curve       string
 		bundle      bool
 		force       bool
@@ -193,7 +196,7 @@ A valid example for a JSON formatted template::
 		Args: cobra.RangeArgs(2, 3),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			if len(args) == 2 && flags.existingKey == "" {
-				return serrors.New("positional key file is required")
+				return serrors.New("the positional key file is required")
 			}
 			ct, err := parseCertType(flags.profile)
 			if err != nil {
@@ -203,6 +206,9 @@ A valid example for a JSON formatted template::
 			if err != nil {
 				return serrors.Wrap("creating subject", err)
 			}
+			if flags.existingKey == "" && flags.kms != "" {
+				return serrors.New("the kms flag is only allowed with an existing key")
+			}
 
 			// Only check that the flags are set appropriately here.
 			// Do the actual parsing after the usage help message is silenced.
@@ -220,10 +226,10 @@ A valid example for a JSON formatted template::
 
 			cmd.SilenceUsage = true
 
-			var privKey key.PrivateKey
+			var privKey crypto.Signer
 			var encodedKey []byte
 			if flags.existingKey != "" {
-				if privKey, err = key.LoadPrivateKey(flags.existingKey); err != nil {
+				if privKey, err = key.LoadPrivateKey(flags.kms, flags.existingKey); err != nil {
 					return serrors.Wrap("loading existing private key", err)
 				}
 			} else {
@@ -234,10 +240,15 @@ A valid example for a JSON formatted template::
 					return serrors.Wrap("encoding fresh private key", err)
 				}
 			}
+			if !key.IsX509Signer(privKey) {
+				return serrors.New("the private key cannot be used in X.509 certificates",
+					"type", fmt.Sprintf("%T", privKey),
+				)
+			}
 
 			var caCertRaw []byte
 			var caCert *x509.Certificate
-			var caKey key.PrivateKey
+			var caKey crypto.Signer
 			if loadCA {
 				if caCertRaw, err = os.ReadFile(flags.ca); err != nil {
 					return serrors.Wrap("read CA certificate", err)
@@ -245,7 +256,7 @@ A valid example for a JSON formatted template::
 				if caCert, err = parseCertificate(caCertRaw); err != nil {
 					return serrors.Wrap("parsing CA certificate", err)
 				}
-				if caKey, err = key.LoadPrivateKey(flags.caKey); err != nil {
+				if caKey, err = key.LoadPrivateKey(flags.caKms, flags.caKey); err != nil {
 					return serrors.Wrap("loading CA private key", err)
 				}
 			}
@@ -272,6 +283,12 @@ A valid example for a JSON formatted template::
 				}
 				fmt.Printf("CSR successfully written to %q\n", csrFile)
 			} else {
+				if !key.IsX509Signer(caKey) {
+					return serrors.New("the CA key cannot be used to create X.509 certificates",
+						"type", fmt.Sprintf("%T", caKey),
+					)
+				}
+
 				cert, err := CreateCertificate(CertParams{
 					Type:      ct,
 					Subject:   subject,
@@ -359,7 +376,8 @@ offset from the current time.`,
 	cmd.Flags().BoolVar(&flags.force, "force", false,
 		"Force overwritting existing files",
 	)
-
+	scionpki.BindFlagKmsCA(cmd.Flags(), &flags.caKms)
+	scionpki.BindFlagKms(cmd.Flags(), &flags.kms)
 	return cmd
 }
 
diff --git a/scion-pki/certs/match.go b/scion-pki/certs/match.go
index 3e3691bb7..36c97bd47 100644
--- a/scion-pki/certs/match.go
+++ b/scion-pki/certs/match.go
@@ -26,6 +26,7 @@ import (
 	"github.com/scionproto/scion/pkg/private/serrors"
 	"github.com/scionproto/scion/pkg/scrypto/cppki"
 	"github.com/scionproto/scion/private/app/command"
+	scionpki "github.com/scionproto/scion/scion-pki"
 	"github.com/scionproto/scion/scion-pki/key"
 )
 
@@ -45,6 +46,7 @@ func newMatchCmd(pather command.Pather) *cobra.Command {
 func newMatchPrivateKey(pather command.Pather) *cobra.Command {
 	var flags struct {
 		separator string
+		kms       string
 	}
 	cmd := &cobra.Command{
 		Use:   "private <certificate> <private-key> [<private-key> ...]",
@@ -74,7 +76,7 @@ The output contains all the private keys that are authenticated by the certifica
 
 			var keys []string
 			for _, file := range args[1:] {
-				key, err := loadPackedPublicFromPrivate(file)
+				key, err := loadPackedPublicFromPrivate(flags.kms, file)
 				if err != nil {
 					fmt.Fprintf(os.Stderr, "WARN: ignoring %q: %s\n", file, err)
 					continue
@@ -92,11 +94,12 @@ The output contains all the private keys that are authenticated by the certifica
 		},
 	}
 	cmd.Flags().StringVar(&flags.separator, "separator", "\n", "The separator between file names")
+	scionpki.BindFlagKms(cmd.Flags(), &flags.kms)
 	return cmd
 }
 
-func loadPackedPublicFromPrivate(file string) ([]byte, error) {
-	key, err := key.LoadPrivateKey(file)
+func loadPackedPublicFromPrivate(kms, name string) ([]byte, error) {
+	key, err := key.LoadPrivateKey(kms, name)
 	if err != nil {
 		return nil, err
 	}
diff --git a/scion-pki/certs/renew.go b/scion-pki/certs/renew.go
index 7b94935e6..2b28c5de7 100644
--- a/scion-pki/certs/renew.go
+++ b/scion-pki/certs/renew.go
@@ -333,7 +333,11 @@ The template is expressed in JSON. A valid example::
 			span.SetTag("remote-options", remotes)
 
 			// Load private key.
-			privPrev, err := key.LoadPrivateKey(keyFile)
+			// XXX(roosd): The renewal process does currently not support KMS.
+			// This is a bit more involved, and requires some refactoring of the
+			// flags and the key loading/creation process. For now, KMS is also
+			// not a direct use-case for AS certificates.
+			privPrev, err := key.LoadPrivateKey("", keyFile)
 			if err != nil {
 				return serrors.Wrap("reading private key", err)
 			}
diff --git a/scion-pki/certs/sign.go b/scion-pki/certs/sign.go
index 08dc94237..6bc46f533 100644
--- a/scion-pki/certs/sign.go
+++ b/scion-pki/certs/sign.go
@@ -27,6 +27,7 @@ import (
 	"github.com/scionproto/scion/pkg/scrypto/cppki"
 	"github.com/scionproto/scion/private/app/command"
 	"github.com/scionproto/scion/private/app/flag"
+	scionpki "github.com/scionproto/scion/scion-pki"
 	"github.com/scionproto/scion/scion-pki/key"
 )
 
@@ -39,6 +40,7 @@ func newSignCmd(pather command.Pather) *cobra.Command {
 		notAfter  flag.Time
 		ca        string
 		caKey     string
+		caKms     string
 		bundle    bool
 	}
 	flags.notBefore = flag.Time{
@@ -115,10 +117,15 @@ and not to \--not-before.
 			if err != nil {
 				return serrors.Wrap("parsing CA certificate", err)
 			}
-			caKey, err := key.LoadPrivateKey(flags.caKey)
+			caKey, err := key.LoadPrivateKey(flags.caKms, flags.caKey)
 			if err != nil {
 				return serrors.Wrap("loading CA private key", err)
 			}
+			if !key.IsX509Signer(caKey) {
+				return serrors.New("the CA key cannot be used to create X.509 certificates",
+					"type", fmt.Sprintf("%T", caKey),
+				)
+			}
 
 			subject := csr.Subject
 			subject.ExtraNames = csr.Subject.Names
@@ -190,6 +197,7 @@ offset from the current time.`,
 	cmd.Flags().BoolVar(&flags.bundle, "bundle", false,
 		"Bundle the certificate with the issuer certificate as a certificate chain",
 	)
+	scionpki.BindFlagKmsCA(cmd.Flags(), &flags.caKms)
 	cmd.MarkFlagRequired("ca")
 	cmd.MarkFlagRequired("ca-key")
 
diff --git a/scion-pki/cmd/scion-pki/BUILD.bazel b/scion-pki/cmd/scion-pki/BUILD.bazel
index a05f4f19f..12379d8b6 100644
--- a/scion-pki/cmd/scion-pki/BUILD.bazel
+++ b/scion-pki/cmd/scion-pki/BUILD.bazel
@@ -11,6 +11,7 @@ go_library(
     name = "go_default_library",
     srcs = [
         "gendocs.go",
+        "kms.go",
         "main.go",
         "version.go",
     ],
@@ -20,6 +21,7 @@ go_library(
         "//pkg/private/serrors:go_default_library",
         "//private/app:go_default_library",
         "//private/env:go_default_library",
+        "//scion-pki:go_default_library",
         "//scion-pki/certs:go_default_library",
         "//scion-pki/key:go_default_library",
         "//scion-pki/testcrypto:go_default_library",
diff --git a/scion-pki/cmd/scion-pki/kms.go b/scion-pki/cmd/scion-pki/kms.go
new file mode 100644
index 000000000..99d1f6bbd
--- /dev/null
+++ b/scion-pki/cmd/scion-pki/kms.go
@@ -0,0 +1,113 @@
+// Copyright 2024 Anapaya Systems
+//
+// 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 main
+
+import (
+	"fmt"
+	"io"
+	"os/exec"
+	"strings"
+	"text/template"
+	"unicode"
+
+	"github.com/spf13/cobra"
+
+	scionpki "github.com/scionproto/scion/scion-pki"
+)
+
+func newKms(_ CommandPather) *cobra.Command {
+	var cmd = &cobra.Command{
+		Use:   "kms [command]",
+		Short: "Run the step-kms-plugin",
+		Long: `This command leverages the step-kms-plugin to interact with cloud Key Management
+Systems (KMS) and Hardware Security Modules (HSM).
+
+The commands are passed directly to the step-kms-plugin. For more information on
+the available commands and their usage, please refer to the step-kms-plugin
+documentation at https://github.com/smallstep/step-kms-plugin. In order to enable
+KMS support, the step-kms-plugin must be installed and available in the PATH.
+
+Various commands of the scion-pki tool allow the use of KMS. In all cases, the
+private key needs to already exist in the KMS. To instruct the scion-pki tool to
+use the key in the KMS, the --kms flag must be set.
+
+For more information about supported KMSs and uri pattern, please consult
+https://smallstep.com/docs/step-ca/cryptographic-protection.
+`,
+		RunE: func(c *cobra.Command, args []string) error {
+			file, err := scionpki.LookKms()
+			if err != nil {
+				return err
+			}
+			cmd := exec.Command(file, args...)
+			cmd.Stdin = c.InOrStdin()
+			cmd.Stdout = c.OutOrStdout()
+			cmd.Stderr = c.ErrOrStderr()
+			return cmd.Run()
+		},
+	}
+	cmd.SetHelpFunc(func(c *cobra.Command, args []string) {
+		if len(args) <= 2 {
+			err := tmpl(c.OutOrStdout(), `{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}}
+
+{{end}}`, c)
+			if err != nil {
+				c.PrintErrln(err)
+			}
+			fmt.Fprintf(c.OutOrStdout(), "\n\nstep-kms-plugin help output:\n\n")
+		}
+
+		file, err := scionpki.LookKms()
+		if err != nil {
+			c.PrintErrln(err)
+			return
+		}
+
+		cmd := exec.Command(file, args[1:]...)
+		cmd.Stdin = c.InOrStdin()
+		cmd.Stdout = c.OutOrStdout()
+		cmd.Stderr = c.ErrOrStderr()
+		if err := cmd.Run(); err != nil {
+			c.PrintErrln(err)
+		}
+	})
+
+	return cmd
+}
+
+// tmpl executes the given template text on data, writing the result to w.
+func tmpl(w io.Writer, text string, data interface{}) error {
+	t := template.New("top")
+	t.Funcs(templateFuncs)
+	template.Must(t.Parse(text))
+	return t.Execute(w, data)
+}
+
+var templateFuncs = template.FuncMap{
+	"trim":                    strings.TrimSpace,
+	"trimRightSpace":          trimRightSpace,
+	"trimTrailingWhitespaces": trimRightSpace,
+	"rpad":                    rpad,
+	"removeEscape":            removeEscape,
+}
+
+func trimRightSpace(s string) string {
+	return strings.TrimRightFunc(s, unicode.IsSpace)
+}
+
+// rpad adds padding to the right of a string.
+func rpad(s string, padding int) string {
+	return fmt.Sprintf("%-*s", padding, s)
+}
diff --git a/scion-pki/cmd/scion-pki/main.go b/scion-pki/cmd/scion-pki/main.go
index c64f4afd7..6e0d88d02 100644
--- a/scion-pki/cmd/scion-pki/main.go
+++ b/scion-pki/cmd/scion-pki/main.go
@@ -57,15 +57,12 @@ func main() {
 		trcs.Cmd(cmd),
 		testcrypto.Cmd(cmd),
 		newGendocs(cmd),
+		newKms(cmd),
 	)
 	// This Templatefunc allows use some escape characters for the rst
 	// documentation conversion without compromising the readability of the help
 	// text in the CLI.
-	cobra.AddTemplateFunc("removeEscape", func(s string) string {
-		s = strings.ReplaceAll(s, "::", ":")
-		s = strings.ReplaceAll(s, "\\-", "-")
-		return s
-	})
+	cobra.AddTemplateFunc("removeEscape", removeEscape)
 
 	cmd.SetHelpTemplate(`{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces | removeEscape}}
 
@@ -80,3 +77,9 @@ func main() {
 		os.Exit(1)
 	}
 }
+
+func removeEscape(s string) string {
+	s = strings.ReplaceAll(s, "::", ":")
+	s = strings.ReplaceAll(s, "\\-", "-")
+	return s
+}
diff --git a/scion-pki/flags.go b/scion-pki/flags.go
new file mode 100644
index 000000000..690748000
--- /dev/null
+++ b/scion-pki/flags.go
@@ -0,0 +1,29 @@
+// Copyright 2024 Anapaya Systems
+//
+// 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 scionpki
+
+import "github.com/spf13/pflag"
+
+func BindFlagKms(flags *pflag.FlagSet, kms *string) {
+	flags.StringVar(kms, "kms", "",
+		"The uri to configure a Cloud KMS or an HSM.",
+	)
+}
+
+func BindFlagKmsCA(flags *pflag.FlagSet, kms *string) {
+	flags.StringVar(kms, "ca-kms", "",
+		"The uri to configure a Cloud KMS or an HSM used for signing the certificate.",
+	)
+}
diff --git a/scion-pki/key/BUILD.bazel b/scion-pki/key/BUILD.bazel
index 173a22eb0..ea179503e 100644
--- a/scion-pki/key/BUILD.bazel
+++ b/scion-pki/key/BUILD.bazel
@@ -3,6 +3,7 @@ load("//tools/lint:go.bzl", "go_library", "go_test")
 go_library(
     name = "go_default_library",
     srcs = [
+        "cryptoutil.go",
         "fingerprint.go",
         "key.go",
         "match.go",
@@ -17,6 +18,7 @@ go_library(
         "//pkg/scrypto:go_default_library",
         "//pkg/scrypto/cppki:go_default_library",
         "//private/app/command:go_default_library",
+        "//scion-pki:go_default_library",
         "//scion-pki/encoding:go_default_library",
         "//scion-pki/file:go_default_library",
         "@com_github_spf13_cobra//:go_default_library",
diff --git a/scion-pki/key/cryptoutil.go b/scion-pki/key/cryptoutil.go
new file mode 100644
index 000000000..4cda6bf09
--- /dev/null
+++ b/scion-pki/key/cryptoutil.go
@@ -0,0 +1,144 @@
+//  Copyright 2020 Smallstep Labs, 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.
+
+// This file is a copy of
+// https://github.com/smallstep/cli/blob/111bcb9cfbb101718f9c4a39f5ab439504b9c07f/internal/cryptoutil/cryptoutil.go
+// with the irrelevant parts stripped out and small adjustments to make it fit
+// our codebase.
+
+package key
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/ed25519"
+	"crypto/rsa"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"io"
+	"os/exec"
+
+	scionpki "github.com/scionproto/scion/scion-pki"
+)
+
+// IsX509Signer returns true if the given signer is supported by Go's
+// crypto/x509 package to sign X509 certificates. This methods returns true
+// for ECDSA, RSA and Ed25519 keys.
+func IsX509Signer(signer crypto.Signer) bool {
+	if signer == nil {
+		return false
+	}
+	pub := signer.Public()
+	switch pub.(type) {
+	case *ecdsa.PublicKey, *rsa.PublicKey, ed25519.PublicKey:
+		return true
+	default:
+		return false
+	}
+}
+
+type kmsSigner struct {
+	crypto.PublicKey
+	name     string
+	kms, key string
+}
+
+// exitError returns the error displayed on stderr after running the given
+// command.
+func exitError(cmd *exec.Cmd, err error) error {
+	var ee *exec.ExitError
+	if errors.As(err, &ee) {
+		return fmt.Errorf("command %q failed with:\n%s", cmd.String(), ee.Stderr)
+	}
+	return fmt.Errorf("command %q failed with: %w", cmd.String(), err)
+}
+
+// newKMSSigner creates a signer using `step-kms-plugin` as the signer.
+func newKMSSigner(kms, key string) (*kmsSigner, error) {
+	name, err := scionpki.LookKms()
+	if err != nil {
+		return nil, err
+	}
+
+	args := []string{"key"}
+	if kms != "" {
+		args = append(args, "--kms", kms)
+	}
+	args = append(args, key)
+
+	// Get public key
+	cmd := exec.Command(name, args...)
+	out, err := cmd.Output()
+	if err != nil {
+		return nil, exitError(cmd, err)
+	}
+
+	pub, err := loadPublicKeyPem(out)
+	if err != nil {
+		return nil, err
+	}
+
+	return &kmsSigner{
+		PublicKey: pub,
+		name:      name,
+		kms:       kms,
+		key:       key,
+	}, nil
+}
+
+// Public implements crypto.Signer and returns the public key.
+func (s *kmsSigner) Public() crypto.PublicKey {
+	return s.PublicKey
+}
+
+// Sign implements crypto.Signer using the `step-kms-plugin`.
+func (s *kmsSigner) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
+	args := []string{"sign", "--format", "base64"}
+	if s.kms != "" {
+		args = append(args, "--kms", s.kms)
+	}
+	if _, ok := s.PublicKey.(*rsa.PublicKey); ok {
+		if _, pss := opts.(*rsa.PSSOptions); pss {
+			args = append(args, "--pss")
+		}
+		switch opts.HashFunc() {
+		case crypto.SHA256:
+			args = append(args, "--alg", "SHA256")
+		case crypto.SHA384:
+			args = append(args, "--alg", "SHA384")
+		case crypto.SHA512:
+			args = append(args, "--alg", "SHA512")
+		default:
+			return nil, fmt.Errorf("unsupported hash function %q", opts.HashFunc().String())
+		}
+	}
+	args = append(args, s.key)
+
+	//nolint:gosec // arguments controlled by step.
+	cmd := exec.Command(s.name, args...)
+	stdin, err := cmd.StdinPipe()
+	if err != nil {
+		return nil, err
+	}
+	go func() {
+		defer stdin.Close()
+		_, _ = stdin.Write(digest)
+	}()
+	out, err := cmd.Output()
+	if err != nil {
+		return nil, exitError(cmd, err)
+	}
+	return base64.StdEncoding.DecodeString(string(out))
+}
diff --git a/scion-pki/key/fingerprint.go b/scion-pki/key/fingerprint.go
index 07d4c147e..88d242e45 100644
--- a/scion-pki/key/fingerprint.go
+++ b/scion-pki/key/fingerprint.go
@@ -106,6 +106,10 @@ func loadPublicKey(filename string) (crypto.PublicKey, error) {
 	if err != nil {
 		return nil, serrors.Wrap("reading input file", err)
 	}
+	return loadPublicKeyPem(raw)
+}
+
+func loadPublicKeyPem(raw []byte) (crypto.PublicKey, error) {
 	block, _ := pem.Decode(raw)
 	if block == nil {
 		return nil, serrors.New("parsing input failed")
diff --git a/scion-pki/key/match.go b/scion-pki/key/match.go
index c69da0ab2..e9b92dfa6 100644
--- a/scion-pki/key/match.go
+++ b/scion-pki/key/match.go
@@ -26,6 +26,7 @@ import (
 	"github.com/scionproto/scion/pkg/private/serrors"
 	"github.com/scionproto/scion/pkg/scrypto/cppki"
 	"github.com/scionproto/scion/private/app/command"
+	scionpki "github.com/scionproto/scion/scion-pki"
 )
 
 func newMatchCmd(pather command.Pather) *cobra.Command {
@@ -44,6 +45,7 @@ func newMatchCmd(pather command.Pather) *cobra.Command {
 func newMatchCertificate(pather command.Pather) *cobra.Command {
 	var flags struct {
 		separator string
+		kms       string
 	}
 	cmd := &cobra.Command{
 		Use:   "certificate <private-key> <certificate> [<certificate> ...]",
@@ -62,7 +64,7 @@ The output contains all certificates that authenticate the key.
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cmd.SilenceUsage = true
 
-			key, err := LoadPrivateKey(args[0])
+			key, err := LoadPrivateKey(flags.kms, args[0])
 			if err != nil {
 				return err
 			}
@@ -91,6 +93,7 @@ The output contains all certificates that authenticate the key.
 		},
 	}
 	cmd.Flags().StringVar(&flags.separator, "separator", "\n", "The separator between file names")
+	scionpki.BindFlagKms(cmd.Flags(), &flags.kms)
 	return cmd
 }
 
diff --git a/scion-pki/key/private.go b/scion-pki/key/private.go
index 27dd019a1..460d59659 100644
--- a/scion-pki/key/private.go
+++ b/scion-pki/key/private.go
@@ -84,7 +84,7 @@ The contents are the private key in PKCS #8 ASN.1 DER format.
 }
 
 // GeneratePrivateKey generates a new private key.
-func GeneratePrivateKey(curve string) (PrivateKey, error) {
+func GeneratePrivateKey(curve string) (crypto.Signer, error) {
 	switch strings.ToLower(curve) {
 	case "p-256", "p256":
 		return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
diff --git a/scion-pki/key/public.go b/scion-pki/key/public.go
index 6a587afea..55e088082 100644
--- a/scion-pki/key/public.go
+++ b/scion-pki/key/public.go
@@ -26,6 +26,7 @@ import (
 
 	"github.com/scionproto/scion/pkg/private/serrors"
 	"github.com/scionproto/scion/private/app/command"
+	scionpki "github.com/scionproto/scion/scion-pki"
 	"github.com/scionproto/scion/scion-pki/file"
 )
 
@@ -35,6 +36,7 @@ func NewPublicCmd(pather command.Pather) *cobra.Command {
 	var flags struct {
 		out   string
 		force bool
+		kms   string
 	}
 	var cmd = &cobra.Command{
 		Use:   "public [flags] <private-key-file>",
@@ -50,7 +52,7 @@ By default, the public key is written to standard out.
 			cmd.SilenceUsage = true
 
 			filename := args[0]
-			priv, err := LoadPrivateKey(filename)
+			priv, err := LoadPrivateKey(flags.kms, filename)
 			if err != nil {
 				return err
 			}
@@ -89,36 +91,40 @@ By default, the public key is written to standard out.
 	cmd.Flags().BoolVar(&flags.force, "force", false,
 		"Force overwritting existing public key",
 	)
+	scionpki.BindFlagKms(cmd.Flags(), &flags.kms)
 	return cmd
 }
 
 // LoadPrivate key loads a private key from file.
-func LoadPrivateKey(filename string) (crypto.Signer, error) {
-	raw, err := os.ReadFile(filename)
-	if err != nil {
-		return nil, serrors.Wrap("reading private key", err)
-	}
-	p, rest := pem.Decode(raw)
-	if p == nil {
-		return nil, serrors.New("parsing private key failed")
-	}
-	if len(rest) != 0 {
-		return nil, serrors.New("file must only contain private key")
-	}
-	if p.Type != "PRIVATE KEY" {
-		return nil, serrors.New("file does not contain a private key", "type", p.Type)
-	}
+func LoadPrivateKey(kms, name string) (crypto.Signer, error) {
+	if kms == "" {
+		raw, err := os.ReadFile(name)
+		if err != nil {
+			return nil, serrors.Wrap("reading private key", err)
+		}
+		p, rest := pem.Decode(raw)
+		if p == nil {
+			return nil, serrors.New("parsing private key failed")
+		}
+		if len(rest) != 0 {
+			return nil, serrors.New("file must only contain private key")
+		}
+		if p.Type != "PRIVATE KEY" {
+			return nil, serrors.New("file does not contain a private key", "type", p.Type)
+		}
 
-	key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
-	if err != nil {
-		return nil, serrors.Wrap("parsing private key", err)
-	}
+		key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
+		if err != nil {
+			return nil, serrors.Wrap("parsing private key", err)
+		}
 
-	priv, ok := key.(crypto.Signer)
-	if !ok {
-		return nil, serrors.New("cannot get public key from private key",
-			"type", fmt.Sprintf("%T", key),
-		)
+		priv, ok := key.(crypto.Signer)
+		if !ok {
+			return nil, serrors.New("cannot get public key from private key",
+				"type", fmt.Sprintf("%T", key),
+			)
+		}
+		return priv, nil
 	}
-	return priv, nil
+	return newKMSSigner(kms, name)
 }
diff --git a/scion-pki/plugin.go b/scion-pki/plugin.go
new file mode 100644
index 000000000..b720e75f1
--- /dev/null
+++ b/scion-pki/plugin.go
@@ -0,0 +1,32 @@
+// Copyright 2024 Anapaya Systems
+//
+// 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 scionpki
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+)
+
+func LookKms() (string, error) {
+	path, err := exec.LookPath("step-kms-plugin")
+	if err != nil {
+		fmt.Fprintln(os.Stderr, "step-kms-plugin not found in PATH\n"+
+			"Install it from https://github.com/smallstep/step-kms-plugin",
+		)
+		return "", err
+	}
+	return path, nil
+}
diff --git a/scion-pki/trcs/BUILD.bazel b/scion-pki/trcs/BUILD.bazel
index 71af983a4..7ed45950d 100644
--- a/scion-pki/trcs/BUILD.bazel
+++ b/scion-pki/trcs/BUILD.bazel
@@ -25,6 +25,7 @@ go_library(
         "//pkg/scrypto/cms/protocol:go_default_library",
         "//pkg/scrypto/cppki:go_default_library",
         "//private/app/command:go_default_library",
+        "//scion-pki:go_default_library",
         "//scion-pki/conf:go_default_library",
         "//scion-pki/file:go_default_library",
         "//scion-pki/key:go_default_library",
diff --git a/scion-pki/trcs/sign.go b/scion-pki/trcs/sign.go
index a0bb53a9f..2f3c5ced4 100644
--- a/scion-pki/trcs/sign.go
+++ b/scion-pki/trcs/sign.go
@@ -31,6 +31,7 @@ import (
 	"github.com/scionproto/scion/pkg/scrypto/cms/protocol"
 	"github.com/scionproto/scion/pkg/scrypto/cppki"
 	"github.com/scionproto/scion/private/app/command"
+	scionpki "github.com/scionproto/scion/scion-pki"
 	"github.com/scionproto/scion/scion-pki/key"
 )
 
@@ -38,6 +39,7 @@ func newSign(pather command.Pather) *cobra.Command {
 	var flags struct {
 		out    string
 		outDir string
+		kms    string
 	}
 
 	cmd := &cobra.Command{
@@ -66,7 +68,7 @@ a TRC signing ceremony.
 		Args: cobra.ExactArgs(3),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cmd.SilenceUsage = true
-			return RunSign(args[0], args[1], args[2], flags.out, flags.outDir)
+			return RunSign(args[0], args[1], args[2], flags.kms, flags.out, flags.outDir)
 		},
 	}
 
@@ -75,11 +77,11 @@ a TRC signing ceremony.
 	)
 	cmd.Flags().StringVar(&flags.outDir, "out-dir", ".", "Output directory. "+
 		"If --out is set, --out-dir is ignored.")
-
+	scionpki.BindFlagKms(cmd.Flags(), &flags.kms)
 	return cmd
 }
 
-func RunSign(pld, certfile, keyfile, out, outDir string) error {
+func RunSign(pld, certfile, keyName, kms, out, outDir string) error {
 	dummy := pld == "dummy"
 
 	// Read TRC payload
@@ -97,7 +99,7 @@ func RunSign(pld, certfile, keyfile, out, outDir string) error {
 		rawPld = pldBlock.Bytes
 	}
 	// Load signing key
-	priv, err := key.LoadPrivateKey(keyfile)
+	priv, err := key.LoadPrivateKey(kms, keyName)
 	if err != nil {
 		return err
 	}


Test patch:
diff --git a/scion-pki/certs/create_test.go b/scion-pki/certs/create_test.go
index 10fb6f094..6a2fe33b0 100644
--- a/scion-pki/certs/create_test.go
+++ b/scion-pki/certs/create_test.go
@@ -610,7 +610,7 @@ func TestNewCreateCmdCSR(t *testing.T) {
 			Validate: func(t *testing.T, csr *x509.CertificateRequest) {
 				require.NoError(t, csr.CheckSignature())
 				require.Equal(t, "1-ff00:0:111 Certificate", csr.Subject.CommonName)
-				priv, err := key.LoadPrivateKey("testdata/create/private.key")
+				priv, err := key.LoadPrivateKey("", "testdata/create/private.key")
 				require.NoError(t, err)
 				require.Equal(t, priv.Public(), csr.PublicKey)
 			},
diff --git a/scion-pki/certs/renew_test.go b/scion-pki/certs/renew_test.go
index d6b52229b..31fa5eef3 100644
--- a/scion-pki/certs/renew_test.go
+++ b/scion-pki/certs/renew_test.go
@@ -107,7 +107,7 @@ func TestExtractChain(t *testing.T) {
 	chain := xtest.LoadChain(t, "testdata/renew/ISD1-ASff00_0_111.pem")
 
 	caChain := xtest.LoadChain(t, "testdata/renew/ISD1-ASff00_0_110.pem")
-	key, err := key.LoadPrivateKey("testdata/renew/cp-as-110.key")
+	key, err := key.LoadPrivateKey("", "testdata/renew/cp-as-110.key")
 	require.NoError(t, err)
 	caSigner := trust.Signer{
 		PrivateKey:   key,
diff --git a/scion-pki/key/private_test.go b/scion-pki/key/private_test.go
index a47a236a5..4110aa8d3 100644
--- a/scion-pki/key/private_test.go
+++ b/scion-pki/key/private_test.go
@@ -91,7 +91,7 @@ func TestNewPrivateCmd(t *testing.T) {
 				return
 			}
 			filename := tc.Args[len(tc.Args)-1]
-			_, err = key.LoadPrivateKey(filename)
+			_, err = key.LoadPrivateKey("", filename)
 			require.NoError(t, err)
 
 			info, err := os.Stat(filename)
diff --git a/scion-pki/testcrypto/testcrypto.go b/scion-pki/testcrypto/testcrypto.go
index a641ee813..a6884ad88 100644
--- a/scion-pki/testcrypto/testcrypto.go
+++ b/scion-pki/testcrypto/testcrypto.go
@@ -366,12 +366,11 @@ func createTRCs(cfg config) error {
 }
 
 func loadVoterInfo(voter addr.IA, votingDir string) (*voterInfo, error) {
-	sensitiveKey, err := key.LoadPrivateKey(
-		filepath.Join(votingDir, "sensitive-voting.key"))
+	sensitiveKey, err := key.LoadPrivateKey("", filepath.Join(votingDir, "sensitive-voting.key"))
 	if err != nil {
 		return nil, serrors.Wrap("loading sensitive key", err)
 	}
-	regularKey, err := key.LoadPrivateKey(filepath.Join(votingDir, "regular-voting.key"))
+	regularKey, err := key.LoadPrivateKey("", filepath.Join(votingDir, "regular-voting.key"))
 	if err != nil {
 		return nil, serrors.Wrap("loading regular key", err)
 	}
diff --git a/scion-pki/trcs/sign_test.go b/scion-pki/trcs/sign_test.go
index 5e5de9f93..89e9ae7d9 100644
--- a/scion-pki/trcs/sign_test.go
+++ b/scion-pki/trcs/sign_test.go
@@ -100,7 +100,7 @@ func TestSign(t *testing.T) {
 	}
 	for name, tc := range testCases {
 		t.Run(name, func(t *testing.T) {
-			err := trcs.RunSign(tc.pld, tc.cert, tc.key, "", outDir)
+			err := trcs.RunSign(tc.pld, tc.cert, tc.key, "", "", outDir)
 			assert.NoError(t, err)
 			fn := filepath.Join(outDir, fmt.Sprintf("ISD1-B1-S2.1-1-%s.trc", tc.signType))
 
@@ -159,7 +159,7 @@ func TestOpensslCompatible(t *testing.T) {
 	for name, tc := range testCases {
 		name, tc := name, tc
 		t.Run(name, func(t *testing.T) {
-			err := trcs.RunSign(tc.pld, tc.cert, tc.key, "", outDir)
+			err := trcs.RunSign(tc.pld, tc.cert, tc.key, "", "", outDir)
 			assert.NoError(t, err)
 			fn := filepath.Join(outDir, fmt.Sprintf("ISD1-B1-S2.1-1-%s.trc", tc.signType))