From 5550a98862bcd855b2d53381e8faff1aab1576ef Mon Sep 17 00:00:00 2001 From: Dominik Roos Date: Tue, 24 Sep 2024 08:05:39 +0200 Subject: [PATCH 1/2] scion-pki: enable kms support (#4617) 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 --- doc/command/scion-pki/scion-pki.rst | 1 + .../scion-pki_certificate_create.rst | 2 + .../scion-pki_certificate_match_private.rst | 1 + .../scion-pki/scion-pki_certificate_sign.rst | 1 + .../scion-pki_key_match_certificate.rst | 1 + .../scion-pki/scion-pki_key_public.rst | 1 + doc/command/scion-pki/scion-pki_kms.rst | 45 ++++++ doc/command/scion-pki/scion-pki_trc_sign.rst | 1 + scion-pki/BUILD.bazel | 12 ++ scion-pki/certs/BUILD.bazel | 1 + scion-pki/certs/create.go | 30 +++- scion-pki/certs/create_test.go | 2 +- scion-pki/certs/match.go | 9 +- scion-pki/certs/renew.go | 6 +- scion-pki/certs/renew_test.go | 2 +- scion-pki/certs/sign.go | 10 +- scion-pki/cmd/scion-pki/BUILD.bazel | 2 + scion-pki/cmd/scion-pki/kms.go | 113 ++++++++++++++ scion-pki/cmd/scion-pki/main.go | 13 +- scion-pki/flags.go | 29 ++++ scion-pki/key/BUILD.bazel | 2 + scion-pki/key/cryptoutil.go | 144 ++++++++++++++++++ scion-pki/key/fingerprint.go | 4 + scion-pki/key/match.go | 5 +- scion-pki/key/private.go | 2 +- scion-pki/key/private_test.go | 2 +- scion-pki/key/public.go | 58 +++---- scion-pki/plugin.go | 32 ++++ scion-pki/testcrypto/testcrypto.go | 5 +- scion-pki/trcs/BUILD.bazel | 1 + scion-pki/trcs/sign.go | 10 +- scion-pki/trcs/sign_test.go | 4 +- 32 files changed, 495 insertions(+), 56 deletions(-) create mode 100644 doc/command/scion-pki/scion-pki_kms.rst create mode 100644 scion-pki/BUILD.bazel create mode 100644 scion-pki/cmd/scion-pki/kms.go create mode 100644 scion-pki/flags.go create mode 100644 scion-pki/key/cryptoutil.go create mode 100644 scion-pki/plugin.go diff --git a/doc/command/scion-pki/scion-pki.rst b/doc/command/scion-pki/scion-pki.rst index a46874a656..fc3578253a 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 ` - Manage certificates for the SCION control plane PKI. * :ref:`scion-pki completion ` - Generate the autocompletion script for the specified shell * :ref:`scion-pki key ` - Manage private and public keys +* :ref:`scion-pki kms ` - Run the step-kms-plugin * :ref:`scion-pki trc ` - Manage TRCs for the SCION control plane PKI * :ref:`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 fb97bcf466..25aa3bcba3 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 c9501438e0..0dba125af1 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 625ec980cc..34f3d726df 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 2cc013f11a..f86434d514 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 2c5c160f4d..a9d58887b8 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 0000000000..f3fda47f26 --- /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 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 db160eddb0..0fb394e1c9 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 0000000000..196f93c102 --- /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 85a32f4d7b..8c53576a4c 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 17beaa61d5..8cee7cc991 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/create_test.go b/scion-pki/certs/create_test.go index 10fb6f0943..6a2fe33b07 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/match.go b/scion-pki/certs/match.go index 3e3691bb74..36c97bd47f 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 [ ...]", @@ -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 7b94935e62..2b28c5de72 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/renew_test.go b/scion-pki/certs/renew_test.go index d6b52229b2..31fa5eef31 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/certs/sign.go b/scion-pki/certs/sign.go index 08dc94237f..6bc46f5335 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 a05f4f19fa..12379d8b6f 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 0000000000..99d1f6bbda --- /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 c64f4afd79..6e0d88d029 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 0000000000..690748000f --- /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 173a22eb0d..ea179503e3 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 0000000000..4cda6bf093 --- /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 07d4c147ec..88d242e45b 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 c69da0ab2b..e9b92dfa63 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 [ ...]", @@ -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 27dd019a1c..460d596598 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/private_test.go b/scion-pki/key/private_test.go index a47a236a5e..4110aa8d32 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/key/public.go b/scion-pki/key/public.go index 6a587afea5..55e088082d 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] ", @@ -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 0000000000..b720e75f10 --- /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/testcrypto/testcrypto.go b/scion-pki/testcrypto/testcrypto.go index a641ee813a..a6884ad884 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/BUILD.bazel b/scion-pki/trcs/BUILD.bazel index 71af983a4d..7ed45950df 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 a0bb53a9f1..2f3c5ced40 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 } diff --git a/scion-pki/trcs/sign_test.go b/scion-pki/trcs/sign_test.go index 5e5de9f937..89e9ae7d97 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)) From 9ceb22a4ec08ff69278540cfcf0c84845e0e8e99 Mon Sep 17 00:00:00 2001 From: Lukas Vogel Date: Tue, 24 Sep 2024 13:31:53 +0200 Subject: [PATCH 2/2] daemon/combinator: memoize calculated fingerprints (IDs) (#4608) A profile shows that ID calculation is a big part of the CPU time of the daemon: ![image](https://github.com/user-attachments/assets/613eedfe-c969-41ef-aa78-bee1488f9afc) Therefore this change memoizes IDs where possible. Also re-use buffer for fingerprint calculation. Use slices package for sorting in the combinator. --- private/path/combinator/combinator.go | 48 ++++++++---- private/path/combinator/combinator_test.go | 1 + private/path/combinator/export_test.go | 2 + private/path/combinator/graph.go | 90 ++++++++++------------ private/revcache/util.go | 3 - 5 files changed, 77 insertions(+), 67 deletions(-) diff --git a/private/path/combinator/combinator.go b/private/path/combinator/combinator.go index 14b15d8ba9..a1d7dcc4fd 100644 --- a/private/path/combinator/combinator.go +++ b/private/path/combinator/combinator.go @@ -23,7 +23,8 @@ package combinator import ( "crypto/sha256" "encoding/binary" - "sort" + "hash" + "slices" "github.com/scionproto/scion/pkg/addr" seg "github.com/scionproto/scion/pkg/segment" @@ -61,8 +62,9 @@ func Combine(src, dst addr.IA, ups, cores, downs []*seg.PathSegment, solutions := newDMG(ups, cores, downs).GetPaths(vertexFromIA(src), vertexFromIA(dst)) paths := make([]Path, len(solutions)) + st := newHashState() for i, solution := range solutions { - paths[i] = solution.Path() + paths[i] = solution.Path(st) } paths = filterLongPaths(paths) if !findAllIdentical { @@ -72,10 +74,11 @@ func Combine(src, dst addr.IA, ups, cores, downs []*seg.PathSegment, } type Path struct { - Dst addr.IA - SCIONPath path.SCION - Metadata snet.PathMetadata - Weight int // XXX(matzf): unused, drop this? + Dst addr.IA + SCIONPath path.SCION + Metadata snet.PathMetadata + Weight int // XXX(matzf): unused, drop this? + Fingerprint snet.PathFingerprint } // filterLongPaths returns a new slice containing only those paths that do not @@ -112,10 +115,9 @@ func filterDuplicates(paths []Path) []Path { // unique path interface sequence (== fingerprint). uniquePaths := make(map[snet.PathFingerprint]int) for i, p := range paths { - key := fingerprint(p.Metadata.Interfaces) - prev, dupe := uniquePaths[key] + prev, dupe := uniquePaths[p.Fingerprint] if !dupe || p.Metadata.Expiry.After(paths[prev].Metadata.Expiry) { - uniquePaths[key] = i + uniquePaths[p.Fingerprint] = i } } @@ -123,7 +125,7 @@ func filterDuplicates(paths []Path) []Path { for _, idx := range uniquePaths { toKeep = append(toKeep, idx) } - sort.Ints(toKeep) + slices.Sort(toKeep) filtered := make([]Path, 0, len(toKeep)) for _, i := range toKeep { filtered = append(filtered, paths[i]) @@ -135,8 +137,9 @@ func filterDuplicates(paths []Path) []Path { // ASes and BRs, i.e. by its PathInterfaces. // XXX(matzf): copied from snet.Fingerprint. Perhaps snet.Fingerprint could be adapted to // take []snet.PathInterface directly. -func fingerprint(interfaces []snet.PathInterface) snet.PathFingerprint { - h := sha256.New() +func fingerprint(interfaces []snet.PathInterface, st hashState) snet.PathFingerprint { + h := st.hash + h.Reset() for _, intf := range interfaces { if err := binary.Write(h, binary.BigEndian, intf.IA); err != nil { panic(err) @@ -145,5 +148,24 @@ func fingerprint(interfaces []snet.PathInterface) snet.PathFingerprint { panic(err) } } - return snet.PathFingerprint(h.Sum(nil)) + // convert the snet.PathFingerprint, this is safe because the underlying + // type is string. + return snet.PathFingerprint(h.Sum(st.buf[:0])) +} + +func checkUnderlyingType[S ~string](_ S) bool { return true } + +// This check ensures that the underlying type of the PathFingerprint +// is string. This is required to create a copy of the buffer. If it were +// to be moved to a []byte, we need to change the code to create +// a proper copy. +var _ = checkUnderlyingType(snet.PathFingerprint("")) + +type hashState struct { + hash hash.Hash + buf []byte +} + +func newHashState() hashState { + return hashState{hash: sha256.New(), buf: make([]byte, 0, sha256.Size)} } diff --git a/private/path/combinator/combinator_test.go b/private/path/combinator/combinator_test.go index f65e629986..f9aced5828 100644 --- a/private/path/combinator/combinator_test.go +++ b/private/path/combinator/combinator_test.go @@ -598,6 +598,7 @@ func TestFilterDuplicates(t *testing.T) { Interfaces: interfaces, Expiry: expiry, }, + Fingerprint: combinator.Fingerprint(interfaces, combinator.NewHashState()), } } diff --git a/private/path/combinator/export_test.go b/private/path/combinator/export_test.go index 256b10af8c..4d374768e0 100644 --- a/private/path/combinator/export_test.go +++ b/private/path/combinator/export_test.go @@ -16,4 +16,6 @@ package combinator var ( FilterDuplicates = filterDuplicates + Fingerprint = fingerprint + NewHashState = newHashState ) diff --git a/private/path/combinator/graph.go b/private/path/combinator/graph.go index 2531c77b78..090e11ad17 100644 --- a/private/path/combinator/graph.go +++ b/private/path/combinator/graph.go @@ -16,10 +16,11 @@ package combinator import ( "bytes" + "cmp" "encoding/binary" "fmt" "math" - "sort" + "slices" "time" "github.com/scionproto/scion/pkg/addr" @@ -193,9 +194,9 @@ func (g *dmg) AddEdge(src, dst vertex, segment *inputSegment, e *edge) { } // GetPaths returns all the paths from src to dst, sorted according to weight. -func (g *dmg) GetPaths(src, dst vertex) pathSolutionList { - var solutions pathSolutionList - queue := pathSolutionList{&pathSolution{currentVertex: src}} +func (g *dmg) GetPaths(src, dst vertex) []*pathSolution { + var solutions []*pathSolution + queue := []*pathSolution{{currentVertex: src}} for len(queue) > 0 { currentPathSolution := queue[0] queue = queue[1:] @@ -234,7 +235,29 @@ func (g *dmg) GetPaths(src, dst vertex) pathSolutionList { } } } - sort.Sort(solutions) + slices.SortFunc(solutions, func(a, b *pathSolution) int { + d := cmp.Or( + cmp.Compare(a.cost, b.cost), + cmp.Compare(len(a.edges), len(b.edges)), + ) + if d != 0 { + return d + } + trailA, trailB := a.edges, b.edges + for ka := range trailA { + idA := trailA[ka].segment.ID() + idB := trailB[ka].segment.ID() + d := cmp.Or( + bytes.Compare(idA, idB), + cmp.Compare(trailA[ka].edge.Shortcut, trailB[ka].edge.Shortcut), + cmp.Compare(trailA[ka].edge.Peer, trailB[ka].edge.Peer), + ) + if d != 0 { + return d + } + } + return 0 + }) return solutions } @@ -247,6 +270,7 @@ func (g *dmg) GetPaths(src, dst vertex) pathSolutionList { type inputSegment struct { *seg.PathSegment Type proto.PathSegType + id []byte } // IsDownSeg returns true if the segment is a DownSegment. @@ -254,6 +278,13 @@ func (s *inputSegment) IsDownSeg() bool { return s.Type == proto.PathSegType_down } +func (s *inputSegment) ID() []byte { + if s.id == nil { + s.id = s.PathSegment.ID() + } + return s.id +} + // Vertex is a union-like type for the AS vertices and Peering link vertices in // a DMG that can be used as key in maps. type vertex struct { @@ -310,7 +341,7 @@ type pathSolution struct { // Path builds the forwarding path with metadata by extracting it from a path // between source and destination in the DMG. -func (solution *pathSolution) Path() Path { +func (solution *pathSolution) Path(hashState hashState) Path { mtu := ^uint16(0) var segments segmentList var epicPathAuths [][]byte @@ -427,7 +458,8 @@ func (solution *pathSolution) Path() Path { InternalHops: staticInfo.InternalHops, Notes: staticInfo.Notes, }, - Weight: solution.cost, + Weight: solution.cost, + Fingerprint: fingerprint(interfaces, hashState), } if authPHVF, authLHVF, ok := isEpicAvailable(epicPathAuths); ok { @@ -531,50 +563,6 @@ func calculateBeta(se *solutionEdge) uint16 { return beta } -// PathSolutionList is a sort.Interface implementation for a slice of solutions. -type pathSolutionList []*pathSolution - -func (sl pathSolutionList) Len() int { - return len(sl) -} - -// Less sorts according to the following priority list: -// - total path cost (number of hops) -// - number of segments -// - segmentIDs -// - shortcut index -// - peer entry index -func (sl pathSolutionList) Less(i, j int) bool { - if sl[i].cost != sl[j].cost { - return sl[i].cost < sl[j].cost - } - - trailI, trailJ := sl[i].edges, sl[j].edges - if len(trailI) != len(trailJ) { - return len(trailI) < len(trailJ) - } - - for ki := range trailI { - idI := trailI[ki].segment.ID() - idJ := trailJ[ki].segment.ID() - idcmp := bytes.Compare(idI, idJ) - if idcmp != 0 { - return idcmp == -1 - } - if trailI[ki].edge.Shortcut != trailJ[ki].edge.Shortcut { - return trailI[ki].edge.Shortcut < trailJ[ki].edge.Shortcut - } - if trailI[ki].edge.Peer != trailJ[ki].edge.Peer { - return trailI[ki].edge.Peer < trailJ[ki].edge.Peer - } - } - return false -} - -func (sl pathSolutionList) Swap(i, j int) { - sl[i], sl[j] = sl[j], sl[i] -} - // solutionEdge contains a graph edge and additional metadata required during // graph exploration. type solutionEdge struct { diff --git a/private/revcache/util.go b/private/revcache/util.go index 36f1b115c1..151b451d2a 100644 --- a/private/revcache/util.go +++ b/private/revcache/util.go @@ -44,9 +44,6 @@ func NoRevokedHopIntf(ctx context.Context, revCache RevCache, if err != nil || rev != nil { return false, err } - if rev != nil { - return false, nil - } } } return true, nil