diff --git a/cmd/adam.go b/cmd/adam.go index c15c0dade..7f49743c1 100644 --- a/cmd/adam.go +++ b/cmd/adam.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "os" "github.com/lf-edge/eden/pkg/defaults" "github.com/lf-edge/eden/pkg/eden" @@ -24,6 +25,7 @@ func newAdamCmd(configName, verbosity *string) *cobra.Command { newStartAdamCmd(cfg), newStopAdamCmd(), newStatusAdamCmd(), + newChangeCertCmd(), }, }, } @@ -91,3 +93,27 @@ func newStatusAdamCmd() *cobra.Command { return statusAdamCmd } + +func newChangeCertCmd() *cobra.Command { + var certFile string + + var changeCertCmd = &cobra.Command{ + Use: "change-signing-cert", + Short: "change signing certificate for adam", + Long: `Set Adam's signing certificate from a file.`, + Run: func(cmd *cobra.Command, args []string) { + certData, err := os.ReadFile(certFile) + if err != nil { + log.Fatalf("Failed to read certificate file: %s", err) + } + + if err := openEVEC.ChangeSigningCert(certData); err != nil { + log.Fatalf("Failed to upload certificate to adam: %s", err) + } + }, + } + + changeCertCmd.Flags().StringVarP(&certFile, "cert-file", "", "", "path to the signing certificate file") + + return changeCertCmd +} diff --git a/cmd/certs.go b/cmd/certs.go index 312ead666..98a8fc2f7 100644 --- a/cmd/certs.go +++ b/cmd/certs.go @@ -7,6 +7,7 @@ import ( "github.com/lf-edge/eden/pkg/defaults" "github.com/lf-edge/eden/pkg/eden" "github.com/lf-edge/eden/pkg/openevec" + "github.com/lf-edge/eden/pkg/utils" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -47,3 +48,29 @@ func newCertsCmd(cfg *openevec.EdenSetupArgs) *cobra.Command { return certsCmd } + +func newGenSigningCertCmd() *cobra.Command { + var certPath string + + var certsCmd = &cobra.Command{ + Use: "gen-signing-cert", + Short: "generate new signing certificate for controller", + Long: `Generate a new signing certificate for the controller using the same signing key`, + Run: func(cmd *cobra.Command, args []string) { + if err := utils.GenServerCertFromPrevCertAndKey(certPath); err != nil { + log.Errorf("cannot generate signing cert: %s", err) + } else { + log.Info("GenServerCertEllipticFromPrevCertAndKey done") + } + }, + } + + edenHome, err := utils.DefaultEdenDir() + if err != nil { + log.Fatal(err) + } + + certsCmd.Flags().StringVarP(&certPath, "out", "o", filepath.Join(edenHome, defaults.DefaultCertsDist, "signing-new.pem"), "certificate output path") + + return certsCmd +} diff --git a/cmd/edenUtils.go b/cmd/edenUtils.go index 0e98325e1..4b53e617f 100644 --- a/cmd/edenUtils.go +++ b/cmd/edenUtils.go @@ -30,6 +30,7 @@ func newUtilsCmd(configName, verbosity *string) *cobra.Command { newDownloaderCmd(cfg), newOciImageCmd(), newCertsCmd(cfg), + newGenSigningCertCmd(), newGcpCmd(cfg), newSdInfoEveCmd(), newDebugCmd(cfg), diff --git a/pkg/controller/adam/adam.go b/pkg/controller/adam/adam.go index 5f49b8eae..09c8c2eed 100644 --- a/pkg/controller/adam/adam.go +++ b/pkg/controller/adam/adam.go @@ -22,8 +22,11 @@ import ( "github.com/lf-edge/eden/pkg/defaults" "github.com/lf-edge/eden/pkg/device" "github.com/lf-edge/eden/pkg/utils" + "github.com/lf-edge/eve-api/go/auth" + "github.com/lf-edge/eve-api/go/certs" uuid "github.com/satori/go.uuid" log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/proto" ) const ( @@ -39,11 +42,11 @@ type Ctx struct { serverCA string insecureTLS bool AdamRemote bool - AdamRemoteRedis bool //use redis for obtain logs and info - AdamRedisURLEden string //string with redis url for obtain logs and info - AdamCaching bool //enable caching of adam`s logs/info - AdamCachingRedis bool //caching to redis instead of files - AdamCachingPrefix string //custom prefix for file or stream naming for cache + AdamRemoteRedis bool // use redis for obtain logs and info + AdamRedisURLEden string // string with redis url for obtain logs and info + AdamCaching bool // enable caching of adam`s logs/info + AdamCachingRedis bool // caching to redis instead of files + AdamCachingPrefix string // custom prefix for file or stream naming for cache } // parseRedisURL try to use string from config to obtain redis url @@ -196,9 +199,26 @@ func (adam *Ctx) ConfigGet(devUUID uuid.UUID) (out string, err error) { return adam.getObj(path.Join("/admin/device", devUUID.String(), "config"), mimeProto) } -// CertsGet get attest certs for devID -func (adam *Ctx) CertsGet(devUUID uuid.UUID) (out string, err error) { - return adam.getObj(path.Join("/admin/device", devUUID.String(), "certs"), mimeJSON) +// GetECDHCert get cert for ECDH exchange for devID +func (adam *Ctx) GetECDHCert(devUUID uuid.UUID) ([]byte, error) { + attestData, err := adam.getObj(path.Join("/admin/device", devUUID.String(), "certs"), mimeJSON) + if err != nil { + return nil, fmt.Errorf("cannot get attestation certificates from cloud for %s", devUUID) + } + req := &types.Zcerts{} + if err := json.Unmarshal([]byte(attestData), req); err != nil { + return nil, fmt.Errorf("cannot unmarshal attest: %w", err) + } + var devCert []byte + for _, c := range req.Certs { + if c.Type == certs.ZCertType_CERT_TYPE_DEVICE_ECDH_EXCHANGE { + devCert = c.Cert + } + } + if len(devCert) == 0 { + return nil, fmt.Errorf("no DEVICE_ECDH_EXCHANGE certificate") + } + return devCert, nil } // RequestLastCallback check request by pattern from existence files with callback @@ -405,3 +425,28 @@ func (adam *Ctx) GetGlobalOptions() (*types.GlobalOptions, error) { } return &globalOptions, nil } + +// SigningCertGet gets signing certificate from Adam +func (adam *Ctx) SigningCertGet() (signCert []byte, err error) { + certsData, err := adam.getObj("/api/v2/edgedevice/certs", mimeProto) + if err != nil { + return nil, err + } + zcloudMsg := &auth.AuthContainer{} + err = proto.Unmarshal([]byte(certsData), zcloudMsg) + if err != nil { + return nil, err + } + ctrlCert := &certs.ZControllerCert{} + err = proto.Unmarshal(zcloudMsg.ProtectedPayload.Payload, ctrlCert) + if err != nil { + return nil, err + } + for _, c := range ctrlCert.Certs { + // there should be only one signing certificate, so we return the first one we find + if c.Type == certs.ZCertType_CERT_TYPE_CONTROLLER_SIGNING { + return c.Cert, nil + } + } + return nil, fmt.Errorf("no signing certificate found") +} diff --git a/pkg/controller/application.go b/pkg/controller/application.go index ac055ab31..644ea2509 100644 --- a/pkg/controller/application.go +++ b/pkg/controller/application.go @@ -2,6 +2,7 @@ package controller import ( "fmt" + "github.com/lf-edge/eden/pkg/utils" "github.com/lf-edge/eve-api/go/config" ) @@ -15,7 +16,7 @@ func (cloud *CloudCtx) getApplicationInstanceInd(id string) (applicationInstance return -1, fmt.Errorf("not found applicationInstance with ID: %s", id) } -//GetApplicationInstanceConfig return AppInstanceConfig config from cloud by ID +// GetApplicationInstanceConfig return AppInstanceConfig config from cloud by ID func (cloud *CloudCtx) GetApplicationInstanceConfig(id string) (applicationInstanceConfig *config.AppInstanceConfig, err error) { applicationInstanceConfigInd, err := cloud.getApplicationInstanceInd(id) if err != nil { @@ -24,13 +25,13 @@ func (cloud *CloudCtx) GetApplicationInstanceConfig(id string) (applicationInsta return cloud.applicationInstances[applicationInstanceConfigInd], nil } -//AddApplicationInstanceConfig add AppInstanceConfig config to cloud +// AddApplicationInstanceConfig add AppInstanceConfig config to cloud func (cloud *CloudCtx) AddApplicationInstanceConfig(applicationInstanceConfig *config.AppInstanceConfig) error { cloud.applicationInstances = append(cloud.applicationInstances, applicationInstanceConfig) return nil } -//RemoveApplicationInstanceConfig remove AppInstanceConfig config to cloud +// RemoveApplicationInstanceConfig remove AppInstanceConfig config to cloud func (cloud *CloudCtx) RemoveApplicationInstanceConfig(id string) error { applicationInstanceConfigInd, err := cloud.getApplicationInstanceInd(id) if err != nil { @@ -40,7 +41,7 @@ func (cloud *CloudCtx) RemoveApplicationInstanceConfig(id string) error { return nil } -//ListApplicationInstanceConfig return ApplicationInstance configs from cloud +// ListApplicationInstanceConfig return ApplicationInstance configs from cloud func (cloud *CloudCtx) ListApplicationInstanceConfig() []*config.AppInstanceConfig { return cloud.applicationInstances } diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 404ed6dbd..99413ae1f 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -15,9 +15,10 @@ import ( uuid "github.com/satori/go.uuid" ) -//Controller is an interface of controller +// Controller is an interface of controller type Controller interface { - CertsGet(devUUID uuid.UUID) (out string, err error) + GetECDHCert(devUUID uuid.UUID) ([]byte, error) + SigningCertGet() (signCert []byte, err error) ConfigGet(devUUID uuid.UUID) (out string, err error) ConfigSet(devUUID uuid.UUID, devConfig []byte) (err error) LogAppsChecker(devUUID uuid.UUID, appUUID uuid.UUID, q map[string]string, handler eapps.HandlerFunc, mode eapps.LogCheckerMode, timeout time.Duration) (err error) diff --git a/pkg/controller/functions.go b/pkg/controller/functions.go index 6f52dafb4..8818d5bf1 100644 --- a/pkg/controller/functions.go +++ b/pkg/controller/functions.go @@ -165,8 +165,8 @@ func (cloud *CloudCtx) OnBoardDev(node *device.Ctx) error { if err = cloud.ConfigSync(node); err != nil { log.Fatal(err) } - //wait for certs - if _, err = cloud.CertsGet(node.GetID()); err != nil { + // wait for certs + if _, err = cloud.GetECDHCert(node.GetID()); err != nil { log.Fatal(err) } } diff --git a/pkg/eden/eden.go b/pkg/eden/eden.go index 556112686..40e0f72ce 100644 --- a/pkg/eden/eden.go +++ b/pkg/eden/eden.go @@ -130,53 +130,20 @@ func StartAdam(adamPort int, adamPath string, adamForce bool, adamTag string, ad return err } globalCertsDir := filepath.Join(edenHome, defaults.DefaultCertsDist) - serverCertPath := filepath.Join(globalCertsDir, "server.pem") - serverKeyPath := filepath.Join(globalCertsDir, "server-key.pem") - cert, err := os.ReadFile(serverCertPath) - if err != nil { - return fmt.Errorf("StartAdam: cannot load %s: %s", serverCertPath, err) - } - key, err := os.ReadFile(serverKeyPath) - if err != nil { - return fmt.Errorf("StartAdam: cannot load %s: %s", serverKeyPath, err) - } - envs := []string{ - fmt.Sprintf("SERVER_CERT=%s", cert), - fmt.Sprintf("SERVER_KEY=%s", key), - } - if !apiV1 { - signingCertPath := filepath.Join(globalCertsDir, "signing.pem") - signingKeyPath := filepath.Join(globalCertsDir, "signing-key.pem") - signingCert, err := os.ReadFile(signingCertPath) - if err != nil { - return fmt.Errorf("StartAdam: cannot load %s: %s", signingCertPath, err) - } - signingKey, err := os.ReadFile(signingKeyPath) - if err != nil { - return fmt.Errorf("StartAdam: cannot load %s: %s", signingKeyPath, err) - } - envs = append(envs, fmt.Sprintf("SIGNING_CERT=%s", signingCert)) - envs = append(envs, fmt.Sprintf("SIGNING_KEY=%s", signingKey)) - encryptCertPath := filepath.Join(globalCertsDir, "encrypt.pem") - encryptKeyPath := filepath.Join(globalCertsDir, "encrypt-key.pem") - encryptCert, err := os.ReadFile(encryptCertPath) - if err != nil { - return fmt.Errorf("StartAdam: cannot load %s: %s", encryptCertPath, err) - } - encryptKey, err := os.ReadFile(encryptKeyPath) - if err != nil { - return fmt.Errorf("StartAdam: cannot load %s: %s", encryptKeyPath, err) - } - envs = append(envs, fmt.Sprintf("ENCRYPT_CERT=%s", encryptCert)) - envs = append(envs, fmt.Sprintf("ENCRYPT_KEY=%s", encryptKey)) - } portMap := map[string]string{"8080": strconv.Itoa(adamPort)} - volumeMap := map[string]string{"/adam/run": fmt.Sprintf("%s/run", adamPath)} - adamServerCommand := strings.Fields("server --conf-dir ./run/conf") + volumeMap := map[string]string{ + globalCertsDir: globalCertsDir, + } + + var adamServerCommand []string + if adamPath == "" { - volumeMap = map[string]string{"/adam/run": ""} + volumeMap["/adam/run"] = "" adamServerCommand = strings.Fields("server") + } else { + volumeMap["/adam/run"] = fmt.Sprintf("%s/run", adamPath) + adamServerCommand = strings.Fields("server --conf-dir ./run/conf") } if adamRemoteRedisURL != "" { redisPasswordFile := filepath.Join(globalCertsDir, defaults.DefaultRedisPasswordFile) @@ -189,10 +156,32 @@ func StartAdam(adamPort int, adamPath string, adamForce bool, adamTag string, ad } adamServerCommand = append(adamServerCommand, strings.Fields(fmt.Sprintf("--db-url %s", adamRemoteRedisURL))...) } + + serverCertPath := filepath.Join(globalCertsDir, "server.pem") + adamServerCommand = append(adamServerCommand, strings.Fields(fmt.Sprintf("--server-cert %s", serverCertPath))...) + + serverKeyPath := filepath.Join(globalCertsDir, "server-key.pem") + adamServerCommand = append(adamServerCommand, strings.Fields(fmt.Sprintf("--server-key %s", serverKeyPath))...) + + if !apiV1 { + signingCertPath := filepath.Join(globalCertsDir, "signing.pem") + adamServerCommand = append(adamServerCommand, strings.Fields(fmt.Sprintf("--signing-cert %s", signingCertPath))...) + + signingKeyPath := filepath.Join(globalCertsDir, "signing-key.pem") + adamServerCommand = append(adamServerCommand, strings.Fields(fmt.Sprintf("--signing-key %s", signingKeyPath))...) + + encryptCertPath := filepath.Join(globalCertsDir, "encrypt.pem") + adamServerCommand = append(adamServerCommand, strings.Fields(fmt.Sprintf("--encrypt-cert %s", encryptCertPath))...) + + encryptKeyPath := filepath.Join(globalCertsDir, "encrypt-key.pem") + adamServerCommand = append(adamServerCommand, strings.Fields(fmt.Sprintf("--encrypt-key %s", encryptKeyPath))...) + } + adamServerCommand = append(adamServerCommand, opts...) + if adamForce { _ = utils.StopContainer(defaults.DefaultAdamContainerName, true) - if err := utils.CreateAndRunContainer(defaults.DefaultAdamContainerName, defaults.DefaultAdamContainerRef+":"+adamTag, portMap, volumeMap, adamServerCommand, envs); err != nil { + if err := utils.CreateAndRunContainer(defaults.DefaultAdamContainerName, defaults.DefaultAdamContainerRef+":"+adamTag, portMap, volumeMap, adamServerCommand, nil); err != nil { return fmt.Errorf("StartAdam: error in create adam container: %s", err) } } else { @@ -201,7 +190,7 @@ func StartAdam(adamPort int, adamPath string, adamForce bool, adamTag string, ad return fmt.Errorf("StartAdam: error in get state of adam container: %s", err) } if state == "" { - if err := utils.CreateAndRunContainer(defaults.DefaultAdamContainerName, defaults.DefaultAdamContainerRef+":"+adamTag, portMap, volumeMap, adamServerCommand, envs); err != nil { + if err := utils.CreateAndRunContainer(defaults.DefaultAdamContainerName, defaults.DefaultAdamContainerRef+":"+adamTag, portMap, volumeMap, adamServerCommand, nil); err != nil { return fmt.Errorf("StartAdam: error in create adam container: %s", err) } } else if !strings.Contains(state, "running") { diff --git a/pkg/expect/encrypt.go b/pkg/expect/encrypt.go index 9553e1abc..c03d751fa 100644 --- a/pkg/expect/encrypt.go +++ b/pkg/expect/encrypt.go @@ -1,16 +1,13 @@ package expect import ( - "bytes" "encoding/base64" - "encoding/json" "fmt" + "os" "path/filepath" - "github.com/lf-edge/eden/pkg/controller/types" "github.com/lf-edge/eden/pkg/defaults" "github.com/lf-edge/eden/pkg/utils" - "github.com/lf-edge/eve-api/go/certs" "github.com/lf-edge/eve-api/go/config" "github.com/lf-edge/eve-api/go/evecommon" log "github.com/sirupsen/logrus" @@ -53,51 +50,41 @@ func (exp *AppExpectation) applyDatastoreCipher(datastoreConfig *config.Datastor } func (exp *AppExpectation) prepareCipherData(encBlock *evecommon.EncryptionBlock) (*evecommon.CipherBlock, error) { - attestData, err := exp.ctrl.CertsGet(exp.device.GetID()) + // get device certificate from the controller + devCert, err := exp.ctrl.GetECDHCert(exp.device.GetID()) if err != nil { - log.Errorf("cannot get attestation certificates from cloud for %s will use plaintext", exp.device.GetID()) + log.Errorf("cannot get device certificate from cloud. will use plaintext. error: %s", err) return nil, nil } - edenHome, err := utils.DefaultEdenDir() + + // get signing certificate from the controller + signCert, err := exp.ctrl.SigningCertGet() if err != nil { - return nil, fmt.Errorf("DefaultEdenDir: %s", err) - } - req := &types.Zcerts{} - if err := json.Unmarshal([]byte(attestData), req); err != nil { - return nil, fmt.Errorf("cannot unmarshal attest: %v", err) - } - var cert []byte - for _, c := range req.Certs { - if c.Type == certs.ZCertType_CERT_TYPE_DEVICE_ECDH_EXCHANGE { - cert = c.Cert - } - } - if len(cert) == 0 { - return nil, fmt.Errorf("no DEVICE_ECDH_EXCHANGE certificate") + log.Errorf("cannot get cloud's signing certificate. will use plaintext. error: %s", err) + return nil, nil } - globalCertsDir := filepath.Join(edenHome, defaults.DefaultCertsDist) - cryptoConfig, err := utils.GetCommonCryptoConfig(cert, filepath.Join(globalCertsDir, "signing.pem"), filepath.Join(globalCertsDir, "signing-key.pem")) + + edenHome, err := utils.DefaultEdenDir() if err != nil { - return nil, fmt.Errorf("GetCommonCryptoConfig: %v", err) + return nil, fmt.Errorf("DefaultEdenDir: %w", err) } - cipherCtx, err := utils.CreateCipherCtx(cryptoConfig) + keyPath := filepath.Join(edenHome, defaults.DefaultCertsDist, "signing-key.pem") + ctrlPrivKey, err := os.ReadFile(keyPath) if err != nil { - return nil, fmt.Errorf("CreateCipherCtx: %v", err) - } - appendCipherCtx := true - for _, c := range exp.device.GetCipherContexts() { - // we do not change controller certificates - if bytes.Equal(c.DeviceCertHash, cipherCtx.DeviceCertHash) { - cipherCtx = c - appendCipherCtx = false - } + return nil, fmt.Errorf("cannot read %s: %w", keyPath, err) } - if appendCipherCtx { - exp.device.SetCipherContexts(append(exp.device.GetCipherContexts(), cipherCtx)) + + cryptoConfig, err := utils.GetCommonCryptoConfig(devCert, signCert, ctrlPrivKey) + if err != nil { + return nil, fmt.Errorf("GetCommonCryptoConfig: %w", err) } - cipherBlock, err := utils.CryptoConfigWrapper(encBlock, cryptoConfig, cipherCtx) + cipherCtx, err := utils.CreateCipherCtx(cryptoConfig) if err != nil { - return nil, fmt.Errorf("CryptoConfigWrapper: %v", err) + return nil, fmt.Errorf("CreateCipherCtx: %w", err) } - return cipherBlock, nil + + // add cipher context to device or return a matching existing one + cipherCtx = utils.AddCipherCtxToDev(exp.device, cipherCtx) + + return utils.CryptoConfigWrapper(encBlock, cryptoConfig, cipherCtx) } diff --git a/pkg/openevec/adam.go b/pkg/openevec/adam.go index ba4be015b..bfe6bd260 100644 --- a/pkg/openevec/adam.go +++ b/pkg/openevec/adam.go @@ -3,11 +3,17 @@ package openevec import ( "fmt" "os" + "path/filepath" + "github.com/lf-edge/eden/pkg/controller" + "github.com/lf-edge/eden/pkg/defaults" + "github.com/lf-edge/eden/pkg/device" "github.com/lf-edge/eden/pkg/eden" + "github.com/lf-edge/eden/pkg/utils" log "github.com/sirupsen/logrus" ) +// AdamStart starts the OpenEVEC controller. func (openEVEC *OpenEVEC) AdamStart() error { cfg := openEVEC.cfg command, err := os.Executable() @@ -25,3 +31,118 @@ func (openEVEC *OpenEVEC) AdamStart() error { } return nil } + +// ChangeSigningCert uploads the provided signing certificate to the OpenEVEC controller. +func (openEVEC *OpenEVEC) ChangeSigningCert(newSignCert []byte) error { + changer := &adamChanger{} + ctrl, dev, err := changer.getControllerAndDevFromConfig(openEVEC.cfg) + if err != nil { + return fmt.Errorf("getControllerAndDevFromConfig: %w", err) + } + + // we need to re-encrypt existing configs with the new certificate because EVE has support only for one server signing certificate + err = reencryptConfigs(ctrl, dev, newSignCert) + if err != nil { + return fmt.Errorf("failed to reencrypt existing configs: %w", err) + } + + if err = changer.setControllerAndDev(ctrl, dev); err != nil { + return fmt.Errorf("setControllerAndDev: %w", err) + } + + edenHome, err := utils.DefaultEdenDir() + if err != nil { + return err + } + globalCertsDir := filepath.Join(edenHome, defaults.DefaultCertsDist) + signingCertPath := filepath.Join(globalCertsDir, "signing.pem") + + if err = os.WriteFile(signingCertPath, newSignCert, 0644); err != nil { + return fmt.Errorf("cannot write signing cert to %s: %w", signingCertPath, err) + } + + log.Infof("Signing cert changed successfully") + return nil +} + +func reencryptConfigs(ctrl controller.Cloud, dev *device.Ctx, newSignCert []byte) error { + // get device certificate from the controller + devCert, err := ctrl.GetECDHCert(dev.GetID()) + if err != nil { + return fmt.Errorf("cannot get device certificate from cloud: %w", err) + } + + // get signing certificate from the controller + oldSignCert, err := ctrl.SigningCertGet() + if err != nil { + log.Error("cannot get cloud's signing certificate. will use plaintext") + return nil + } + + edenHome, err := utils.DefaultEdenDir() + if err != nil { + return fmt.Errorf("DefaultEdenDir: %w", err) + } + keyPath := filepath.Join(edenHome, defaults.DefaultCertsDist, "signing-key.pem") + ctrlPrivKey, err := os.ReadFile(keyPath) + if err != nil { + return fmt.Errorf("cannot read %s: %w", keyPath, err) + } + + oldCryptoConfig, err := utils.GetCommonCryptoConfig(devCert, oldSignCert, ctrlPrivKey) + if err != nil { + return fmt.Errorf("GetCommonCryptoConfig: %w", err) + } + + newCryptoConfig, err := utils.GetCommonCryptoConfig(devCert, newSignCert, ctrlPrivKey) + if err != nil { + return fmt.Errorf("GetCommonCryptoConfig: %w", err) + } + + cipherCtx, err := utils.CreateCipherCtx(newCryptoConfig) + if err != nil { + return fmt.Errorf("CreateCipherCtx: %w", err) + } + // add cipher context to device or return a matching existing one + cipherCtx = utils.AddCipherCtxToDev(dev, cipherCtx) + + // re-encrypt all app configs with the new signing certificate + appConfigs := ctrl.ListApplicationInstanceConfig() + for _, config := range appConfigs { + if err = utils.ReencryptConfigData(config, oldCryptoConfig, newCryptoConfig, cipherCtx); err != nil { + return fmt.Errorf("reencryptConfigData: %w", err) + } + } + + // re-encrypt all datastore configs with the new signing certificate + dsConfigs := ctrl.ListDataStore() + for _, config := range dsConfigs { + if err = utils.ReencryptConfigData(config, oldCryptoConfig, newCryptoConfig, cipherCtx); err != nil { + return fmt.Errorf("reencryptConfigData: %w", err) + } + } + + // re-encrypt all wireless configs with the new signing certificate + for _, networkConfigID := range dev.GetNetworks() { + networkConfig, err := ctrl.GetNetworkConfig(networkConfigID) + if err != nil { + return fmt.Errorf("GetNetworkConfig: %w", err) + } + if networkConfig != nil && networkConfig.Wireless != nil { + for _, config := range networkConfig.Wireless.CellularCfg { + for _, ap := range config.AccessPoints { + if err = utils.ReencryptConfigData(ap, oldCryptoConfig, newCryptoConfig, cipherCtx); err != nil { + return fmt.Errorf("reencryptConfigData: %w", err) + } + } + } + for _, config := range networkConfig.Wireless.WifiCfg { + if err = utils.ReencryptConfigData(config, oldCryptoConfig, newCryptoConfig, cipherCtx); err != nil { + return fmt.Errorf("reencryptConfigData: %w", err) + } + } + } + } + + return nil +} diff --git a/pkg/utils/cipher.go b/pkg/utils/cipher.go index c7ba92a88..2d05833a5 100644 --- a/pkg/utils/cipher.go +++ b/pkg/utils/cipher.go @@ -34,7 +34,7 @@ func createCipherBlock(plainText []byte, cipherCtxID string, cmnCryptoCfg *Commo cipherBlock.ClearTextSha256 = shaOfPlainTextSecret[:] cipherBlock.InitialValue = iv - //encrypt paintext secret using symmetric key and initial value. + // encrypt paintext secret using symmetric key and initial value. cipherText, ecErr := aesEncrypt(iv, cmnCryptoCfg.SymmetricKey, plainText) if ecErr != nil { return nil, ecErr @@ -55,12 +55,87 @@ func CryptoConfigWrapper(encBlock *evecommon.EncryptionBlock, cmnCryptoCfg *Comm mEncBlock, mErr := proto.Marshal(encBlock) if mErr != nil { - return nil, fmt.Errorf("error marshalling user data: %v", mErr) + return nil, fmt.Errorf("error marshalling user data: %w", mErr) } - //Fill CipherBlock. + // Fill CipherBlock. cipherBlock, cbErr := createCipherBlock(mEncBlock, cipherCtx.ContextId, cmnCryptoCfg, iv[:16]) if cbErr != nil { - return nil, fmt.Errorf("error creating cipher block: %v", cbErr) + return nil, fmt.Errorf("error creating cipher block: %w", cbErr) } return cipherBlock, nil } + +// internal decryption method +func aesDecrypt(iv, symmetricKey, ciphertext []byte) ([]byte, error) { + plaintext := make([]byte, len(ciphertext)) + aesBlockDecrypter, err := aes.NewCipher(symmetricKey) + if err != nil { + return nil, err + } + aesDecrypter := cipher.NewCFBDecrypter(aesBlockDecrypter, iv) + aesDecrypter.XORKeyStream(plaintext, ciphertext) + return plaintext, nil +} + +// reverse the process of creating a cipher block +func decryptCipherBlock(cipherBlock *evecommon.CipherBlock, cmnCryptoCfg *CommonCryptoConfig) ([]byte, error) { + if cipherBlock == nil { + return nil, fmt.Errorf("nil cipher block in decrypt cipher block method") + } + + // decrypt ciphertext using symmetric key and initial value + decryptedText, decErr := aesDecrypt(cipherBlock.InitialValue, cmnCryptoCfg.SymmetricKey, cipherBlock.CipherData) + if decErr != nil { + return nil, decErr + } + + // verify sha256 checksum of decrypted text + shaOfDecryptedText := sha256.Sum256(decryptedText) + if !CompareSlices(shaOfDecryptedText[:], cipherBlock.ClearTextSha256) { + return nil, fmt.Errorf("decrypted text does not match original sha256 checksum") + } + + return decryptedText, nil +} + +// CryptoConfigUnwrapper reverses the process of CryptoConfigWrapper +func CryptoConfigUnwrapper(cipherBlock *evecommon.CipherBlock, cmnCryptoCfg *CommonCryptoConfig) (*evecommon.EncryptionBlock, error) { + decryptedBytes, err := decryptCipherBlock(cipherBlock, cmnCryptoCfg) + if err != nil { + return nil, fmt.Errorf("error decrypting cipher block: %w", err) + } + + var encBlock evecommon.EncryptionBlock + if err := proto.Unmarshal(decryptedBytes, &encBlock); err != nil { + return nil, fmt.Errorf("error unmarshalling decrypted bytes: %w", err) + } + + return &encBlock, nil +} + +// CipherDataHolder is an interface for objects that have CipherData field. +type CipherDataHolder interface { + GetCipherData() *evecommon.CipherBlock +} + +// ReencryptConfigData re-encrypts config data with new crypto config. +func ReencryptConfigData(holder CipherDataHolder, oldCryptoConfig, newCryptoConfig *CommonCryptoConfig, cipherCtx *evecommon.CipherContext) error { + if cipherData := holder.GetCipherData(); cipherData != nil { + encBlock, err := CryptoConfigUnwrapper(cipherData, oldCryptoConfig) + if err != nil { + return fmt.Errorf("CryptoConfigUnwrapper error: %w", err) + } + + newCipherData, err := CryptoConfigWrapper(encBlock, newCryptoConfig, cipherCtx) + if err != nil { + return fmt.Errorf("CryptoConfigWrapper error: %w", err) + } + + // copy each field separately to avoid copying the lock in MessageState + cipherData.CipherContextId = newCipherData.CipherContextId + cipherData.InitialValue = newCipherData.InitialValue + cipherData.CipherData = newCipherData.CipherData + cipherData.ClearTextSha256 = newCipherData.ClearTextSha256 + } + return nil +} diff --git a/pkg/utils/cryptoConfig.go b/pkg/utils/cryptoConfig.go index e727a2f9c..8b84043bf 100644 --- a/pkg/utils/cryptoConfig.go +++ b/pkg/utils/cryptoConfig.go @@ -3,10 +3,10 @@ package utils import ( "crypto/sha256" "fmt" - "os" "strings" "github.com/google/uuid" + "github.com/lf-edge/eden/pkg/device" "github.com/lf-edge/eve-api/go/evecommon" ) @@ -23,26 +23,16 @@ type CommonCryptoConfig struct { // 1. Calculate sha of controller cert. // 2. Calculate sha of device cert. // 3. Calculate symmetric key. -func GetCommonCryptoConfig(devCert []byte, controllerCert, controllerKey string) (*CommonCryptoConfig, error) { - ctrlEncCert, rErr := os.ReadFile(controllerCert) - if rErr != nil { - return nil, rErr - } - //first trim space from controller cert before calculating hash. - strCtrlEncCert := string(ctrlEncCert) +func GetCommonCryptoConfig(devCert, signCert, controllerKey []byte) (*CommonCryptoConfig, error) { + // first trim space from controller cert before calculating hash. + strCtrlEncCert := string(signCert) controllerEncCertSha := sha256.Sum256([]byte(strings.TrimSpace(strCtrlEncCert))) - //calculate sha256 of devCert. + // calculate sha256 of devCert. devCertSha := sha256.Sum256(devCert) - //read controller encryption priv key and - //use it for computing symmetric key. - ctrlPrivKey, rErr := os.ReadFile(controllerKey) - if rErr != nil { - return nil, rErr - } - //calculate symmetric key. - symmetricKey, syErr := calculateSymmetricKeyForEcdhAES(devCert, ctrlPrivKey) + // calculate symmetric key. + symmetricKey, syErr := calculateSymmetricKeyForEcdhAES(devCert, controllerKey) if syErr != nil { return nil, syErr } @@ -57,13 +47,13 @@ func GetCommonCryptoConfig(devCert []byte, controllerCert, controllerKey string) // CreateCipherCtx for edge dev config. func CreateCipherCtx(cmnCryptoCfg *CommonCryptoConfig) (*evecommon.CipherContext, error) { if cmnCryptoCfg.DevCertHash == nil { - return nil, fmt.Errorf("Empty device certificate in create cipher context method") + return nil, fmt.Errorf("empty device certificate in create cipher context method") } cipherCtx := &evecommon.CipherContext{} - //prepare ctx using controller and device cert hash. - //append device cert has and controller cert hash. + // prepare ctx using controller and device cert hash. + // append device cert has and controller cert hash. var uid uuid.UUID appendedHash := append(cmnCryptoCfg.ControllerEncCertHash[:16], cmnCryptoCfg.DevCertHash[:16]...) ctxID := uuid.NewSHA1(uid, appendedHash) @@ -78,3 +68,19 @@ func CreateCipherCtx(cmnCryptoCfg *CommonCryptoConfig) (*evecommon.CipherContext return cipherCtx, nil } + +// AddCipherCtxToDev add cipher context to device, unless it already exists. +// It returns the existing or the added cipher context. +func AddCipherCtxToDev(dev *device.Ctx, cipherCtx *evecommon.CipherContext) *evecommon.CipherContext { + // check if we already have cipherCtx with the same certificates + for _, c := range dev.GetCipherContexts() { + sameCipherCtx := CompareSlices(c.DeviceCertHash, cipherCtx.DeviceCertHash) && + CompareSlices(c.ControllerCertHash, cipherCtx.ControllerCertHash) + if sameCipherCtx { + return c + } + } + + dev.SetCipherContexts(append(dev.GetCipherContexts(), cipherCtx)) + return cipherCtx +} diff --git a/pkg/utils/slices.go b/pkg/utils/slices.go index 6c45c1e60..7e3f44651 100644 --- a/pkg/utils/slices.go +++ b/pkg/utils/slices.go @@ -3,8 +3,8 @@ package utils import "reflect" // DelEleInSlice delete an element from slice by index -// - arr: the reference of slice -// - index: the index of element will be deleted +// - arr: the reference of slice +// - index: the index of element will be deleted func DelEleInSlice(arr interface{}, index int) { vField := reflect.ValueOf(arr) value := vField.Elem() @@ -15,8 +15,8 @@ func DelEleInSlice(arr interface{}, index int) { } // DelEleInSliceByFunction delete an element from slice by function -// - arr: the reference of slice -// - f: delete if it returns true on element of slice +// - arr: the reference of slice +// - f: delete if it returns true on element of slice func DelEleInSliceByFunction(arr interface{}, f func(interface{}) bool) { vField := reflect.ValueOf(arr) value := vField.Elem() @@ -41,3 +41,19 @@ func FindEleInSlice(slice []string, val string) (int, bool) { } return -1, false } + +// CompareSlices compares two slices of comparable types. It returns true if +// they are equal and false otherwise. +func CompareSlices[T comparable](lhs, rhs []T) bool { + if len(lhs) != len(rhs) { + return false + } + + for i := range lhs { + if lhs[i] != rhs[i] { + return false + } + } + + return true +} diff --git a/pkg/utils/x509.go b/pkg/utils/x509.go index 29405588a..7d70edab7 100644 --- a/pkg/utils/x509.go +++ b/pkg/utils/x509.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "os" + "path/filepath" "time" "crypto/ecdsa" @@ -106,6 +107,64 @@ func GenServerCertElliptic(cert *x509.Certificate, key *rsa.PrivateKey, serial * } +// GenServerCertFromPrevCertAndKey generate new signing certificate for the controller using the same signing key and saves it to give path +func GenServerCertFromPrevCertAndKey(writePath string) error { + edenHome, err := DefaultEdenDir() + if err != nil { + return err + } + + // Read root cert + rootCert, err := ParseCertificate(filepath.Join(edenHome, defaults.DefaultCertsDist, "root-certificate.pem")) + if err != nil { + return err + } + + // Read root key + rootKey, err := ParsePrivateKey(filepath.Join(edenHome, defaults.DefaultCertsDist, "root-certificate-key.pem")) + if err != nil { + return err + } + + // Read server cert + oldServerCert, err := ParseCertificate(filepath.Join(edenHome, defaults.DefaultCertsDist, "signing.pem")) + if err != nil { + return err + } + + // Read ecdsa server key + serverKeyBytes, err := os.ReadFile(filepath.Join(edenHome, defaults.DefaultCertsDist, "signing-key.pem")) + if err != nil { + return err + } + var serverKey *ecdsa.PrivateKey + for block, rest := pem.Decode(serverKeyBytes); block != nil; block, rest = pem.Decode(rest) { + if block.Type == "EC PRIVATE KEY" { + serverKey, err = x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return err + } + break + } + } + + // keep all the same except for dates + serverTemplate := *oldServerCert + serverTemplate.NotBefore = time.Now().Add(-10 * time.Second) + serverTemplate.NotAfter = time.Now().AddDate(10, 0, 0) + + // create new certificate and write it to file + serverCert := genCertECDSA(&serverTemplate, rootCert, &serverKey.PublicKey, rootKey) + certOut, err := os.Create(writePath) + if err != nil { + return err + } + if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: serverCert.Raw}); err != nil { + return err + } + return certOut.Close() +} + // WriteToFiles write cert and key func WriteToFiles(crt *x509.Certificate, key interface{}, certFile string, keyFile string) (err error) { certOut, err := os.Create(certFile) diff --git a/tests/eclient/testdata/ctrl_cert_change.txt b/tests/eclient/testdata/ctrl_cert_change.txt new file mode 100644 index 000000000..d4718a622 --- /dev/null +++ b/tests/eclient/testdata/ctrl_cert_change.txt @@ -0,0 +1,77 @@ +# Test of controller certificate change +# This test validates the re-encryption of an application's user data +# following a change in the controller's certificate, accompanied by an edge node reboot. +# The test involves deploying three applications to make sure the config is (re)applied to all of them. + +{{$port := "2223"}} + +{{$userdata := "variable=value"}} +{{define "eclient_image"}}docker://{{EdenConfig "eden.eclient.image"}}:{{EdenConfig "eden.eclient.tag"}}{{end}} + +[!exec:bash] stop +[!exec:sleep] stop +[!exec:chmod] stop + +exec chmod 600 {{EdenConfig "eden.tests"}}/eclient/image/cert/id_rsa + +eden network create 10.11.12.0/24 -n n1 +eden pod deploy -n eclient1 --memory=512MB --networks=n1 {{template "eclient_image"}} -p {{$port}}:22 --metadata={{$userdata}} + +test eden.app.test -test.v -timewait 20m RUNNING eclient1 + +# generate new controller certificate +eden utils gen-signing-cert -o /tmp/signing-new.pem + +# upload new certificate to controller, resign old config and reapply it +eden adam change-signing-cert --cert-file /tmp/signing-new.pem + +# wait for changes to be applied +test eden.lim.test -test.v -timewait 15m -test.run TestLog -out content 'content:Rebuilding.intended.global.config,.reasons:.reconnecting.app' + +eden pod deploy -n eclient2 --memory=512MB --networks=n1 {{template "eclient_image"}} --metadata={{$userdata}} + +test eden.app.test -test.v -timewait 20m RUNNING eclient2 + +# check EVE got the new signing certificate +exec -t 2m bash check_sign_cert.sh + +# send reboot command and wait in background +test eden.reboot.test -test.v -timewait=20m -reboot=1 -count=1 & + +# wait for RUNNING state after reboot +test eden.app.test -test.v -timewait 20m -check-new RUNNING eclient1 +test eden.app.test -test.v -timewait 20m -check-new RUNNING eclient2 + +eden pod deploy -n eclient3 --memory=512MB --networks=n1 {{template "eclient_image"}} --metadata={{$userdata}} + +# check all apps are RUNNING + +test eden.app.test -test.v -timewait 20m RUNNING eclient1 +test eden.app.test -test.v -timewait 20m RUNNING eclient2 +test eden.app.test -test.v -timewait 20m RUNNING eclient3 + +# cleanup +eden pod delete eclient1 +eden pod delete eclient2 +eden pod delete eclient3 +eden network delete n1 + +test eden.app.test -test.v -timewait 10m - eclient1 +test eden.app.test -test.v -timewait 10m - eclient2 +test eden.app.test -test.v -timewait 10m - eclient3 +test eden.network.test -test.v -timewait 10m - n1 + +-- eden-config.yml -- +{{/* Test's config. file */}} +test: + controller: adam://{{EdenConfig "adam.ip"}}:{{EdenConfig "adam.port"}} + eve: + {{EdenConfig "eve.name"}}: + onboard-cert: {{EdenConfigPath "eve.cert"}} + serial: "{{EdenConfig "eve.serial"}}" + model: {{EdenConfig "eve.devmodel"}} + +-- check_sign_cert.sh -- +EDEN={{EdenConfig "eden.root"}}/{{EdenConfig "eden.bin-dist"}}/{{EdenConfig "eden.eden-bin"}} +$EDEN eve ssh cat /persist/certs/server-signing-cert.pem > /tmp/server-signing-cert.pem +diff -Z /tmp/signing-new.pem /tmp/server-signing-cert.pem diff --git a/tests/workflow/smoke.tests.txt b/tests/workflow/smoke.tests.txt index 717433a4e..4e7790b10 100644 --- a/tests/workflow/smoke.tests.txt +++ b/tests/workflow/smoke.tests.txt @@ -1,5 +1,5 @@ # Number of tests -{{$tests := 22}} +{{$tests := 23}} # EDEN_TEST_SETUP env. var. -- "y"(default) performs the EDEN setup steps {{$setup := "y"}} {{$setup_env := EdenGetEnv "EDEN_TEST_SETUP"}} @@ -70,12 +70,14 @@ eden.escript.test -testdata ../eclient/testdata/ -test.run TestEdenScripts/metad eden.escript.test -testdata ../eclient/testdata/ -test.run TestEdenScripts/userdata /bin/echo Eden app log test (19/{{$tests}}) eden.escript.test -testdata ../eclient/testdata/ -test.run TestEdenScripts/app_logs +/bin/echo Eden change controller certificate test (20/{{$tests}}) +eden.escript.test -testdata ../eclient/testdata/ -test.run TestEdenScripts/ctrl_cert_change -/bin/echo Eden Shutdown test (20/{{$tests}}) +/bin/echo Eden Shutdown test (21/{{$tests}}) eden.escript.test -testdata ../eclient/testdata/ -test.run TestEdenScripts/shutdown_test -/bin/echo EVE reset (21/{{$tests}}) +/bin/echo EVE reset (22/{{$tests}}) eden.escript.test -test.run TestEdenScripts/eden_reset -/bin/echo EVE security tests (22/{{$tests}}) +/bin/echo EVE security tests (23/{{$tests}}) eden.escript.test -test.run TestEdenScripts/sec_eden