Skip to content

Commit

Permalink
Encrypted SoftwareUpdatable v2 artifacts support
Browse files Browse the repository at this point in the history
Signed-off-by: Kristiyan Gostev <[email protected]>
  • Loading branch information
k-gostev committed Apr 10, 2024
1 parent 8881b9e commit cfb09f3
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 39 deletions.
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

0 comments on commit cfb09f3

Please sign in to comment.