diff --git a/doc/command/scion-pki/scion-pki_trc_extract_certificates.rst b/doc/command/scion-pki/scion-pki_trc_extract_certificates.rst index 082f51bc29..cf1dbed532 100644 --- a/doc/command/scion-pki/scion-pki_trc_extract_certificates.rst +++ b/doc/command/scion-pki/scion-pki_trc_extract_certificates.rst @@ -29,8 +29,10 @@ Options :: - -h, --help help for certificates - -o, --out string Output file (required) + -h, --help help for certificates + -o, --out string Output file (optional) + --subject.isd-as strings Filter certificates by ISD-AS of the subject (e.g., 1-ff00:0:110) + --type strings Filter certificates by type (any|cp-as|cp-ca|cp-root|regular-voting|sensitive-voting) SEE ALSO ~~~~~~~~ diff --git a/private/app/helper.go b/private/app/helper.go index afd92bf9c1..cbd834dac5 100644 --- a/private/app/helper.go +++ b/private/app/helper.go @@ -16,6 +16,7 @@ package app import ( "context" + "io" "os" "os/signal" "syscall" @@ -106,3 +107,12 @@ func (c *Cleanup) Do() error { } return errs.ToError() } + +// ReadFileOrStdin reads the content of a file or stdin if the path is "-". +// It returns the content as a byte slice. +func ReadFileOrStdin(path string) ([]byte, error) { + if path == "-" { + return io.ReadAll(os.Stdin) + } + return os.ReadFile(path) +} diff --git a/scion-pki/certs/fingerprint.go b/scion-pki/certs/fingerprint.go index 7205d8c6e8..6ee49fe617 100644 --- a/scion-pki/certs/fingerprint.go +++ b/scion-pki/certs/fingerprint.go @@ -16,16 +16,29 @@ package certs import ( "crypto/sha256" + "crypto/x509" "fmt" "github.com/spf13/cobra" "github.com/scionproto/scion/pkg/private/serrors" "github.com/scionproto/scion/pkg/scrypto/cppki" + "github.com/scionproto/scion/private/app" "github.com/scionproto/scion/private/app/command" "github.com/scionproto/scion/scion-pki/encoding" ) +func ReadPEMCerts(file string) ([]*x509.Certificate, error) { + raw, err := app.ReadFileOrStdin(file) + if err != nil { + return nil, err + } + if len(raw) == 0 { + return nil, fmt.Errorf("file %q is empty", file) + } + return cppki.ParsePEMCerts(raw) +} + func newFingerprintCmd(pather command.Pather) *cobra.Command { var flags struct { format string @@ -53,7 +66,7 @@ If the flag \--format is set to "emoji", the format of the output is a string of } cmd.SilenceUsage = true - chain, err := cppki.ReadPEMCerts(args[0]) + chain, err := ReadPEMCerts(args[0]) if err != nil { return serrors.Wrap("loading certificate chain", err) } diff --git a/scion-pki/certs/inspect.go b/scion-pki/certs/inspect.go index c71ae83ff5..b5478577a4 100644 --- a/scion-pki/certs/inspect.go +++ b/scion-pki/certs/inspect.go @@ -19,12 +19,12 @@ import ( "encoding/pem" "fmt" "io" - "os" "github.com/spf13/cobra" "github.com/scionproto/scion/pkg/private/serrors" "github.com/scionproto/scion/pkg/scrypto/cppki" + "github.com/scionproto/scion/private/app" "github.com/scionproto/scion/private/app/command" ) @@ -46,7 +46,7 @@ request (CSR) in human readable format.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - raw, err := os.ReadFile(args[0]) + raw, err := app.ReadFileOrStdin(args[0]) if err != nil { return serrors.Wrap("loading file", err) } diff --git a/scion-pki/certs/sign.go b/scion-pki/certs/sign.go index 1848f8f064..6db972430c 100644 --- a/scion-pki/certs/sign.go +++ b/scion-pki/certs/sign.go @@ -25,6 +25,7 @@ import ( "github.com/scionproto/scion/pkg/private/serrors" "github.com/scionproto/scion/pkg/scrypto/cppki" + "github.com/scionproto/scion/private/app" "github.com/scionproto/scion/private/app/command" "github.com/scionproto/scion/private/app/flag" scionpki "github.com/scionproto/scion/scion-pki" @@ -96,7 +97,7 @@ and not to \--not-before. cmd.SilenceUsage = true - csrRaw, err := os.ReadFile(args[0]) + csrRaw, err := app.ReadFileOrStdin(args[0]) if err != nil { return serrors.Wrap("loading CSR", err) } diff --git a/scion-pki/certs/validate.go b/scion-pki/certs/validate.go index d17f29567f..8059f8a42d 100644 --- a/scion-pki/certs/validate.go +++ b/scion-pki/certs/validate.go @@ -65,7 +65,7 @@ period. This can be enabled by specifying the \--check-time flag. cmd.SilenceUsage = true filename := args[0] - certs, err := cppki.ReadPEMCerts(filename) + certs, err := ReadPEMCerts(filename) if err != nil { return err } diff --git a/scion-pki/certs/verify.go b/scion-pki/certs/verify.go index 52d8a225d7..207336694e 100644 --- a/scion-pki/certs/verify.go +++ b/scion-pki/certs/verify.go @@ -61,7 +61,7 @@ the expected ISD-AS value. Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - chain, err := cppki.ReadPEMCerts(args[0]) + chain, err := ReadPEMCerts(args[0]) if err != nil { return serrors.Wrap("reading chain", err, "file", args[0]) } @@ -148,7 +148,7 @@ The CA certificate must be a PEM encoded. Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - certs, err := cppki.ReadPEMCerts(args[0]) + certs, err := ReadPEMCerts(args[0]) if err != nil { return serrors.Wrap("reading certificate", err, "file", args[0]) } diff --git a/scion-pki/key/BUILD.bazel b/scion-pki/key/BUILD.bazel index b41c63e959..a1b46ce25a 100644 --- a/scion-pki/key/BUILD.bazel +++ b/scion-pki/key/BUILD.bazel @@ -18,6 +18,7 @@ go_library( "//pkg/private/serrors:go_default_library", "//pkg/scrypto:go_default_library", "//pkg/scrypto/cppki:go_default_library", + "//private/app:go_default_library", "//private/app/command:go_default_library", "//scion-pki:go_default_library", "//scion-pki/encoding:go_default_library", diff --git a/scion-pki/key/fingerprint.go b/scion-pki/key/fingerprint.go index 88d242e45b..c967f6b6a7 100644 --- a/scion-pki/key/fingerprint.go +++ b/scion-pki/key/fingerprint.go @@ -20,12 +20,12 @@ import ( "crypto/x509" "encoding/pem" "fmt" - "os" "github.com/spf13/cobra" "github.com/scionproto/scion/pkg/private/serrors" "github.com/scionproto/scion/pkg/scrypto/cppki" + "github.com/scionproto/scion/private/app" "github.com/scionproto/scion/private/app/command" "github.com/scionproto/scion/scion-pki/encoding" ) @@ -102,7 +102,7 @@ The subject key ID is written to standard out. // loadPublicKey loads the public key from file and distinguishes what type of key it is. func loadPublicKey(filename string) (crypto.PublicKey, error) { - raw, err := os.ReadFile(filename) + raw, err := app.ReadFileOrStdin(filename) if err != nil { return nil, serrors.Wrap("reading input file", err) } diff --git a/scion-pki/key/public.go b/scion-pki/key/public.go index 55e088082d..8edb18675f 100644 --- a/scion-pki/key/public.go +++ b/scion-pki/key/public.go @@ -19,12 +19,12 @@ import ( "crypto/x509" "encoding/pem" "fmt" - "os" "path/filepath" "github.com/spf13/cobra" "github.com/scionproto/scion/pkg/private/serrors" + "github.com/scionproto/scion/private/app" "github.com/scionproto/scion/private/app/command" scionpki "github.com/scionproto/scion/scion-pki" "github.com/scionproto/scion/scion-pki/file" @@ -98,7 +98,7 @@ By default, the public key is written to standard out. // LoadPrivate key loads a private key from file. func LoadPrivateKey(kms, name string) (crypto.Signer, error) { if kms == "" { - raw, err := os.ReadFile(name) + raw, err := app.ReadFileOrStdin(name) if err != nil { return nil, serrors.Wrap("reading private key", err) } diff --git a/scion-pki/trcs/BUILD.bazel b/scion-pki/trcs/BUILD.bazel index 55bb8a94bc..b0a95a53c4 100644 --- a/scion-pki/trcs/BUILD.bazel +++ b/scion-pki/trcs/BUILD.bazel @@ -25,6 +25,7 @@ go_library( "//pkg/scrypto/cms/oid:go_default_library", "//pkg/scrypto/cms/protocol:go_default_library", "//pkg/scrypto/cppki:go_default_library", + "//private/app:go_default_library", "//private/app/command:go_default_library", "//scion-pki:go_default_library", "//scion-pki/conf:go_default_library", diff --git a/scion-pki/trcs/decode.go b/scion-pki/trcs/decode.go index 168a58c121..dc3a3573f4 100644 --- a/scion-pki/trcs/decode.go +++ b/scion-pki/trcs/decode.go @@ -16,14 +16,15 @@ package trcs import ( "encoding/pem" - "os" "github.com/scionproto/scion/pkg/scrypto/cppki" + "github.com/scionproto/scion/private/app" ) // DecodeFromFile decodes a signed TRC from the provided file. +// In case the name is "-", we read from stdin. func DecodeFromFile(name string) (cppki.SignedTRC, error) { - raw, err := os.ReadFile(name) + raw, err := app.ReadFileOrStdin(name) if err != nil { return cppki.SignedTRC{}, err } diff --git a/scion-pki/trcs/extract.go b/scion-pki/trcs/extract.go index 18bf03f4f6..698135c895 100644 --- a/scion-pki/trcs/extract.go +++ b/scion-pki/trcs/extract.go @@ -19,10 +19,14 @@ import ( "encoding/pem" "fmt" "os" + "slices" + "strings" "github.com/spf13/cobra" + "github.com/scionproto/scion/pkg/addr" "github.com/scionproto/scion/pkg/private/serrors" + "github.com/scionproto/scion/pkg/scrypto/cppki" "github.com/scionproto/scion/private/app/command" ) @@ -71,7 +75,7 @@ To inspect the created asn.1 file you can use the openssl tool:: Bytes: raw, }) } - if err := os.WriteFile(flags.out, raw, 0644); err != nil { + if err := os.WriteFile(flags.out, raw, 0o644); err != nil { return serrors.Wrap("failed to write extracted payload", err) } fmt.Printf("Successfully extracted payload at %s\n", flags.out) @@ -86,51 +90,138 @@ To inspect the created asn.1 file you can use the openssl tool:: func newExtractCertificates(pather command.Pather) *cobra.Command { var flags struct { - out string + out string + ias []string + types []string } cmd := &cobra.Command{ Use: "certificates", - Aliases: []string{"certs"}, + Aliases: []string{"certs", "certificate", "cert"}, Short: "Extract the bundled certificates", Example: fmt.Sprintf(` %[1]s certificates -o bundle.pem input.trc`, pather.CommandPath()), Long: `'certificates' extracts the certificates into a bundled PEM file.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - if err := runExtractCertificates(args[0], flags.out); err != nil { + types := make(map[cppki.CertType]bool) + for _, t := range flags.types { + if t == "any" { + types = nil // No filter, all types are included. + break + } + typ, ok := certTypes[t] + if !ok { + return fmt.Errorf("unknown certificate type %q, valid types are: %s", + t, strings.Join(getTypes(), ", ")) + } + types[typ] = true + } + + ias := make(map[addr.IA]bool) + for _, v := range flags.ias { + ia, err := addr.ParseIA(v) + if err != nil { + return fmt.Errorf("invalid ISD-AS %q: %w", v, err) + } + ias[ia] = true + } + + cmd.SilenceUsage = true + + if err := runExtractCertificates(args[0], flags.out, types, ias); err != nil { return err } - fmt.Printf("Successfully extracted certificates at %s\n", flags.out) + if flags.out != "" && flags.out != "-" { + fmt.Fprintf(cmd.ErrOrStderr(), + "Successfully extracted certificates at %s\n", flags.out) + } return nil }, } - addOutputFlag(&flags.out, cmd) + addOptionalOutputFlag(&flags.out, cmd) + + cmd.Flags().StringSliceVar(&flags.ias, "subject.isd-as", nil, + "Filter certificates by ISD-AS of the subject (e.g., 1-ff00:0:110)") + cmd.Flags().StringSliceVar(&flags.types, "type", nil, + "Filter certificates by type ("+strings.Join(getTypes(), "|")+")") return cmd } -func runExtractCertificates(in, out string) error { +func runExtractCertificates( + in, out string, types map[cppki.CertType]bool, ias map[addr.IA]bool, +) error { signed, err := DecodeFromFile(in) if err != nil { return serrors.Wrap("failed to load signed TRC", err) } - return writeBundle(out, signed.TRC.Certificates) + certs := make([]*x509.Certificate, 0, len(signed.TRC.Certificates)) + + // Filter the certificates based on the user input. + for _, cert := range signed.TRC.Certificates { + // Check certificate type + { + typ, err := cppki.ValidateCert(cert) + if err != nil { + return fmt.Errorf("invalid certificate %s: %w", cert.Subject.CommonName, err) + } + if len(types) > 0 && !types[typ] { + continue + } + } + + // Check certificate ISD-AS + { + ia, err := cppki.ExtractIA(cert.Subject) + if err != nil { + return fmt.Errorf("failed to extract ISD-AS from certificate %s: %w", + cert.Subject.CommonName, err) + } + if len(ias) > 0 && !ias[ia] { + continue + } + } + certs = append(certs, cert) + } + + return writeBundle(out, certs) } func writeBundle(out string, certs []*x509.Certificate) error { - file, err := os.Create(out) - if err != nil { - return serrors.Wrap("unable to create file", err) + o := os.Stdout + if out != "" && out != "-" { + var err error + if o, err = os.Create(out); err != nil { + return serrors.Wrap("unable to create file", err) + } + defer o.Close() } - defer file.Close() for i, cert := range certs { block := pem.Block{ Type: "CERTIFICATE", Bytes: cert.Raw, } - if err := pem.Encode(file, &block); err != nil { + if err := pem.Encode(o, &block); err != nil { return serrors.Wrap("unable to encode certificate", err, "index", i) } } return nil } + +var certTypes = map[string]cppki.CertType{ + cppki.Root.String(): cppki.Root, + cppki.CA.String(): cppki.CA, + cppki.AS.String(): cppki.AS, + cppki.Sensitive.String(): cppki.Sensitive, + cppki.Regular.String(): cppki.Regular, +} + +func getTypes() []string { + options := make([]string, 0, len(certTypes)+1) + for k := range certTypes { + options = append(options, k) + } + options = append(options, "any") + slices.Sort(options) + return options +} diff --git a/scion-pki/trcs/format.go b/scion-pki/trcs/format.go index 2e42ffba42..7355a9d9df 100644 --- a/scion-pki/trcs/format.go +++ b/scion-pki/trcs/format.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/cobra" "github.com/scionproto/scion/pkg/private/serrors" + "github.com/scionproto/scion/private/app" "github.com/scionproto/scion/private/app/command" "github.com/scionproto/scion/scion-pki/file" ) @@ -64,7 +65,7 @@ redirected to a file because the raw characters might mess up the terminal. cmd.SilenceUsage = true filename := args[0] - raw, err := os.ReadFile(filename) + raw, err := app.ReadFileOrStdin(filename) if err != nil { return serrors.Wrap("reading file", err) } diff --git a/scion-pki/trcs/inspect.go b/scion-pki/trcs/inspect.go index 7df9b6627c..07e2ebca4e 100644 --- a/scion-pki/trcs/inspect.go +++ b/scion-pki/trcs/inspect.go @@ -32,6 +32,7 @@ import ( "github.com/scionproto/scion/pkg/private/serrors" "github.com/scionproto/scion/pkg/scrypto/cms/protocol" "github.com/scionproto/scion/pkg/scrypto/cppki" + "github.com/scionproto/scion/private/app" "github.com/scionproto/scion/private/app/command" ) @@ -70,7 +71,7 @@ return an error if parts of a TRC fail to decode, enable the strict mode. } cmd.SilenceUsage = true - raw, err := os.ReadFile(args[0]) + raw, err := app.ReadFileOrStdin(args[0]) if err != nil { return err } diff --git a/scion-pki/trcs/trcs.go b/scion-pki/trcs/trcs.go index ca24f45401..046a950e1d 100644 --- a/scion-pki/trcs/trcs.go +++ b/scion-pki/trcs/trcs.go @@ -43,3 +43,7 @@ func addOutputFlag(flag *string, cmd *cobra.Command) { cmd.Flags().StringVarP(flag, "out", "o", "", "Output file (required)") cmd.MarkFlagRequired("out") } + +func addOptionalOutputFlag(flag *string, cmd *cobra.Command) { + cmd.Flags().StringVarP(flag, "out", "o", "", "Output file (optional)") +} diff --git a/scion-pki/trcs/verify_test.go b/scion-pki/trcs/verify_test.go index d76d8b78a3..d80466a0b5 100644 --- a/scion-pki/trcs/verify_test.go +++ b/scion-pki/trcs/verify_test.go @@ -49,7 +49,8 @@ func TestVerify(t *testing.T) { ISD: 0, Prepare: func(*testing.T) { out := filepath.Join(dir, "base.pem") - require.NoError(t, runExtractCertificates("./testdata/admin/ISD1-B1-S1.trc", out)) + require.NoError(t, runExtractCertificates("./testdata/admin/ISD1-B1-S1.trc", + out, nil, nil)) }, ErrAssertion: require.NoError, }, @@ -78,7 +79,7 @@ func TestVerify(t *testing.T) { sig[len(sig)-1] ^= 0xFF raw, err := signed.Encode() require.NoError(t, err) - require.NoError(t, os.WriteFile(out, raw, 0666)) + require.NoError(t, os.WriteFile(out, raw, 0o666)) }, ErrAssertion: require.Error, },