Repository: scionproto/scion
Pull request #3672 (2020-02-19 11:36:43)

SPKI: Add verify commands
This PR adds verify commands to the crts and trcs command.

<!-- 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/3672)
<!-- Reviewable:end -->



Patch:
diff --git a/go/tools/scion-pki/internal/certs/BUILD.bazel b/go/tools/scion-pki/internal/certs/BUILD.bazel
index c4f1b8f52..bb0b81320 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 863079b2b..9daf2ed28 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 e887cc60e..8cdbd18c9 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/trcs/BUILD.bazel b/go/tools/scion-pki/internal/trcs/BUILD.bazel
index 08545842f..8bc1f7cc3 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 cc51d6c05..5f5ecaf63 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 1f43f50d6..ec1ca3ced 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 0f88d6b67..292a7723d 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 b911d8261..c8ca5a842 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 3779d6ca2..b00448a7b 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)


Test patch:
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 000000000..10ffb6188
--- /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/validator_test.go b/go/tools/scion-pki/internal/trcs/validator_test.go
new file mode 100644
index 000000000..d6260b78e
--- /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)
+		})
+	}
+}