Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encrypted SoftwareUpdatable v2 artifacts support #70

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion internal/feature_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ func (f *ScriptBasedSoftwareUpdatable) installModule(
setLastOS(su, newOS(cid, module, hawkbit.StatusStarted))
storage.WriteLn(s, string(hawkbit.StatusStarted))
Started:

// Downloading
logger.Debugf("[%s.%s] Downloading module", module.Name, module.Version)
setLastOS(su, newOS(cid, module, hawkbit.StatusDownloading))
Expand Down
16 changes: 10 additions & 6 deletions internal/storage/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"fmt"
"hash"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
Expand All @@ -39,8 +38,7 @@ const prefix = "_temporary-"
var secureCiphers = supportedCipherSuites()

// downloadArtifact tries to resume previous download operation or perform a new download.
func downloadArtifact(to string, artifact *Artifact, progress progressBytes, serverCert string, retryCount int, retryInterval time.Duration,
done chan struct{}) error {
func downloadArtifact(to string, artifact *Artifact, progress progressBytes, serverCert string, retryCount int, retryInterval time.Duration, postProcess func(fileName string) error, done chan struct{}) error {
logger.Infof("download [%s] to file [%s]", artifact.Link, to)

// Check for available file.
Expand Down Expand Up @@ -95,6 +93,12 @@ func downloadArtifact(to string, artifact *Artifact, progress progressBytes, ser
}
}

if postProcess != nil {
if err := postProcess(tmp); err != nil {
return err
}
}

// Rename to the original file name.
return os.Rename(tmp, to)
}
Expand Down Expand Up @@ -143,7 +147,7 @@ func resume(to string, offset int64, artifact *Artifact, progress progressBytes,

func downloadFile(file *os.File, input io.ReadCloser, to string, offset int64, artifact *Artifact,
progress progressBytes, serverCert string, retryCount int, retryInterval time.Duration, done chan struct{}) (int64, error) {
w, err := copy(file, input, int64(artifact.Size)-offset, progress, done)
w, err := copyWithProgress(file, input, int64(artifact.Size)-offset, progress, done)
if err == nil {
err = validate(to, artifact.HashType, artifact.HashValue)
offset = 0 // in case of error, re-download the file
Expand Down Expand Up @@ -237,7 +241,7 @@ func requestDownload(link string, offset int64, serverCert string) (*http.Respon
var transport http.Transport
var caCertPool *x509.CertPool
if len(serverCert) > 0 {
caCert, err := ioutil.ReadFile(serverCert)
caCert, err := os.ReadFile(serverCert)
if err != nil {
return nil, fmt.Errorf("error reading CA certificate file - \"%s\"", serverCert)
}
Expand Down Expand Up @@ -275,7 +279,7 @@ func download(to string, in io.ReadCloser, artifact *Artifact, progress progress
return downloadFile(file, in, to, 0, artifact, progress, serverCert, retryCount, retryInterval, done)
}

func copy(dst io.Writer, src io.Reader, size int64, progress progressBytes, done chan struct{}) (w int64, err error) {
func copyWithProgress(dst io.Writer, src io.Reader, size int64, progress progressBytes, done chan struct{}) (w int64, err error) {
buf := make([]byte, 32*1024)
for {
select {
Expand Down
54 changes: 27 additions & 27 deletions internal/storage/download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,22 +119,22 @@ func TestDownloadToFile(t *testing.T) {
HashType: "SHA256",
HashValue: "4eefb9a7a40a8b314b586a00f307157043c0bbe4f59fa39cba88773680758bc3",
},
}, "", "", t)
}, "", t)
}

// TestDownloadToFileSecureSystemPool tests downloadToFile function, using secure protocol(s) and certificates from system pool.
func TestDownloadToFileSecureSystemPool(t *testing.T) {
setSSLCerts(t)
defer unsetSSLCerts(t)
testDownloadToFileSecure("", "", t)
testDownloadToFileSecure("", t)
}

// TestDownloadToFileSecureCustomCertificate tests downloadToFile function, using secure protocol(s) and a custom certificate.
func TestDownloadToFileSecureCustomCertificate(t *testing.T) {
testDownloadToFileSecure(validCert, validKey, t)
testDownloadToFileSecure(validCert, t)
}

func testDownloadToFileSecure(certFile, certKey string, t *testing.T) {
func testDownloadToFileSecure(certFile string, t *testing.T) {
testDownloadToFile([]*Artifact{
{ // An Artifact with MD5 checksum.
FileName: "test.txt", Size: 65536, Link: "https://localhost:43234/test.txt",
Expand All @@ -151,10 +151,10 @@ func testDownloadToFileSecure(certFile, certKey string, t *testing.T) {
HashType: "SHA256",
HashValue: "4eefb9a7a40a8b314b586a00f307157043c0bbe4f59fa39cba88773680758bc3",
},
}, certFile, certKey, t)
}, certFile, t)
}

func testDownloadToFile(arts []*Artifact, certFile, certKey string, t *testing.T) {
func testDownloadToFile(arts []*Artifact, certFile string, t *testing.T) {
for _, art := range arts {
t.Run(art.HashType, func(t *testing.T) {
// Prepare
Expand All @@ -174,7 +174,7 @@ func testDownloadToFile(arts []*Artifact, certFile, certKey string, t *testing.T

// 1. Resume download of corrupted temporary file.
WriteLn(filepath.Join(dir, prefix+art.FileName), "wrong start")
if err := downloadArtifact(name, art, nil, certFile, 0, 0, make(chan struct{})); err == nil {
if err := downloadArtifact(name, art, nil, certFile, 0, 0, nil, make(chan struct{})); err == nil {
t.Fatal("download of corrupted temporary file must fail")
}

Expand All @@ -183,7 +183,7 @@ func testDownloadToFile(arts []*Artifact, certFile, certKey string, t *testing.T
callback := func(bytes int64) {
close(done)
}
if err := downloadArtifact(name, art, callback, certFile, 0, 0, done); err != ErrCancel {
if err := downloadArtifact(name, art, callback, certFile, 0, 0, nil, done); err != ErrCancel {
t.Fatalf("failed to cancel download operation: %v", err)
}
if _, err := os.Stat(filepath.Join(dir, prefix+art.FileName)); os.IsNotExist(err) {
Expand All @@ -192,13 +192,13 @@ func testDownloadToFile(arts []*Artifact, certFile, certKey string, t *testing.T

// 3. Resume previous download operation.
callback = func(bytes int64) { /* Do nothing. */ }
if err := downloadArtifact(name, art, callback, certFile, 0, 0, make(chan struct{})); err != nil {
if err := downloadArtifact(name, art, callback, certFile, 0, 0, nil, make(chan struct{})); err != nil {
t.Fatalf("failed to download artifact: %v", err)
}
check(name, art.Size, t)

// 4. Download available file.
if err := downloadArtifact(name, art, callback, certFile, 0, 0, make(chan struct{})); err != nil {
if err := downloadArtifact(name, art, callback, certFile, 0, 0, nil, make(chan struct{})); err != nil {
t.Fatalf("failed to download artifact: %v", err)
}
check(name, art.Size, t)
Expand All @@ -211,14 +211,14 @@ func testDownloadToFile(arts []*Artifact, certFile, certKey string, t *testing.T
// 5. Try to resume with file bigger than expected.
WriteLn(filepath.Join(dir, prefix+art.FileName), "1111111111111")
art.Size -= 10
if err := downloadArtifact(name, art, nil, certFile, 0, 0, make(chan struct{})); err == nil {
if err := downloadArtifact(name, art, nil, certFile, 0, 0, nil, make(chan struct{})); err == nil {
t.Fatal("validate resume with file bigger than expected")
}

// 6. Try to resume from missing link.
WriteLn(filepath.Join(dir, prefix+art.FileName), "1111111111111")
art.Link = "http://localhost:43234/test-missing.txt"
if err := downloadArtifact(name, art, nil, "", 0, 0, make(chan struct{})); err == nil {
if err := downloadArtifact(name, art, nil, "", 0, 0, nil, make(chan struct{})); err == nil {
t.Fatal("failed to validate with missing link")
}

Expand Down Expand Up @@ -256,7 +256,7 @@ func TestDownloadToFileLocalLink(t *testing.T) {
HashValue: "4eefb9a7a40a8b314b586a00f307157043c0bbe4f59fa39cba88773680758bc3",
Local: true,
},
}, "", "", t)
}, "", t)
}

// TestDownloadToFileError tests downloadToFile function for some edge cases.
Expand Down Expand Up @@ -284,41 +284,41 @@ func TestDownloadToFileError(t *testing.T) {

// 1. Resume is not supported.
WriteLn(filepath.Join(dir, prefix+art.FileName), "1111")
if err := downloadArtifact(name, art, nil, "", 0, 0, make(chan struct{})); err != nil {
if err := downloadArtifact(name, art, nil, "", 0, 0, nil, make(chan struct{})); err != nil {
t.Fatalf("failed to download file artifact: %v", err)
}
check(name, art.Size, t)

// 2. Try with missing checksum.
art.HashValue = ""
if err := downloadArtifact(name, art, nil, "", 0, 0, make(chan struct{})); err == nil {
if err := downloadArtifact(name, art, nil, "", 0, 0, nil, make(chan struct{})); err == nil {
t.Fatal("validated with missing checksum")
}

// 3. Try with missing link.
art.Link = "http://localhost:43234/test-missing.txt"
if err := downloadArtifact(name, art, nil, "", 0, 0, make(chan struct{})); err == nil {
if err := downloadArtifact(name, art, nil, "", 0, 0, nil, make(chan struct{})); err == nil {
t.Fatal("failed to validate with missing link")
}

// 4. Try with wrong checksum type.
art.Link = "http://localhost:43234/test-simple.txt"
art.HashType = ""
if err := downloadArtifact(name, art, nil, "", 0, 0, make(chan struct{})); err == nil {
if err := downloadArtifact(name, art, nil, "", 0, 0, nil, make(chan struct{})); err == nil {
t.Fatal("validate with wrong checksum type")
}

// 5. Try with wrong checksum format.
art.HashValue = ";;"
if err := downloadArtifact(name, art, nil, "", 0, 0, make(chan struct{})); err == nil {
if err := downloadArtifact(name, art, nil, "", 0, 0, nil, make(chan struct{})); err == nil {
t.Fatal("validate with wrong checksum format")
}

// 6. Try to download file bigger than expected.
art.HashType = "MD5"
art.HashValue = "ab2ce340d36bbaafe17965a3a2c6ed5b"
art.Size -= 10
if err := downloadArtifact(name, art, nil, "", 0, 0, make(chan struct{})); err == nil {
if err := downloadArtifact(name, art, nil, "", 0, 0, nil, make(chan struct{})); err == nil {
t.Fatal("validate with file bigger than expected")
}

Expand Down Expand Up @@ -346,11 +346,11 @@ func TestRobustDownloadRetryBadStatus(t *testing.T) {

name := filepath.Join(dir, art.FileName)

if err := downloadArtifact(name, art, nil, "", 1, 0, make(chan struct{})); err == nil {
if err := downloadArtifact(name, art, nil, "", 1, 0, nil, make(chan struct{})); err == nil {
t.Fatal("error is expected when downloading artifact, due to bad response status")
}

if err := downloadArtifact(name, art, nil, "", 5, time.Second, make(chan struct{})); err != nil {
if err := downloadArtifact(name, art, nil, "", 5, time.Second, nil, make(chan struct{})); err != nil {
t.Fatal("expected to handle download error, by using retry download strategy")
}
check(name, art.Size, t)
Expand All @@ -359,7 +359,7 @@ func TestRobustDownloadRetryBadStatus(t *testing.T) {
t.Fatalf("failed to delete test file %s", name)
}
setIncorrectBehavior(2, false, false)
if err := downloadArtifact(name, art, nil, "", 0, 0, make(chan struct{})); err == nil {
if err := downloadArtifact(name, art, nil, "", 0, 0, nil, make(chan struct{})); err == nil {
t.Fatal("error is expected when downloading artifact, due to bad response status")
}
}
Expand Down Expand Up @@ -417,7 +417,7 @@ func testCopyError(withInsufficientRetryCount bool, withCorruptedFile bool, t *t
if withInsufficientRetryCount {
retryCount = 2
}
err := downloadArtifact(name, art, nil, "", retryCount, 2*time.Second, make(chan struct{}))
err := downloadArtifact(name, art, nil, "", retryCount, 2*time.Second, nil, make(chan struct{}))
if withInsufficientRetryCount {
if err == nil {
t.Fatal("error is expected when downloading artifact, due to copy error")
Expand Down Expand Up @@ -461,22 +461,22 @@ func TestDownloadToFileSecureError(t *testing.T) {

// 1. Server uses expired certificate
art.Link = "https://localhost:43234/test.txt"
if err := downloadArtifact(name, art, nil, "", 0, 0, make(chan struct{})); err == nil {
if err := downloadArtifact(name, art, nil, "", 0, 0, nil, make(chan struct{})); err == nil {
t.Fatalf("download must fail(client uses no certificate, server uses expired): %v", err)
}
if err := downloadArtifact(name, art, nil, expiredCert, 0, 0, make(chan struct{})); err == nil {
if err := downloadArtifact(name, art, nil, expiredCert, 0, 0, nil, make(chan struct{})); err == nil {
t.Fatalf("download must fail(client and server use expired certificate): %v", err)
}

// 2. Server uses untrusted certificate
art.Link = "https://localhost:43235/test.txt"
if err := downloadArtifact(name, art, nil, "", 0, 0, make(chan struct{})); err == nil {
if err := downloadArtifact(name, art, nil, "", 0, 0, nil, make(chan struct{})); err == nil {
t.Fatalf("download must fail(client uses no certificate, server uses untrusted): %v", err)
}

// 3. Server uses valid certificate
art.Link = "https://localhost:43236/test.txt"
if err := downloadArtifact(name, art, nil, untrustedCert, 0, 0, make(chan struct{})); err == nil {
if err := downloadArtifact(name, art, nil, untrustedCert, 0, 0, nil, make(chan struct{})); err == nil {
t.Fatalf("download must fail(client uses untrusted certificate, server uses valid): %v", err)
}
}
Expand Down
45 changes: 43 additions & 2 deletions internal/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
package storage

import (
"crypto/aes"
"crypto/cipher"
"errors"
"fmt"
"io/fs"
Expand All @@ -21,6 +23,7 @@ import (
"path/filepath"
"sort"
"strconv"
"strings"
"time"

"github.com/eclipse-kanto/software-update/hawkbit"
Expand Down Expand Up @@ -268,11 +271,49 @@ func (st *Storage) DownloadModule(toDir string, module *Module, progress Progres
continue
}
onlyLocalNoCopyArtifacts = false
if err = downloadArtifact(filepath.Join(toDir, sa.FileName), sa, callback, serverCert, retryCount,
retryInterval, st.done); err != nil {
var postProcess func(fileName string) error
if module.Metadata != nil && module.Metadata["AES256.key"] != "" {
var iv string
if iv = module.Metadata["AES256.iv"]; iv == "" {
return errors.New("AES256 key is provided, but initialization vector is missing. Only CBC encryption is supported")
}
postProcess = func(fileName string) error {
data, ppError := os.ReadFile(fileName)
if ppError != nil {
return ppError
}
format := module.Metadata["AES256.format"]
cipherTextDecoded, ppError := decodeString(format, strings.TrimSpace(string(data)))
if ppError != nil {
return fmt.Errorf("unable to decode artifact: %s", ppError)
}
encKeyDecoded, ppError := decodeString(format, module.Metadata["AES256.key"])
if ppError != nil {
return fmt.Errorf("unable to decode the provided key: %s", ppError)
}
ivDecoded, ppError := decodeString(format, iv)
if ppError != nil {
return fmt.Errorf("unable to decode the initialization vector (IV): %s", ppError)
}
block, ppError := aes.NewCipher([]byte(encKeyDecoded))
if ppError != nil {
return ppError
}
cipher.NewCBCDecrypter(block, []byte(ivDecoded)).CryptBlocks([]byte(cipherTextDecoded), []byte(cipherTextDecoded))
return os.WriteFile(fileName, cipherTextDecoded, 0755)

}
}
defer func() {
if r := recover(); r != nil { // the cipher.NewCBCDecrypter panics against all good practices...
err = fmt.Errorf("error during decryption %v", r)
}
}()
if err = downloadArtifact(filepath.Join(toDir, sa.FileName), sa, callback, serverCert, retryCount, retryInterval, postProcess, st.done); err != nil {
return err
}
}

if progress != nil && onlyLocalNoCopyArtifacts {
progress(100)
}
Expand Down
Loading
Loading