From 45883516ac24058bf970653fee4e9b25f0bdf929 Mon Sep 17 00:00:00 2001 From: Oncilla Date: Mon, 7 Oct 2019 16:52:00 +0200 Subject: [PATCH] SPKI: Generate private keys (#3218) Adds: - support to generate private keys form keys.toml runGenKeys is not replaced currently, because it is in use by the trust/v2 tests. It will be replaced as soon as TRC and certificate signing with the new key format is available. --- go/lib/keyconf/key.go | 2 +- go/lib/keyconf/key_test.go | 2 +- go/tools/scion-pki/internal/pkicmn/pkicmn.go | 14 ++ go/tools/scion-pki/internal/v2/conf/key.go | 14 +- .../scion-pki/internal/v2/conf/validity.go | 15 ++ .../scion-pki/internal/v2/keys/BUILD.bazel | 25 ++- go/tools/scion-pki/internal/v2/keys/priv.go | 209 ++++++++++++++++++ .../scion-pki/internal/v2/keys/priv_test.go | 142 ++++++++++++ go/tools/scion-pki/internal/v2/keys/util.go | 32 +++ go/tools/scion-pki/internal/v2/tmpl/topo.go | 7 +- 10 files changed, 452 insertions(+), 10 deletions(-) create mode 100644 go/tools/scion-pki/internal/v2/keys/priv.go create mode 100644 go/tools/scion-pki/internal/v2/keys/priv_test.go create mode 100644 go/tools/scion-pki/internal/v2/keys/util.go diff --git a/go/lib/keyconf/key.go b/go/lib/keyconf/key.go index 35f9bc7e2b..4530034e91 100644 --- a/go/lib/keyconf/key.go +++ b/go/lib/keyconf/key.go @@ -127,7 +127,7 @@ type Key struct { } // KeyFromPEM parses the PEM block. -func KeyFromPEM(block pem.Block) (Key, error) { +func KeyFromPEM(block *pem.Block) (Key, error) { k := Key{} if err := k.Type.UnmarshalText([]byte(block.Type)); err != nil { return Key{}, serrors.WrapStr("unable to parse key type", err) diff --git a/go/lib/keyconf/key_test.go b/go/lib/keyconf/key_test.go index 30cb9e3195..911e40f4b2 100644 --- a/go/lib/keyconf/key_test.go +++ b/go/lib/keyconf/key_test.go @@ -104,7 +104,7 @@ func TestKeyFromPEM(t *testing.T) { t.Run(name, func(t *testing.T) { block := pemBlock(t) test.Modify(&block) - k, err := keyconf.KeyFromPEM(block) + k, err := keyconf.KeyFromPEM(&block) test.ErrAssertion(t, err) if err != nil { return diff --git a/go/tools/scion-pki/internal/pkicmn/pkicmn.go b/go/tools/scion-pki/internal/pkicmn/pkicmn.go index e7c2330338..07fdaa9b0f 100644 --- a/go/tools/scion-pki/internal/pkicmn/pkicmn.go +++ b/go/tools/scion-pki/internal/pkicmn/pkicmn.go @@ -49,6 +49,20 @@ var ( Quiet bool ) +// Dirs holds the directory configuration. +type Dirs struct { + Root string + Out string +} + +// GetDirs returns the directory configuration. +func GetDirs() Dirs { + return Dirs{ + Root: RootDir, + Out: OutDir, + } +} + // ParseSelector parses the given selector. The returned strings are in file format. func ParseSelector(selector string) (string, string, error) { toks := strings.Split(selector, "-") diff --git a/go/tools/scion-pki/internal/v2/conf/key.go b/go/tools/scion-pki/internal/v2/conf/key.go index a25fd452bd..4a8b1cecfe 100644 --- a/go/tools/scion-pki/internal/v2/conf/key.go +++ b/go/tools/scion-pki/internal/v2/conf/key.go @@ -17,18 +17,26 @@ package conf import ( "encoding" "io" + "path/filepath" "strconv" "github.com/BurntSushi/toml" + "github.com/scionproto/scion/go/lib/addr" "github.com/scionproto/scion/go/lib/scrypto" "github.com/scionproto/scion/go/lib/scrypto/cert/v2" "github.com/scionproto/scion/go/lib/scrypto/trc/v2" "github.com/scionproto/scion/go/lib/serrors" + "github.com/scionproto/scion/go/tools/scion-pki/internal/pkicmn" ) -// KeysFileName is the file name of the key configuration. -const KeysFileName = "keys.toml" +// keysFileName is the file name of the key configuration. +const keysFileName = "keys.toml" + +// KeysFile returns the file where the keys config is written to. +func KeysFile(dir string, ia addr.IA) string { + return filepath.Join(pkicmn.GetAsPath(dir, ia), keysFileName) +} // Keys holds the key configuration. type Keys struct { @@ -95,7 +103,7 @@ func (k Keys) validateKeyMetas(metas map[scrypto.KeyVersion]KeyMeta) error { return nil } -// KeyMeta defines the +// KeyMeta defines the key metadata. type KeyMeta struct { Algorithm string `toml:"algorithm"` Validity Validity `toml:"validity"` diff --git a/go/tools/scion-pki/internal/v2/conf/validity.go b/go/tools/scion-pki/internal/v2/conf/validity.go index 96b61e14a5..2d5fdf3224 100644 --- a/go/tools/scion-pki/internal/v2/conf/validity.go +++ b/go/tools/scion-pki/internal/v2/conf/validity.go @@ -15,6 +15,9 @@ package conf import ( + "time" + + "github.com/scionproto/scion/go/lib/scrypto" "github.com/scionproto/scion/go/lib/serrors" "github.com/scionproto/scion/go/lib/util" ) @@ -32,3 +35,15 @@ func (v Validity) Validate() error { } return nil } + +// Eval returns the validity period. The not before parameter is only used if +// the struct's not before field value is zero. +func (v Validity) Eval(notBefore time.Time) scrypto.Validity { + if v.NotBefore != 0 { + notBefore = util.SecsToTime(v.NotBefore) + } + return scrypto.Validity{ + NotBefore: util.UnixTime{Time: notBefore}, + NotAfter: util.UnixTime{Time: notBefore.Add(v.Validity.Duration)}, + } +} diff --git a/go/tools/scion-pki/internal/v2/keys/BUILD.bazel b/go/tools/scion-pki/internal/v2/keys/BUILD.bazel index cfe07180e8..d30772c369 100644 --- a/go/tools/scion-pki/internal/v2/keys/BUILD.bazel +++ b/go/tools/scion-pki/internal/v2/keys/BUILD.bazel @@ -1,17 +1,22 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ "cmd.go", "gen.go", + "priv.go", + "util.go", ], importpath = "github.com/scionproto/scion/go/tools/scion-pki/internal/v2/keys", 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/cert/v2:go_default_library", + "//go/lib/scrypto/trc/v2:go_default_library", "//go/lib/serrors:go_default_library", "//go/tools/scion-pki/internal/pkicmn:go_default_library", "//go/tools/scion-pki/internal/v2/conf:go_default_library", @@ -19,3 +24,21 @@ go_library( "@org_golang_x_crypto//ed25519:go_default_library", ], ) + +go_test( + name = "go_default_test", + srcs = ["priv_test.go"], + data = glob(["testdata/**"]), + embed = [":go_default_library"], + deps = [ + "//go/lib/addr:go_default_library", + "//go/lib/keyconf:go_default_library", + "//go/lib/scrypto:go_default_library", + "//go/lib/xtest:go_default_library", + "//go/tools/scion-pki/internal/pkicmn:go_default_library", + "//go/tools/scion-pki/internal/v2/conf:go_default_library", + "//go/tools/scion-pki/internal/v2/conf/testdata:go_default_library", + "@com_github_stretchr_testify//assert:go_default_library", + "@com_github_stretchr_testify//require:go_default_library", + ], +) diff --git a/go/tools/scion-pki/internal/v2/keys/priv.go b/go/tools/scion-pki/internal/v2/keys/priv.go new file mode 100644 index 0000000000..c4649f8c1e --- /dev/null +++ b/go/tools/scion-pki/internal/v2/keys/priv.go @@ -0,0 +1,209 @@ +// Copyright 2018 ETH Zurich +// Copyright 2019 ETH Zurich, 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 keys + +import ( + "encoding/pem" + "os" + "path/filepath" + "time" + + "github.com/scionproto/scion/go/lib/addr" + "github.com/scionproto/scion/go/lib/keyconf" + "github.com/scionproto/scion/go/lib/scrypto" + "github.com/scionproto/scion/go/lib/scrypto/cert/v2" + "github.com/scionproto/scion/go/lib/scrypto/trc/v2" + "github.com/scionproto/scion/go/lib/serrors" + "github.com/scionproto/scion/go/tools/scion-pki/internal/pkicmn" + "github.com/scionproto/scion/go/tools/scion-pki/internal/v2/conf" +) + +type privGen struct { + Dirs pkicmn.Dirs +} + +func (g privGen) Run(asMap map[addr.ISD][]addr.IA) error { + cfgs, err := g.loadConfigs(asMap) + if err != nil { + return err + } + keys, err := g.generateAllKeys(cfgs) + if err != nil { + return err + } + if err := g.createDirs(keys); err != nil { + return err + } + if err := g.writeKeys(keys); err != nil { + return err + } + return nil +} + +func (g privGen) loadConfigs(asMap map[addr.ISD][]addr.IA) (map[addr.IA]conf.Keys, error) { + cfgs := make(map[addr.IA]conf.Keys) + for _, ases := range asMap { + for _, ia := range ases { + file := conf.KeysFile(g.Dirs.Root, ia) + keys, err := conf.LoadKeys(file) + if err != nil { + return nil, serrors.WrapStr("unable to load keys config file", err, "file", file) + } + cfgs[ia] = keys + } + } + return cfgs, nil +} + +func (g privGen) generateAllKeys(cfgs map[addr.IA]conf.Keys) (map[addr.IA][]keyconf.Key, error) { + keys := make(map[addr.IA][]keyconf.Key) + for ia, cfg := range cfgs { + k, err := g.generateKeys(ia, cfg) + if err != nil { + return nil, serrors.WrapStr("unable to generate keys for AS", err, "ia", ia) + } + keys[ia] = k + } + return keys, nil +} + +func (g privGen) generateKeys(ia addr.IA, cfg conf.Keys) ([]keyconf.Key, error) { + var keys []keyconf.Key + for keyType, metas := range cfg.Primary { + for version, meta := range metas { + usage, err := usageFromTRCKeyType(keyType) + if err != nil { + return nil, serrors.WrapStr("error determining key usage", err, + "type", keyType, "version", version) + } + key, err := g.generateKey(ia, version, usage, meta) + if err != nil { + return nil, serrors.WrapStr("error generating key", err, "type", keyType, + "version", version) + } + keys = append(keys, key) + } + } + for keyType, metas := range cfg.Issuer { + for version, meta := range metas { + usage, err := usageFromIssuerKeyType(keyType) + if err != nil { + return nil, serrors.WrapStr("error determining key usage", err, + "type", keyType, "version", version) + } + key, err := g.generateKey(ia, version, usage, meta) + if err != nil { + return nil, serrors.WrapStr("error generating key", err, "type", keyType, + "version", version) + } + keys = append(keys, key) + } + } + for keyType, metas := range cfg.AS { + for version, meta := range metas { + usage, err := usageFromASKeyType(keyType) + if err != nil { + return nil, serrors.WrapStr("error determining key usage", err, + "type", keyType, "version", version) + } + key, err := g.generateKey(ia, version, usage, meta) + if err != nil { + return nil, serrors.WrapStr("error generating key", err, "type", keyType, + "version", version) + } + keys = append(keys, key) + } + } + return keys, nil +} + +func (g privGen) generateKey(ia addr.IA, version scrypto.KeyVersion, + usage keyconf.Usage, meta conf.KeyMeta) (keyconf.Key, error) { + + raw, err := genKey(meta.Algorithm) + if err != nil { + return keyconf.Key{}, err + } + key := keyconf.Key{ + Type: keyconf.PrivateKey, + Usage: usage, + Algorithm: meta.Algorithm, + Validity: meta.Validity.Eval(time.Now()), + Version: version, + IA: ia, + Bytes: raw, + } + return key, nil +} + +func (g privGen) createDirs(keys map[addr.IA][]keyconf.Key) error { + for ia := range keys { + if err := os.MkdirAll(PrivateDir(g.Dirs.Out, ia), 0700); err != nil { + return serrors.WrapStr("unable to make private keys directory", err, "ia", ia) + } + } + return nil +} + +func (g privGen) writeKeys(keys map[addr.IA][]keyconf.Key) error { + for ia, list := range keys { + for _, key := range list { + b := key.PEM() + file := filepath.Join(PrivateDir(g.Dirs.Out, ia), key.File()) + if err := pkicmn.WriteToFile(pem.EncodeToMemory(&b), file, 0600); err != nil { + return serrors.WrapStr("error writing private key file", err, "file", file) + } + } + } + return nil +} + +func usageFromTRCKeyType(keyType trc.KeyType) (keyconf.Usage, error) { + switch keyType { + case trc.IssuingKey: + return keyconf.TRCIssuingKey, nil + case trc.OnlineKey: + return keyconf.TRCVotingOnlineKey, nil + case trc.OfflineKey: + return keyconf.TRCVotingOfflineKey, nil + default: + return "", serrors.New("unsupported key type", "type", keyType) + } +} + +func usageFromASKeyType(keyType cert.KeyType) (keyconf.Usage, error) { + switch keyType { + case cert.SigningKey: + return keyconf.ASSigningKey, nil + case cert.EncryptionKey: + return keyconf.ASDecryptionKey, nil + case cert.RevocationKey: + return keyconf.ASRevocationKey, nil + default: + return "", serrors.New("unsupported key type", "type", keyType) + } +} + +func usageFromIssuerKeyType(keyType cert.KeyType) (keyconf.Usage, error) { + switch keyType { + case cert.IssuingKey: + return keyconf.IssCertSigningKey, nil + case cert.RevocationKey: + return keyconf.IssRevocationKey, nil + default: + return "", serrors.New("unsupported key type", "type", keyType) + } +} diff --git a/go/tools/scion-pki/internal/v2/keys/priv_test.go b/go/tools/scion-pki/internal/v2/keys/priv_test.go new file mode 100644 index 0000000000..87366ee47d --- /dev/null +++ b/go/tools/scion-pki/internal/v2/keys/priv_test.go @@ -0,0 +1,142 @@ +// 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 keys + +import ( + "bytes" + "encoding/pem" + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/scionproto/scion/go/lib/addr" + "github.com/scionproto/scion/go/lib/keyconf" + "github.com/scionproto/scion/go/lib/scrypto" + "github.com/scionproto/scion/go/lib/xtest" + "github.com/scionproto/scion/go/tools/scion-pki/internal/pkicmn" + "github.com/scionproto/scion/go/tools/scion-pki/internal/v2/conf" + "github.com/scionproto/scion/go/tools/scion-pki/internal/v2/conf/testdata" +) + +var ia110 = xtest.MustParseIA("1-ff00:0:110") + +func TestPrivGenRun(t *testing.T) { + tmpDir, cleanF := xtest.MustTempDir("", "test-trust") + defer cleanF() + + // Write keys config. + var buf bytes.Buffer + err := testdata.Keys(0).Encode(&buf) + require.NoError(t, err) + file := conf.KeysFile(tmpDir, ia110) + err = os.MkdirAll(filepath.Dir(file), 0755) + require.NoError(t, err) + err = ioutil.WriteFile(file, buf.Bytes(), 0644) + require.NoError(t, err) + + // Generate the key files. + asMap := map[addr.ISD][]addr.IA{1: {ia110}} + err = privGen{Dirs: pkicmn.Dirs{Root: tmpDir, Out: tmpDir}}.Run(asMap) + require.NoError(t, err) + + files := map[string]struct { + Algorithm string + Usage keyconf.Usage + Version scrypto.KeyVersion + Validity time.Duration + }{ + "as-signing-v3.key": { + Algorithm: scrypto.Ed25519, + Usage: keyconf.ASSigningKey, + Version: 3, + Validity: 90 * 24 * time.Hour, + }, + "as-revocation-v2.key": { + Algorithm: scrypto.Ed25519, + Usage: keyconf.ASRevocationKey, + Version: 2, + Validity: 90 * 24 * time.Hour, + }, + "as-decrypt-v1.key": { + Algorithm: scrypto.Curve25519xSalsa20Poly1305, + Usage: keyconf.ASDecryptionKey, + Version: 1, + Validity: 90 * 24 * time.Hour, + }, + "issuer-revocation-v2.key": { + Algorithm: scrypto.Ed25519, + Usage: keyconf.IssRevocationKey, + Version: 2, + Validity: 180 * 24 * time.Hour, + }, + "issuer-cert-signing-v1.key": { + Algorithm: scrypto.Ed25519, + Usage: keyconf.IssCertSigningKey, + Version: 1, + Validity: 180 * 24 * time.Hour, + }, + "trc-voting-online-v2.key": { + Algorithm: scrypto.Ed25519, + Usage: keyconf.TRCVotingOnlineKey, + Version: 2, + Validity: 365 * 24 * time.Hour, + }, + "trc-voting-online-v1.key": { + Algorithm: scrypto.Ed25519, + Usage: keyconf.TRCVotingOnlineKey, + Version: 1, + Validity: 365 * 24 * time.Hour, + }, + "trc-voting-offline-v1.key": { + Algorithm: scrypto.Ed25519, + Usage: keyconf.TRCVotingOfflineKey, + Version: 1, + Validity: 365 * 24 * time.Hour, + }, + "trc-issuing-v1.key": { + Algorithm: scrypto.Ed25519, + Usage: keyconf.TRCIssuingKey, + Version: 1, + Validity: 365 * 24 * time.Hour, + }, + } + for file, exp := range files { + t.Run(file, func(t *testing.T) { + raw, err := ioutil.ReadFile(filepath.Join(PrivateDir(tmpDir, ia110), file)) + require.NoError(t, err) + p, _ := pem.Decode(raw) + require.NotNil(t, p) + key, err := keyconf.KeyFromPEM(p) + require.NoError(t, err) + assert.Equal(t, keyconf.PrivateKey, key.Type) + assert.Equal(t, exp.Usage, key.Usage) + assert.Equal(t, exp.Algorithm, key.Algorithm) + assert.Equal(t, exp.Version, key.Version) + assert.Equal(t, ia110, key.IA) + assert.True(t, len(key.Bytes) > 1) + + validity := key.Validity.NotAfter.Sub(key.Validity.NotBefore.Time) + assert.Equal(t, exp.Validity, validity) + assert.InDelta(t, time.Now().Unix(), key.Validity.NotBefore.Unix(), + float64(10*time.Second)) + }) + } + +} diff --git a/go/tools/scion-pki/internal/v2/keys/util.go b/go/tools/scion-pki/internal/v2/keys/util.go new file mode 100644 index 0000000000..618455a759 --- /dev/null +++ b/go/tools/scion-pki/internal/v2/keys/util.go @@ -0,0 +1,32 @@ +// 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 keys + +import ( + "path/filepath" + + "github.com/scionproto/scion/go/lib/addr" + "github.com/scionproto/scion/go/tools/scion-pki/internal/pkicmn" +) + +// PrivateDir returns the directory where the private keys are written to. +func PrivateDir(out string, ia addr.IA) string { + return filepath.Join(pkicmn.GetAsPath(out, ia), "keys") +} + +// PublicDir returns the directory where the public keys are written to. +func PublicDir(out string, ia addr.IA) string { + return filepath.Join(pkicmn.GetAsPath(out, ia), "pub") +} diff --git a/go/tools/scion-pki/internal/v2/tmpl/topo.go b/go/tools/scion-pki/internal/v2/tmpl/topo.go index 58e2b7f59c..28b4e88f0b 100644 --- a/go/tools/scion-pki/internal/v2/tmpl/topo.go +++ b/go/tools/scion-pki/internal/v2/tmpl/topo.go @@ -60,16 +60,15 @@ func runGenTopoTmpl(path string) error { } } for ia := range topo.ASes { - keys := genKeysTmpl(ia, val, isdCfgs[ia.I]) - dir := pkicmn.GetAsPath(pkicmn.RootDir, ia) - if err := os.MkdirAll(dir, 0755); err != nil { + file := conf.KeysFile(pkicmn.RootDir, ia) + if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil { return serrors.WrapStr("unable to make AS directory", err, "ia", ia) } + keys := genKeysTmpl(ia, val, isdCfgs[ia.I]) var buf bytes.Buffer if err := keys.Encode(&buf); err != nil { return serrors.WithCtx(err, "ia", ia) } - file := filepath.Join(dir, conf.KeysFileName) if err := pkicmn.WriteToFile(buf.Bytes(), file, 0644); err != nil { return serrors.WrapStr("unable to write key config", err, "ia", ia, "file", file) }