From d37df12b0ec60c9a0c0520b6fb16462a39555daf Mon Sep 17 00:00:00 2001 From: Artem Glazychev Date: Tue, 19 Sep 2023 18:33:00 +0700 Subject: [PATCH] Fix IPSec key generation (#747) Signed-off-by: Artem Glazychev --- pkg/networkservice/chains/forwarder/server.go | 9 +- pkg/networkservice/mechanisms/ipsec/client.go | 49 +++++++++-- pkg/networkservice/mechanisms/ipsec/common.go | 85 +++++++++++++------ .../mechanisms/ipsec/options.go | 31 +++++++ pkg/networkservice/mechanisms/ipsec/server.go | 53 +++++++++--- 5 files changed, 180 insertions(+), 47 deletions(-) create mode 100644 pkg/networkservice/mechanisms/ipsec/options.go diff --git a/pkg/networkservice/chains/forwarder/server.go b/pkg/networkservice/chains/forwarder/server.go index 2fe30e7f..0f7e1a86 100644 --- a/pkg/networkservice/chains/forwarder/server.go +++ b/pkg/networkservice/chains/forwarder/server.go @@ -47,6 +47,7 @@ import ( "github.com/networkservicemesh/sdk/pkg/networkservice/common/mechanisms/sendfd" "github.com/networkservicemesh/sdk/pkg/networkservice/common/mechanismtranslation" "github.com/networkservicemesh/sdk/pkg/networkservice/common/roundrobin" + "github.com/networkservicemesh/sdk/pkg/tools/log" authmonitor "github.com/networkservicemesh/sdk/pkg/tools/monitorconnection/authorize" "github.com/networkservicemesh/sdk/pkg/tools/token" @@ -109,6 +110,10 @@ func NewServer(ctx context.Context, tokenGenerator token.GeneratorFunc, vppConn registryclient.WithClientURL(opts.clientURL), registryclient.WithDialOptions(opts.dialOpts...)) + ikev2Key, err := ipsec.GenerateRSAKey() + if err != nil { + log.FromContext(ctx).Fatalf("error ipsec.GenerateRSAKey: %v", err.Error()) + } rv := &xconnectNSServer{} pinholeMutex := new(sync.Mutex) additionalFunctionality := []networkservice.NetworkServiceServer{ @@ -131,7 +136,7 @@ func NewServer(ctx context.Context, tokenGenerator token.GeneratorFunc, vppConn kernel.MECHANISM: kernel.NewServer(vppConn), vxlan.MECHANISM: vxlan.NewServer(vppConn, tunnelIP, opts.vxlanOpts...), wireguard.MECHANISM: wireguard.NewServer(vppConn, tunnelIP), - ipsecapi.MECHANISM: ipsec.NewServer(vppConn, tunnelIP), + ipsecapi.MECHANISM: ipsec.NewServer(vppConn, tunnelIP, ipsec.WithIKEv2PrivateKey(ikev2Key)), }), afxdppinhole.NewServer(), pinhole.NewServer(vppConn, pinhole.WithSharedMutex(pinholeMutex)), @@ -157,7 +162,7 @@ func NewServer(ctx context.Context, tokenGenerator token.GeneratorFunc, vppConn kernel.NewClient(vppConn), vxlan.NewClient(vppConn, tunnelIP, opts.vxlanOpts...), wireguard.NewClient(vppConn, tunnelIP), - ipsec.NewClient(vppConn, tunnelIP), + ipsec.NewClient(vppConn, tunnelIP, ipsec.WithIKEv2PrivateKey(ikev2Key)), vlan.NewClient(vppConn, opts.domain2Device), filtermechanisms.NewClient(), mechanismpriority.NewClient(opts.mechanismPrioriyList...), diff --git a/pkg/networkservice/mechanisms/ipsec/client.go b/pkg/networkservice/mechanisms/ipsec/client.go index 22e8b6c3..2771177c 100644 --- a/pkg/networkservice/mechanisms/ipsec/client.go +++ b/pkg/networkservice/mechanisms/ipsec/client.go @@ -18,7 +18,9 @@ package ipsec import ( "context" + "crypto/rsa" "net" + "sync" "github.com/golang/protobuf/ptypes/empty" "github.com/pkg/errors" @@ -32,6 +34,7 @@ import ( "github.com/networkservicemesh/sdk/pkg/networkservice/core/chain" "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" "github.com/networkservicemesh/sdk/pkg/networkservice/utils/metadata" + "github.com/networkservicemesh/sdk/pkg/tools/log" "github.com/networkservicemesh/sdk/pkg/tools/postpone" "github.com/networkservicemesh/sdk-vpp/pkg/networkservice/mechanisms/ipsec/mtu" @@ -41,14 +44,38 @@ import ( type ipsecClient struct { vppConn api.Connection tunnelIP net.IP + + privateKeyFileName string + privateKey *rsa.PrivateKey + onceInit sync.Once } // NewClient - returns a new client for the IPSec remote mechanism -func NewClient(vppConn api.Connection, tunnelIP net.IP) networkservice.NetworkServiceClient { +func NewClient(vppConn api.Connection, tunnelIP net.IP, options ...Option) networkservice.NetworkServiceClient { + opts := &ipsecOptions{} + for _, opt := range options { + opt(opts) + } + + if opts.privateKeyFileName == "" { + var err error + opts.privateKeyFileName, err = GenerateRSAKey() + if err != nil { + log.FromContext(context.Background()).Fatalf("ipsecClient GenerateRSAKey error: %v", err) + } + } + + privateKey, err := privateKeyFromFile(opts.privateKeyFileName) + if err != nil { + log.FromContext(context.Background()).Fatalf("ipsecClient privateKeyFromFile error: %v", err) + } + return chain.NewNetworkServiceClient( &ipsecClient{ - vppConn: vppConn, - tunnelIP: tunnelIP, + vppConn: vppConn, + tunnelIP: tunnelIP, + privateKeyFileName: opts.privateKeyFileName, + privateKey: privateKey, }, mtu.NewClient(vppConn, tunnelIP), ) @@ -58,12 +85,15 @@ func (i *ipsecClient) Request(ctx context.Context, request *networkservice.Netwo if request.GetConnection().GetPayload() != payload.IP { return next.Client(ctx).Request(ctx, request, opts...) } - - rsaKey, err := generateRSAKey() + var err error + i.onceInit.Do(func() { + err = setIKEv2LocalKey(ctx, i.vppConn, i.privateKeyFileName) + }) if err != nil { return nil, err } - publicKey, err := createCertBase64(rsaKey, metadata.IsClient(i)) + + certificate, err := createCertBase64(i.privateKey, metadata.IsClient(i)) if err != nil { return nil, err } @@ -71,15 +101,16 @@ func (i *ipsecClient) Request(ctx context.Context, request *networkservice.Netwo // else create a new one and store it after successful interface creation if mechanism := ipsecMech.ToMechanism(request.GetConnection().GetMechanism()); mechanism != nil { // If there is a key in mechanism then we can use it - publicKey = mechanism.SrcPublicKey() + certificate = mechanism.SrcPublicKey() } + mechanism := &networkservice.Mechanism{ Cls: cls.REMOTE, Type: ipsecMech.MECHANISM, Parameters: make(map[string]string), } ipsecMech.ToMechanism(mechanism). - SetSrcPublicKey(publicKey). + SetSrcPublicKey(certificate). SetSrcIP(i.tunnelIP). SetSrcPort(ikev2DefaultPort) @@ -95,7 +126,7 @@ func (i *ipsecClient) Request(ctx context.Context, request *networkservice.Netwo return nil, err } - if err = create(ctx, conn, i.vppConn, rsaKey, metadata.IsClient(i)); err != nil { + if err = create(ctx, conn, i.vppConn, metadata.IsClient(i)); err != nil { closeCtx, cancelClose := postponeCtxFunc() defer cancelClose() diff --git a/pkg/networkservice/mechanisms/ipsec/common.go b/pkg/networkservice/mechanisms/ipsec/common.go index 62a34504..a1bfea84 100644 --- a/pkg/networkservice/mechanisms/ipsec/common.go +++ b/pkg/networkservice/mechanisms/ipsec/common.go @@ -30,8 +30,11 @@ import ( "net" "os" "path" + "path/filepath" "time" + "github.com/google/uuid" + "github.com/networkservicemesh/govpp/binapi/ikev2_types" interfaces "github.com/networkservicemesh/govpp/binapi/interface" "github.com/networkservicemesh/govpp/binapi/interface_types" @@ -51,7 +54,7 @@ import ( ) // create - creates IPSEC with IKEv2 -func create(ctx context.Context, conn *networkservice.Connection, vppConn api.Connection, privateKey *rsa.PrivateKey, isClient bool) error { +func create(ctx context.Context, conn *networkservice.Connection, vppConn api.Connection, isClient bool) error { if mechanism := ipsec.ToMechanism(conn.GetMechanism()); mechanism != nil { _, ok := ifindex.Load(ctx, isClient) if ok { @@ -78,7 +81,7 @@ func create(ctx context.Context, conn *networkservice.Connection, vppConn api.Co } // *** SET KEYS *** // - err = setKeys(ctx, vppConn, profileName, mechanism, privateKey, isClient) + err = setKeys(ctx, vppConn, profileName, mechanism, isClient) if err != nil { return err } @@ -254,23 +257,17 @@ func setUDPEncap(ctx context.Context, vppConn api.Connection, profileName string return nil } -func setKeys(ctx context.Context, vppConn api.Connection, profileName string, mechanism *ipsec.Mechanism, privateKey *rsa.PrivateKey, isClient bool) error { +func setKeys(ctx context.Context, vppConn api.Connection, profileName string, mechanism *ipsec.Mechanism, isClient bool) error { publicKeyBase64 := mechanism.SrcPublicKey() if isClient { publicKeyBase64 = mechanism.DstPublicKey() } - publicKeyFileName, err := dumpCertBase64ToFile(publicKeyBase64, profileName, isClient) + publicKeyFileName, err := dumpCertBase64ToFile(publicKeyBase64, profileName) if err != nil { return err } log.FromContext(ctx).WithField("operation", "dumpCertBase64ToFile").Debug("completed") - privateKeyFileName, err := dumpPrivateKeyToFile(privateKey, profileName, isClient) - if err != nil { - return err - } - log.FromContext(ctx).WithField("operation", "dumpPrivateKeyToFile").Debug("completed") - now := time.Now() _, err = ikev2.NewServiceClient(vppConn).Ikev2ProfileSetAuth(ctx, &ikev2.Ikev2ProfileSetAuth{ Name: profileName, @@ -286,8 +283,12 @@ func setKeys(ctx context.Context, vppConn api.Connection, profileName string, me WithField("duration", time.Since(now)). WithField("vppapi", "Ikev2ProfileSetAuth").Debug("completed") - now = time.Now() - _, err = ikev2.NewServiceClient(vppConn).Ikev2SetLocalKey(ctx, &ikev2.Ikev2SetLocalKey{ + return nil +} + +func setIKEv2LocalKey(ctx context.Context, vppConn api.Connection, privateKeyFileName string) error { + now := time.Now() + _, err := ikev2.NewServiceClient(vppConn).Ikev2SetLocalKey(ctx, &ikev2.Ikev2SetLocalKey{ KeyFile: privateKeyFileName, }) if err != nil { @@ -296,7 +297,6 @@ func setKeys(ctx context.Context, vppConn api.Connection, profileName string, me log.FromContext(ctx). WithField("duration", time.Since(now)). WithField("vppapi", "Ikev2SetLocalKey").Debug("completed") - return nil } @@ -527,9 +527,9 @@ func generateRSAKey() (*rsa.PrivateKey, error) { return key, nil } -func dumpPrivateKeyToFile(privatekey *rsa.PrivateKey, profileName string, isClient bool) (string, error) { - dir := path.Join(os.TempDir(), profileName) - err := os.Mkdir(dir, 0o700) +func dumpPrivateKeyToFile(privatekey *rsa.PrivateKey, folderName string) (string, error) { + dir := path.Join(os.TempDir(), "networkservicemesh", folderName) + err := os.MkdirAll(dir, 0o700) if err != nil && !os.IsExist(err) { return "", errors.Wrapf(err, "failed to create directory %s", dir) } @@ -540,9 +540,10 @@ func dumpPrivateKeyToFile(privatekey *rsa.PrivateKey, profileName string, isClie Bytes: privateKeyBytes, } - file, err := os.Create(path.Clean(path.Join(dir, isClientPrefix(isClient)+"-key.pem"))) + keyName := "private.pem" + file, err := os.Create(path.Clean(path.Join(dir, keyName))) if err != nil { - return "", errors.Wrapf(err, "failed to create file %s", path.Clean(path.Join(dir, isClientPrefix(isClient)+"-key.pem"))) + return "", errors.Wrapf(err, "failed to create file %s", path.Clean(path.Join(dir, keyName))) } err = pem.Encode(file, privateKeyBlock) if err != nil { @@ -552,6 +553,24 @@ func dumpPrivateKeyToFile(privatekey *rsa.PrivateKey, profileName string, isClie return file.Name(), nil } +func privateKeyFromFile(f string) (*rsa.PrivateKey, error) { + bytes, err := os.ReadFile(filepath.Clean(f)) + if err != nil { + return nil, err + } + + pemBlock, _ := pem.Decode(bytes) + if pemBlock == nil { + return nil, errors.New("pem decode error") + } + + privateKey, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes) + if err != nil { + return nil, err + } + return privateKey, nil +} + func createCertBase64(privatekey *rsa.PrivateKey, isClient bool) (string, error) { // Generate cryptographically strong pseudo-random between 0 - max max := new(big.Int) @@ -566,8 +585,6 @@ func createCertBase64(privatekey *rsa.PrivateKey, isClient bool) (string, error) Subject: pkix.Name{ CommonName: isClientPrefix(isClient), }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Minute * 5), } certbytes, err := x509.CreateCertificate(rand.Reader, template, template, &privatekey.PublicKey, privatekey) if err != nil { @@ -576,9 +593,9 @@ func createCertBase64(privatekey *rsa.PrivateKey, isClient bool) (string, error) return base64.StdEncoding.EncodeToString(certbytes), nil } -func dumpCertBase64ToFile(base64key, profileName string, isClient bool) (string, error) { - dir := path.Join(os.TempDir(), profileName) - err := os.Mkdir(dir, 0o700) +func dumpCertBase64ToFile(base64key, folderName string) (string, error) { + dir := path.Join(os.TempDir(), "networkservicemesh", folderName) + err := os.MkdirAll(dir, 0o700) if err != nil && !os.IsExist(err) { return "", errors.Wrapf(err, "failed to create directory %s", dir) } @@ -593,9 +610,9 @@ func dumpCertBase64ToFile(base64key, profileName string, isClient bool) (string, Bytes: certbytes, } - file, err := os.Create(path.Clean(path.Join(dir, isClientPrefix(!isClient)+"-cert.pem"))) + file, err := os.Create(path.Clean(path.Join(dir, "cert.pem"))) if err != nil { - return "", errors.Wrapf(err, "failed to create file %s", path.Clean(path.Join(dir, isClientPrefix(!isClient)+"-cert.pem"))) + return "", errors.Wrapf(err, "failed to create file %s", path.Clean(path.Join(dir, "cert.pem"))) } err = pem.Encode(file, publicKeyBlock) if err != nil { @@ -611,3 +628,21 @@ func isClientPrefix(isClient bool) string { } return "server" } + +// GenerateRSAKey generates RSA private key +func GenerateRSAKey() (privateKeyFileName string, err error) { + var privateKey *rsa.PrivateKey + privateKey, err = generateRSAKey() + if err != nil { + return + } + + folderName := "ikev2_" + uuid.New().String()[:8] + var privKeyFileName string + privKeyFileName, err = dumpPrivateKeyToFile(privateKey, folderName) + if err != nil { + return + } + + return privKeyFileName, err +} diff --git a/pkg/networkservice/mechanisms/ipsec/options.go b/pkg/networkservice/mechanisms/ipsec/options.go new file mode 100644 index 00000000..c07c99f3 --- /dev/null +++ b/pkg/networkservice/mechanisms/ipsec/options.go @@ -0,0 +1,31 @@ +// Copyright (c) 2023 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 ipsec + +type ipsecOptions struct { + privateKeyFileName string +} + +// Option is an option pattern for ipsec chain element +type Option func(o *ipsecOptions) + +// WithIKEv2PrivateKey - sets private key paths +func WithIKEv2PrivateKey(privateKeyFileName string) Option { + return func(o *ipsecOptions) { + o.privateKeyFileName = privateKeyFileName + } +} diff --git a/pkg/networkservice/mechanisms/ipsec/server.go b/pkg/networkservice/mechanisms/ipsec/server.go index 34764f54..fc1fd826 100644 --- a/pkg/networkservice/mechanisms/ipsec/server.go +++ b/pkg/networkservice/mechanisms/ipsec/server.go @@ -18,18 +18,21 @@ package ipsec import ( "context" + "crypto/rsa" "net" + "sync" - "github.com/golang/protobuf/ptypes/empty" "github.com/pkg/errors" "go.fd.io/govpp/api" + "github.com/golang/protobuf/ptypes/empty" "github.com/networkservicemesh/api/pkg/api/networkservice" ipsecMech "github.com/networkservicemesh/api/pkg/api/networkservice/mechanisms/ipsec" "github.com/networkservicemesh/api/pkg/api/networkservice/payload" "github.com/networkservicemesh/sdk/pkg/networkservice/core/chain" "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" "github.com/networkservicemesh/sdk/pkg/networkservice/utils/metadata" + "github.com/networkservicemesh/sdk/pkg/tools/log" "github.com/networkservicemesh/sdk/pkg/tools/postpone" "github.com/networkservicemesh/sdk-vpp/pkg/networkservice/mechanisms/ipsec/mtu" @@ -39,15 +42,39 @@ import ( type ipsecServer struct { vppConn api.Connection tunnelIP net.IP + + privateKeyFileName string + privateKey *rsa.PrivateKey + onceInit sync.Once } // NewServer - returns a new server for the IPSec remote mechanism -func NewServer(vppConn api.Connection, tunnelIP net.IP) networkservice.NetworkServiceServer { +func NewServer(vppConn api.Connection, tunnelIP net.IP, options ...Option) networkservice.NetworkServiceServer { + opts := &ipsecOptions{} + for _, opt := range options { + opt(opts) + } + + if opts.privateKeyFileName == "" { + var err error + opts.privateKeyFileName, err = GenerateRSAKey() + if err != nil { + log.FromContext(context.Background()).Fatalf("ipsecServer GenerateRSAKey error: %v", err) + } + } + + privateKey, err := privateKeyFromFile(opts.privateKeyFileName) + if err != nil { + log.FromContext(context.Background()).Fatalf("ipsecServer privateKeyFromFile error: %v", err) + } + return chain.NewNetworkServiceServer( mtu.NewServer(vppConn, tunnelIP), &ipsecServer{ - vppConn: vppConn, - tunnelIP: tunnelIP, + vppConn: vppConn, + tunnelIP: tunnelIP, + privateKeyFileName: opts.privateKeyFileName, + privateKey: privateKey, }, ) } @@ -56,6 +83,14 @@ func (i *ipsecServer) Request(ctx context.Context, request *networkservice.Netwo if request.GetConnection().GetPayload() != payload.IP { return next.Server(ctx).Request(ctx, request) } + var err error + i.onceInit.Do(func() { + err = setIKEv2LocalKey(ctx, i.vppConn, i.privateKeyFileName) + }) + if err != nil { + return nil, err + } + if mechanism := ipsecMech.ToMechanism(request.GetConnection().GetMechanism()); mechanism != nil { mechanism.SetDstIP(i.tunnelIP) mechanism.SetDstPort(ikev2DefaultPort) @@ -72,17 +107,13 @@ func (i *ipsecServer) Request(ctx context.Context, request *networkservice.Netwo } if mechanism := ipsecMech.ToMechanism(conn.GetMechanism()); mechanism != nil { - rsaKey, err := generateRSAKey() - if err != nil { - return nil, err - } - publicKey, err := createCertBase64(rsaKey, metadata.IsClient(i)) + certificate, err := createCertBase64(i.privateKey, metadata.IsClient(i)) if err != nil { return nil, err } - mechanism.SetDstPublicKey(publicKey) + mechanism.SetDstPublicKey(certificate) - err = create(ctx, conn, i.vppConn, rsaKey, metadata.IsClient(i)) + err = create(ctx, conn, i.vppConn, metadata.IsClient(i)) if err != nil { closeCtx, cancelClose := postponeCtxFunc() defer cancelClose()