From 9a446fe8ee281e1df89e06ee6e285378145526b4 Mon Sep 17 00:00:00 2001 From: Oncilla Date: Wed, 19 Feb 2020 14:30:23 +0100 Subject: [PATCH] SPKI: Add verify commands (#3672) This PR adds verify commands to the crts and trcs command. --- go/tools/scion-pki/internal/certs/BUILD.bazel | 2 + go/tools/scion-pki/internal/certs/cmd.go | 20 ++++ go/tools/scion-pki/internal/certs/verify.go | 46 +++++++++ .../scion-pki/internal/certs/verify_test.go | 94 +++++++++++++++++++ go/tools/scion-pki/internal/trcs/BUILD.bazel | 3 +- go/tools/scion-pki/internal/trcs/cmd.go | 20 +++- go/tools/scion-pki/internal/trcs/prototype.go | 4 +- go/tools/scion-pki/internal/trcs/sign.go | 4 +- go/tools/scion-pki/internal/trcs/util.go | 22 +++-- go/tools/scion-pki/internal/trcs/validator.go | 30 +++++- .../scion-pki/internal/trcs/validator_test.go | 76 +++++++++++++++ 11 files changed, 301 insertions(+), 20 deletions(-) create mode 100644 go/tools/scion-pki/internal/certs/verify_test.go create mode 100644 go/tools/scion-pki/internal/trcs/validator_test.go diff --git a/go/tools/scion-pki/internal/certs/BUILD.bazel b/go/tools/scion-pki/internal/certs/BUILD.bazel index c4f1b8f527..bb0b813203 100644 --- a/go/tools/scion-pki/internal/certs/BUILD.bazel +++ b/go/tools/scion-pki/internal/certs/BUILD.bazel @@ -36,11 +36,13 @@ go_test( "issuer_test.go", "loader_test.go", "main_test.go", + "verify_test.go", ], data = glob(["testdata/**"]), embed = [":go_default_library"], deps = [ "//go/lib/scrypto:go_default_library", + "//go/lib/scrypto/cert:go_default_library", "//go/lib/xtest:go_default_library", "//go/tools/scion-pki/internal/pkicmn:go_default_library", "@com_github_stretchr_testify//assert:go_default_library", diff --git a/go/tools/scion-pki/internal/certs/cmd.go b/go/tools/scion-pki/internal/certs/cmd.go index 863079b2be..9daf2ed284 100644 --- a/go/tools/scion-pki/internal/certs/cmd.go +++ b/go/tools/scion-pki/internal/certs/cmd.go @@ -112,10 +112,30 @@ and displays them in a human readable format. }, } +var verifyCmd = &cobra.Command{ + Use: "verify", + Short: "Verify all provided issuer certificates and certificate chains", + Example: ` scion-pki certs verify ISD1/ASff00_0_110/certs/ISD1-ASff00_0_110.crt + scion-pki certs verify ISD1/ASff00_0_110/certs/ISD1-ASff00_0_110.issuer + scion-pki certs verify ISD1/ASff00_0_110/certs/*`, + Long: ` + 'verify' validates and verifies all the issuer certificates and certificate chains. + The TRC that is used to authenticate the issuer certificate must be present. + Whether the file is an issuer certificate or a certificate chain is decided + based on the file ending. +`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + v := verifier{Dirs: pkicmn.GetDirs()} + return v.Run(args) + }, +} + func init() { Cmd.PersistentFlags().Uint64Var(&version, "version", 0, "certificate version (0 indicates newest)") Cmd.AddCommand(genChainCmd) Cmd.AddCommand(genIssuerCmd) Cmd.AddCommand(humanCmd) + Cmd.AddCommand(verifyCmd) } diff --git a/go/tools/scion-pki/internal/certs/verify.go b/go/tools/scion-pki/internal/certs/verify.go index e887cc60e2..8cdbd18c93 100644 --- a/go/tools/scion-pki/internal/certs/verify.go +++ b/go/tools/scion-pki/internal/certs/verify.go @@ -30,6 +30,52 @@ type verifier struct { Dirs pkicmn.Dirs } +// Run validates and verifies all issuer certificates and certificate chains in +// the file list. +func (v verifier) Run(files []string) error { + var errs serrors.List + issuers, chains := MatchFiles(files) + if err := v.verifyIssuers(issuers); err != nil { + errs = append(errs, err) + } + if err := v.verifyChains(chains); err != nil { + errs = append(errs, err) + } + return errs.ToError() +} + +func (v verifier) verifyIssuers(files []string) error { + var errs serrors.List + for _, file := range files { + raw, err := ioutil.ReadFile(file) + if err != nil { + errs = append(errs, serrors.WrapStr("unable to load issuer certificate", err, + "file", file)) + continue + } + if err := v.VerifyIssuer(raw); err != nil { + errs = append(errs, serrors.WithCtx(err, "file", file)) + } + } + return errs.ToError() +} + +func (v verifier) verifyChains(files []string) error { + var errs serrors.List + for _, file := range files { + raw, err := ioutil.ReadFile(file) + if err != nil { + errs = append(errs, serrors.WrapStr("unable to load certificate chain", err, + "file", file)) + continue + } + if err := v.VerifyChain(raw); err != nil { + errs = append(errs, serrors.WithCtx(err, "file", file)) + } + } + return errs.ToError() +} + // VerifyIssuer validates and verifies a raw signed issuer certificate. For // verification, the issuing TRC is loaded from the file system. func (v verifier) VerifyIssuer(raw []byte) error { diff --git a/go/tools/scion-pki/internal/certs/verify_test.go b/go/tools/scion-pki/internal/certs/verify_test.go new file mode 100644 index 0000000000..10ffb6188a --- /dev/null +++ b/go/tools/scion-pki/internal/certs/verify_test.go @@ -0,0 +1,94 @@ +// Copyright 2019 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 certs + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/scionproto/scion/go/lib/scrypto/cert" + "github.com/scionproto/scion/go/lib/xtest" + "github.com/scionproto/scion/go/tools/scion-pki/internal/pkicmn" +) + +func TestVerifierRun(t *testing.T) { + tmpDir, cleanF := xtest.MustTempDir("", "test-certs-verifier") + defer cleanF() + isdDir := filepath.Join(tmpDir, "ISD1") + err := exec.Command("cp", "-r", "./testdata/ISD1", isdDir).Run() + require.NoError(t, err) + + forged := filepath.Join(tmpDir, "forged") + err = os.MkdirAll(Dir(forged, ia110), 0777) + require.NoError(t, err) + + raw, err := ioutil.ReadFile(IssuerFile("./testdata", ia110, 1)) + require.NoError(t, err) + iss, err := cert.ParseSignedIssuer(raw) + require.NoError(t, err) + iss.Signature[0] ^= 0xFF + raw, err = cert.EncodeSignedIssuer(iss) + require.NoError(t, err) + forgedIss := IssuerFile(forged, ia110, 1) + err = ioutil.WriteFile(forgedIss, raw, 0644) + require.NoError(t, err) + + raw, err = ioutil.ReadFile(ASFile("./testdata", ia111, 1)) + require.NoError(t, err) + chain, err := cert.ParseChain(raw) + require.NoError(t, err) + chain.AS.Signature[0] ^= 0xFF + raw, err = chain.MarshalJSON() + require.NoError(t, err) + forgedAS := ASFile(forged, ia110, 1) + err = ioutil.WriteFile(forgedAS, raw, 0644) + require.NoError(t, err) + + tests := map[string]struct { + Files []string + Assertion assert.ErrorAssertionFunc + }{ + "issuer": { + Files: []string{IssuerFile(tmpDir, ia110, 1)}, + Assertion: assert.NoError, + }, + "chain": { + Files: []string{ASFile(tmpDir, ia111, 1)}, + Assertion: assert.NoError, + }, + "all": { + Files: []string{IssuerFile(tmpDir, ia110, 1), ASFile(tmpDir, ia111, 1)}, + Assertion: assert.NoError, + }, + "forged": { + Files: []string{IssuerFile("fake", ia110, 1), ASFile("fake", ia111, 1), + forgedAS, forgedIss}, + Assertion: assert.Error, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + v := verifier{Dirs: pkicmn.Dirs{Root: "./testdata", Out: tmpDir}} + err := v.Run(test.Files) + test.Assertion(t, err) + }) + } +} diff --git a/go/tools/scion-pki/internal/trcs/BUILD.bazel b/go/tools/scion-pki/internal/trcs/BUILD.bazel index 08545842f7..8bc1f7cc37 100644 --- a/go/tools/scion-pki/internal/trcs/BUILD.bazel +++ b/go/tools/scion-pki/internal/trcs/BUILD.bazel @@ -18,7 +18,6 @@ go_library( visibility = ["//go/tools/scion-pki:__subpackages__"], deps = [ "//go/lib/addr:go_default_library", - "//go/lib/common:go_default_library", "//go/lib/keyconf:go_default_library", "//go/lib/scrypto:go_default_library", "//go/lib/scrypto/trc:go_default_library", @@ -38,12 +37,14 @@ go_test( "loader_test.go", "prototype_test.go", "sign_test.go", + "validator_test.go", ], data = glob(["testdata/**"]), embed = [":go_default_library"], deps = [ "//go/lib/addr:go_default_library", "//go/lib/scrypto:go_default_library", + "//go/lib/scrypto/trc:go_default_library", "//go/lib/xtest:go_default_library", "//go/tools/scion-pki/internal/pkicmn:go_default_library", "@com_github_stretchr_testify//assert:go_default_library", diff --git a/go/tools/scion-pki/internal/trcs/cmd.go b/go/tools/scion-pki/internal/trcs/cmd.go index cc51d6c05b..5f5ecaf631 100644 --- a/go/tools/scion-pki/internal/trcs/cmd.go +++ b/go/tools/scion-pki/internal/trcs/cmd.go @@ -18,7 +18,6 @@ package trcs import ( "github.com/spf13/cobra" - "github.com/scionproto/scion/go/lib/common" "github.com/scionproto/scion/go/lib/scrypto" "github.com/scionproto/scion/go/lib/serrors" "github.com/scionproto/scion/go/tools/scion-pki/internal/pkicmn" @@ -182,6 +181,22 @@ See 'scion-pki help trcs' for information on the selector. }, } +var verify = &cobra.Command{ + Use: "verify", + Short: "Verify all provided TRCs", + Example: ` scion-pki trcs verify ISD1/trcs/ISD1-V1.trc + scion-pki trcs verify ISD1/trcs/*`, + Long: ` + 'verify' validates and verifies all the provided TRCs. In order to verify + non-base TRCs, the predecessor TRC must be available. +`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + v := validator{Dirs: pkicmn.GetDirs()} + return v.Run(args) + }, +} + var human = &cobra.Command{ Use: "human", Short: "Display human readable", @@ -192,7 +207,7 @@ var human = &cobra.Command{ Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if err := runHuman(args); err != nil { - return common.NewBasicError("unable to display human TRCs", err) + return serrors.WrapStr("unable to display human TRCs", err) } return nil }, @@ -204,5 +219,6 @@ func init() { Cmd.AddCommand(proto) Cmd.AddCommand(sign) Cmd.AddCommand(combine) + Cmd.AddCommand(verify) Cmd.AddCommand(human) } diff --git a/go/tools/scion-pki/internal/trcs/prototype.go b/go/tools/scion-pki/internal/trcs/prototype.go index 1f43f50d6d..ec1ca3ced8 100644 --- a/go/tools/scion-pki/internal/trcs/prototype.go +++ b/go/tools/scion-pki/internal/trcs/prototype.go @@ -206,9 +206,11 @@ func (g protoGen) newTRC(isd addr.ISD, cfg conf.TRC, keys map[addr.AS]pubKeys) ( if !t.Base() { var err error file := SignedFile(g.Dirs.Out, isd, t.Version-1) - if prev, _, err = loadTRC(file); err != nil { + dec, err := loadTRC(file) + if err != nil { return nil, serrors.WrapStr("unable to load previous TRC", err, "file", file) } + prev = dec.TRC } if err := g.attachVotes(t, prev, cfg.Votes); err != nil { return nil, serrors.WrapStr("unable to attach votes", err) diff --git a/go/tools/scion-pki/internal/trcs/sign.go b/go/tools/scion-pki/internal/trcs/sign.go index 0f88d6b67a..292a7723d0 100644 --- a/go/tools/scion-pki/internal/trcs/sign.go +++ b/go/tools/scion-pki/internal/trcs/sign.go @@ -125,13 +125,13 @@ func (g signatureGen) castVote(signatures map[trc.Protected]trc.Signature, ia ad if !ok { return nil } - prev, _, err := loadTRC(SignedFile(g.Dirs.Out, t.ISD, t.Version-1)) + dec, err := loadTRC(SignedFile(g.Dirs.Out, t.ISD, t.Version-1)) if err != nil { return err } id := keyconf.ID{ IA: ia, - Version: prev.PrimaryASes[ia.A].Keys[keyType].KeyVersion, + Version: dec.TRC.PrimaryASes[ia.A].Keys[keyType].KeyVersion, } id.Usage, err = keys.UsageFromTRCKeyType(keyType) if err != nil { diff --git a/go/tools/scion-pki/internal/trcs/util.go b/go/tools/scion-pki/internal/trcs/util.go index b911d8261b..c8ca5a8427 100644 --- a/go/tools/scion-pki/internal/trcs/util.go +++ b/go/tools/scion-pki/internal/trcs/util.go @@ -80,18 +80,22 @@ func sortSignatures(signatures map[trc.Protected]trc.Signature) []trc.Signature return sigs } -func loadTRC(file string) (*trc.TRC, trc.Encoded, error) { +type decoded struct { + Signed trc.Signed + TRC *trc.TRC +} + +func loadTRC(file string) (decoded, error) { raw, err := ioutil.ReadFile(file) if err != nil { - return nil, "", err + return decoded{}, err } - signed, err := trc.ParseSigned(raw) - if err != nil { - return nil, "", err + var dec decoded + if dec.Signed, err = trc.ParseSigned(raw); err != nil { + return decoded{}, err } - t, err := signed.EncodedTRC.Decode() - if err != nil { - return nil, "", err + if dec.TRC, err = dec.Signed.EncodedTRC.Decode(); err != nil { + return decoded{}, err } - return t, signed.EncodedTRC, nil + return dec, nil } diff --git a/go/tools/scion-pki/internal/trcs/validator.go b/go/tools/scion-pki/internal/trcs/validator.go index 3779d6ca2b..b00448a7b9 100644 --- a/go/tools/scion-pki/internal/trcs/validator.go +++ b/go/tools/scion-pki/internal/trcs/validator.go @@ -25,16 +25,36 @@ type validator struct { Dirs pkicmn.Dirs } +// Run checks that all TRCs provided in the file list are valid and verifiable. +func (v validator) Run(files []string) error { + var errs serrors.List + trcs := make(map[addr.ISD]signedMeta) + for _, file := range files { + dec, err := loadTRC(file) + if err != nil { + errs = append(errs, serrors.WrapStr("unable to load TRC", err, "file", file)) + continue + } + trcs[dec.TRC.ISD] = signedMeta{Signed: dec.Signed, Version: dec.TRC.Version} + } + if err := v.Validate(trcs); err != nil { + errs = append(errs, err) + } + return errs.ToError() +} + // Validate checks that all TRCs in the map are valid and verifiable. For base // TRCs, the TRC invariants are validated and all proof of possessions are // verified. TRC updates are validated and verified based on the previous TRC. func (v validator) Validate(combined map[addr.ISD]signedMeta) error { + var errs serrors.List for isd, meta := range combined { if err := v.validate(isd, meta); err != nil { - return serrors.WrapStr("TRC cannot be validated/verified", err, "isd", isd) + errs = append(errs, serrors.WrapStr("TRC cannot be validated/verified", err, + "isd", isd, "version", meta.Version)) } } - return nil + return errs.ToError() } func (v validator) validate(isd addr.ISD, meta signedMeta) error { @@ -56,13 +76,13 @@ func (v validator) validate(isd addr.ISD, meta signedMeta) error { if t.Base() { return nil } - prev, _, err := loadTRC(SignedFile(v.Dirs.Out, isd, meta.Version-1)) + dec, err := loadTRC(SignedFile(v.Dirs.Out, isd, meta.Version-1)) if err != nil { return serrors.WrapStr("unable to load previous TRC", err, "version", meta.Version-1) } val := trc.UpdateValidator{ Next: t, - Prev: prev, + Prev: dec.TRC, } if _, err := val.Validate(); err != nil { return serrors.WrapStr("unable to validate TRC update", err) @@ -71,7 +91,7 @@ func (v validator) validate(isd addr.ISD, meta signedMeta) error { Next: t, NextEncoded: meta.Signed.EncodedTRC, Signatures: meta.Signed.Signatures, - Prev: prev, + Prev: dec.TRC, } if err := ver.Verify(); err != nil { return serrors.WrapStr("unable to verify TRC update", err) diff --git a/go/tools/scion-pki/internal/trcs/validator_test.go b/go/tools/scion-pki/internal/trcs/validator_test.go new file mode 100644 index 0000000000..d6260b78e7 --- /dev/null +++ b/go/tools/scion-pki/internal/trcs/validator_test.go @@ -0,0 +1,76 @@ +// Copyright 2019 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 trcs + +import ( + "io/ioutil" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/scionproto/scion/go/lib/scrypto/trc" + "github.com/scionproto/scion/go/lib/xtest" + "github.com/scionproto/scion/go/tools/scion-pki/internal/pkicmn" +) + +func TestValidatorRun(t *testing.T) { + tmpDir, cleanF := xtest.MustTempDir("", "test-trcs-validator") + defer cleanF() + isdDir := filepath.Join(tmpDir, "ISD1") + err := exec.Command("cp", "-r", "./testdata/ISD1", isdDir).Run() + require.NoError(t, err) + + dec, err := loadTRC(SignedFile("./testdata", 1, 3)) + require.NoError(t, err) + dec.Signed.Signatures[0].Signature[0] ^= 0xFF + raw, err := trc.EncodeSigned(dec.Signed) + require.NoError(t, err) + forged := filepath.Join(Dir(tmpDir, 1), "forged.trc") + err = ioutil.WriteFile(forged, raw, 0644) + require.NoError(t, err) + + v1, v2, v3 := SignedFile(tmpDir, 1, 1), SignedFile(tmpDir, 1, 2), SignedFile(tmpDir, 1, 3) + tests := map[string]struct { + Files []string + Assertion assert.ErrorAssertionFunc + }{ + "v1": { + Files: []string{v1}, + Assertion: assert.NoError, + }, + "v2": { + Files: []string{v2}, + Assertion: assert.NoError, + }, + "all": { + Files: []string{v1, v2, v3}, + Assertion: assert.NoError, + }, + "forged": { + Files: []string{"./some/fake/path", forged}, + Assertion: assert.Error, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + v := validator{Dirs: pkicmn.Dirs{Root: "./testdata", Out: tmpDir}} + err := v.Run(test.Files) + test.Assertion(t, err) + }) + } +}