This is an automated email from the ASF dual-hosted git repository. ccollins pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 1e5f83d231a283d5d50342b13d30f250e0ea55a2 Author: Christopher Collins <ccoll...@apache.org> AuthorDate: Tue Nov 20 17:41:07 2018 -0800 "artifact" library This library subsumes functionality that is currently tied to the newt tool: Parsing, manipulation, and writing of artifacts that aren't described by a Mynewt project. Specifically: * `.img` file parsing and generation * Manifest file parsing --- artifact/flash/flash.go | 160 +++++++++++++ artifact/image/create.go | 427 +++++++++++++++++++++++++++++++++ artifact/image/encrypted.go | 196 +++++++++++++++ artifact/image/image.go | 544 ++++++++++++++++++++++++++++++++++++++++++ artifact/image/key.go | 294 +++++++++++++++++++++++ artifact/image/keys_test.go | 244 +++++++++++++++++++ artifact/image/v1.go | 487 +++++++++++++++++++++++++++++++++++++ artifact/manifest/manifest.go | 95 ++++++++ 8 files changed, 2447 insertions(+) diff --git a/artifact/flash/flash.go b/artifact/flash/flash.go new file mode 100644 index 0000000..c37d2dd --- /dev/null +++ b/artifact/flash/flash.go @@ -0,0 +1,160 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 flash + +import ( + "fmt" + "sort" +) + +const FLASH_AREA_NAME_BOOTLOADER = "FLASH_AREA_BOOTLOADER" +const FLASH_AREA_NAME_IMAGE_0 = "FLASH_AREA_IMAGE_0" +const FLASH_AREA_NAME_IMAGE_1 = "FLASH_AREA_IMAGE_1" +const FLASH_AREA_NAME_IMAGE_SCRATCH = "FLASH_AREA_IMAGE_SCRATCH" +const AREA_USER_ID_MIN = 16 + +var SYSTEM_AREA_NAME_ID_MAP = map[string]int{ + FLASH_AREA_NAME_BOOTLOADER: 0, + FLASH_AREA_NAME_IMAGE_0: 1, + FLASH_AREA_NAME_IMAGE_1: 2, + FLASH_AREA_NAME_IMAGE_SCRATCH: 3, +} + +type FlashArea struct { + Name string + Id int + Device int + Offset int + Size int +} + +type areaOffSorter struct { + areas []FlashArea +} + +func (s areaOffSorter) Len() int { + return len(s.areas) +} +func (s areaOffSorter) Swap(i, j int) { + s.areas[i], s.areas[j] = s.areas[j], s.areas[i] +} +func (s areaOffSorter) Less(i, j int) bool { + ai := s.areas[i] + aj := s.areas[j] + + if ai.Device < aj.Device { + return true + } + if ai.Device > aj.Device { + return false + } + return ai.Offset < aj.Offset +} + +func SortFlashAreasByDevOff(areas []FlashArea) []FlashArea { + sorter := areaOffSorter{ + areas: make([]FlashArea, len(areas)), + } + + for i, a := range areas { + sorter.areas[i] = a + } + + sort.Sort(sorter) + return sorter.areas +} + +func SortFlashAreasById(areas []FlashArea) []FlashArea { + idMap := make(map[int]FlashArea, len(areas)) + ids := make([]int, 0, len(areas)) + for _, area := range areas { + idMap[area.Id] = area + ids = append(ids, area.Id) + } + sort.Ints(ids) + + sorted := make([]FlashArea, len(ids)) + for i, id := range ids { + sorted[i] = idMap[id] + } + + return sorted +} + +func areasDistinct(a FlashArea, b FlashArea) bool { + var lo FlashArea + var hi FlashArea + + if a.Offset < b.Offset { + lo = a + hi = b + } else { + lo = b + hi = a + } + + return lo.Device != hi.Device || lo.Offset+lo.Size <= hi.Offset +} + +// @return overlapping-areas, id-conflicts. +func DetectErrors(areas []FlashArea) ([][]FlashArea, [][]FlashArea) { + var overlaps [][]FlashArea + var conflicts [][]FlashArea + + for i := 0; i < len(areas)-1; i++ { + iarea := areas[i] + for j := i + 1; j < len(areas); j++ { + jarea := areas[j] + + if !areasDistinct(iarea, jarea) { + overlaps = append(overlaps, []FlashArea{iarea, jarea}) + } + + if iarea.Id == jarea.Id { + conflicts = append(conflicts, []FlashArea{iarea, jarea}) + } + } + } + + return overlaps, conflicts +} + +func ErrorText(overlaps [][]FlashArea, conflicts [][]FlashArea) string { + str := "" + + if len(conflicts) > 0 { + str += "Conflicting flash area IDs detected:\n" + + for _, pair := range conflicts { + str += fmt.Sprintf(" (%d) %s =/= %s\n", + pair[0].Id-AREA_USER_ID_MIN, pair[0].Name, pair[1].Name) + } + } + + if len(overlaps) > 0 { + str += "Overlapping flash areas detected:\n" + + for _, pair := range overlaps { + str += fmt.Sprintf(" %s =/= %s\n", pair[0].Name, pair[1].Name) + } + } + + return str +} diff --git a/artifact/image/create.go b/artifact/image/create.go new file mode 100644 index 0000000..5b28120 --- /dev/null +++ b/artifact/image/create.go @@ -0,0 +1,427 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 image + +import ( + "bytes" + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/asn1" + "encoding/binary" + "encoding/hex" + "io" + "io/ioutil" + "math/big" + + "mynewt.apache.org/newt/util" +) + +type ImageCreator struct { + Body []byte + Version ImageVersion + SigKeys []ImageSigKey + PlainSecret []byte + CipherSecret []byte + HeaderSize int + InitialHash []byte + Bootable bool +} + +type ImageCreateOpts struct { + SrcBinFilename string + SrcEncKeyFilename string + Version ImageVersion + SigKeys []ImageSigKey + LoaderHash []byte +} + +type ECDSASig struct { + R *big.Int + S *big.Int +} + +func NewImageCreator() ImageCreator { + return ImageCreator{ + HeaderSize: IMAGE_HEADER_SIZE, + Bootable: true, + } +} + +func generateEncTlv(cipherSecret []byte) (ImageTlv, error) { + var encType uint8 + + if len(cipherSecret) == 256 { + encType = IMAGE_TLV_ENC_RSA + } else if len(cipherSecret) == 24 { + encType = IMAGE_TLV_ENC_KEK + } else { + return ImageTlv{}, util.FmtNewtError("Invalid enc TLV size ") + } + + return ImageTlv{ + Header: ImageTlvHdr{ + Type: encType, + Pad: 0, + Len: uint16(len(cipherSecret)), + }, + Data: cipherSecret, + }, nil +} + +func generateSigRsa(key *rsa.PrivateKey, hash []byte) ([]byte, error) { + opts := rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthEqualsHash, + } + signature, err := rsa.SignPSS( + rand.Reader, key, crypto.SHA256, hash, &opts) + if err != nil { + return nil, util.FmtNewtError("Failed to compute signature: %s", err) + } + + return signature, nil +} + +func generateSigTlvRsa(key ImageSigKey, hash []byte) (ImageTlv, error) { + sig, err := generateSigRsa(key.Rsa, hash) + if err != nil { + return ImageTlv{}, err + } + + return ImageTlv{ + Header: ImageTlvHdr{ + Type: key.sigTlvType(), + Pad: 0, + Len: 256, /* 2048 bits */ + }, + Data: sig, + }, nil +} + +func generateSigEc(key *ecdsa.PrivateKey, hash []byte) ([]byte, error) { + r, s, err := ecdsa.Sign(rand.Reader, key, hash) + if err != nil { + return nil, util.FmtNewtError("Failed to compute signature: %s", err) + } + + ECDSA := ECDSASig{ + R: r, + S: s, + } + + signature, err := asn1.Marshal(ECDSA) + if err != nil { + return nil, util.FmtNewtError("Failed to construct signature: %s", err) + } + + return signature, nil +} + +func generateSigTlvEc(key ImageSigKey, hash []byte) (ImageTlv, error) { + sig, err := generateSigEc(key.Ec, hash) + if err != nil { + return ImageTlv{}, err + } + + sigLen := key.sigLen() + if len(sig) > int(sigLen) { + return ImageTlv{}, util.FmtNewtError("Something is really wrong\n") + } + + b := &bytes.Buffer{} + + if _, err := b.Write(sig); err != nil { + return ImageTlv{}, + util.FmtNewtError("Failed to append sig: %s", err.Error()) + } + + pad := make([]byte, int(sigLen)-len(sig)) + if _, err := b.Write(pad); err != nil { + return ImageTlv{}, util.FmtNewtError( + "Failed to serialize image trailer: %s", err.Error()) + } + + return ImageTlv{ + Header: ImageTlvHdr{ + Type: key.sigTlvType(), + Pad: 0, + Len: sigLen, + }, + Data: b.Bytes(), + }, nil +} + +func generateSigTlv(key ImageSigKey, hash []byte) (ImageTlv, error) { + key.assertValid() + + if key.Rsa != nil { + return generateSigTlvRsa(key, hash) + } else { + return generateSigTlvEc(key, hash) + } +} + +func generateKeyHashTlv(key ImageSigKey) (ImageTlv, error) { + key.assertValid() + + keyHash, err := key.sigKeyHash() + if err != nil { + return ImageTlv{}, util.FmtNewtError( + "Failed to compute hash of the public key: %s", err.Error()) + } + + return ImageTlv{ + Header: ImageTlvHdr{ + Type: IMAGE_TLV_KEYHASH, + Pad: 0, + Len: uint16(len(keyHash)), + }, + Data: keyHash, + }, nil +} + +func GenerateSigTlvs(keys []ImageSigKey, hash []byte) ([]ImageTlv, error) { + var tlvs []ImageTlv + + for _, key := range keys { + key.assertValid() + + tlv, err := generateKeyHashTlv(key) + if err != nil { + return nil, err + } + tlvs = append(tlvs, tlv) + + tlv, err = generateSigTlv(key, hash) + if err != nil { + return nil, err + } + tlvs = append(tlvs, tlv) + } + + return tlvs, nil +} + +func GenerateImage(opts ImageCreateOpts) (Image, error) { + ic := NewImageCreator() + + srcBin, err := ioutil.ReadFile(opts.SrcBinFilename) + if err != nil { + return Image{}, util.FmtNewtError( + "Can't read app binary: %s", err.Error()) + } + + ic.Body = srcBin + ic.Version = opts.Version + ic.SigKeys = opts.SigKeys + + if opts.LoaderHash != nil { + ic.InitialHash = opts.LoaderHash + ic.Bootable = false + } else { + ic.Bootable = true + } + + if opts.SrcEncKeyFilename != "" { + plainSecret := make([]byte, 16) + if _, err := rand.Read(plainSecret); err != nil { + return Image{}, util.FmtNewtError( + "Random generation error: %s\n", err) + } + + cipherSecret, err := ReadEncKey(opts.SrcEncKeyFilename, plainSecret) + if err != nil { + return Image{}, err + } + + ic.PlainSecret = plainSecret + ic.CipherSecret = cipherSecret + } + + ri, err := ic.Create() + if err != nil { + return Image{}, err + } + + return ri, nil +} + +func calcHash(initialHash []byte, hdr ImageHdr, + plainBody []byte) ([]byte, error) { + + hash := sha256.New() + + add := func(itf interface{}) error { + b := &bytes.Buffer{} + if err := binary.Write(b, binary.LittleEndian, itf); err != nil { + return err + } + if err := binary.Write(hash, binary.LittleEndian, itf); err != nil { + return util.FmtNewtError("Failed to hash data: %s", err.Error()) + } + + return nil + } + + if initialHash != nil { + if err := add(initialHash); err != nil { + return nil, err + } + } + + if err := add(hdr); err != nil { + return nil, err + } + + extra := hdr.HdrSz - IMAGE_HEADER_SIZE + if extra > 0 { + b := make([]byte, extra) + if err := add(b); err != nil { + return nil, err + } + } + + if err := add(plainBody); err != nil { + return nil, err + } + + return hash.Sum(nil), nil +} + +func (ic *ImageCreator) Create() (Image, error) { + ri := Image{} + + // First the header + hdr := ImageHdr{ + Magic: IMAGE_MAGIC, + Pad1: 0, + HdrSz: IMAGE_HEADER_SIZE, + Pad2: 0, + ImgSz: uint32(len(ic.Body)), + Flags: 0, + Vers: ic.Version, + Pad3: 0, + } + + if !ic.Bootable { + hdr.Flags |= IMAGE_F_NON_BOOTABLE + } + + if ic.CipherSecret != nil { + hdr.Flags |= IMAGE_F_ENCRYPTED + } + + if ic.HeaderSize != 0 { + // Pad the header out to the given size. There will + // just be zeros between the header and the start of + // the image when it is padded. + extra := ic.HeaderSize - IMAGE_HEADER_SIZE + if extra < 0 { + return ri, util.FmtNewtError("Image header must be at "+ + "least %d bytes", IMAGE_HEADER_SIZE) + } + + hdr.HdrSz = uint16(ic.HeaderSize) + for i := 0; i < extra; i++ { + ri.Body = append(ri.Body, 0) + } + } + + ri.Header = hdr + + hashBytes, err := calcHash(ic.InitialHash, hdr, ic.Body) + if err != nil { + return ri, err + } + + var stream cipher.Stream + if ic.CipherSecret != nil { + block, err := aes.NewCipher(ic.PlainSecret) + if err != nil { + return ri, util.NewNewtError("Failed to create block cipher") + } + nonce := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + stream = cipher.NewCTR(block, nonce) + } + + /* + * Followed by data. + */ + dataBuf := make([]byte, 16) + encBuf := make([]byte, 16) + r := bytes.NewReader(ic.Body) + w := bytes.Buffer{} + for { + cnt, err := r.Read(dataBuf) + if err != nil && err != io.EOF { + return ri, util.FmtNewtError( + "Failed to read from image body: %s", err.Error()) + } + if cnt == 0 { + break + } + + if ic.CipherSecret == nil { + _, err = w.Write(dataBuf[0:cnt]) + } else { + stream.XORKeyStream(encBuf, dataBuf[0:cnt]) + _, err = w.Write(encBuf[0:cnt]) + } + if err != nil { + return ri, util.FmtNewtError( + "Failed to write to image body: %s", err.Error()) + } + } + ri.Body = append(ri.Body, w.Bytes()...) + + util.StatusMessage(util.VERBOSITY_VERBOSE, + "Computed Hash for image as %s\n", hex.EncodeToString(hashBytes)) + + // Hash TLV. + tlv := ImageTlv{ + Header: ImageTlvHdr{ + Type: IMAGE_TLV_SHA256, + Pad: 0, + Len: uint16(len(hashBytes)), + }, + Data: hashBytes, + } + ri.Tlvs = append(ri.Tlvs, tlv) + + tlvs, err := GenerateSigTlvs(ic.SigKeys, hashBytes) + if err != nil { + return ri, err + } + ri.Tlvs = append(ri.Tlvs, tlvs...) + + if ic.CipherSecret != nil { + tlv, err := generateEncTlv(ic.CipherSecret) + if err != nil { + return ri, err + } + ri.Tlvs = append(ri.Tlvs, tlv) + } + + return ri, nil +} diff --git a/artifact/image/encrypted.go b/artifact/image/encrypted.go new file mode 100644 index 0000000..0547e2f --- /dev/null +++ b/artifact/image/encrypted.go @@ -0,0 +1,196 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +// Decoder for PKCS#5 encrypted PKCS#8 private keys. +package image + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha1" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "fmt" + "hash" + + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/ssh/terminal" +) + +var ( + oidPbes2 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13} + oidPbkdf2 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 12} + oidHmacWithSha1 = asn1.ObjectIdentifier{1, 2, 840, 113549, 2, 7} + oidHmacWithSha224 = asn1.ObjectIdentifier{1, 2, 840, 113549, 2, 8} + oidHmacWithSha256 = asn1.ObjectIdentifier{1, 2, 840, 113549, 2, 9} + oidAes128CBC = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 2} + oidAes256CBC = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 42} +) + +// We only support a narrow set of possible key types, namely the type +// generated by either MCUboot's `imgtool.py` command, or using an +// OpenSSL command such as: +// +// openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ +// -aes-256-cbc > keyfile.pem +// +// or similar for ECDSA. Specifically, the encryption must be done +// with PBES2, and PBKDF2, and aes-256-cbc used as the cipher. +type pkcs5 struct { + Algo pkix.AlgorithmIdentifier + Encrypted []byte +} + +// The parameters when the algorithm in pkcs5 is oidPbes2 +type pbes2 struct { + KeyDerivationFunc pkix.AlgorithmIdentifier + EncryptionScheme pkix.AlgorithmIdentifier +} + +// Salt is given as a choice, but we will only support the inlined +// octet string. +type pbkdf2Param struct { + Salt []byte + IterCount int + HashFunc pkix.AlgorithmIdentifier + // Optional and default values omitted, and unsupported. +} + +type hashFunc func() hash.Hash + +func parseEncryptedPrivateKey(der []byte) (key interface{}, err error) { + var wrapper pkcs5 + if _, err = asn1.Unmarshal(der, &wrapper); err != nil { + return nil, err + } + if !wrapper.Algo.Algorithm.Equal(oidPbes2) { + return nil, fmt.Errorf("pkcs5: Unknown PKCS#5 wrapper algorithm: %v", wrapper.Algo.Algorithm) + } + + var pbparm pbes2 + if _, err = asn1.Unmarshal(wrapper.Algo.Parameters.FullBytes, &pbparm); err != nil { + return nil, err + } + if !pbparm.KeyDerivationFunc.Algorithm.Equal(oidPbkdf2) { + return nil, fmt.Errorf("pkcs5: Unknown KDF: %v", pbparm.KeyDerivationFunc.Algorithm) + } + + var kdfParam pbkdf2Param + if _, err = asn1.Unmarshal(pbparm.KeyDerivationFunc.Parameters.FullBytes, &kdfParam); err != nil { + return nil, err + } + + var hashNew hashFunc + switch { + case kdfParam.HashFunc.Algorithm.Equal(oidHmacWithSha1): + hashNew = sha1.New + case kdfParam.HashFunc.Algorithm.Equal(oidHmacWithSha224): + hashNew = sha256.New224 + case kdfParam.HashFunc.Algorithm.Equal(oidHmacWithSha256): + hashNew = sha256.New + default: + return nil, fmt.Errorf("pkcs5: Unsupported hash: %v", pbparm.EncryptionScheme.Algorithm) + } + + // Get the encryption used. + size := 0 + var iv []byte + switch { + case pbparm.EncryptionScheme.Algorithm.Equal(oidAes256CBC): + size = 32 + if _, err = asn1.Unmarshal(pbparm.EncryptionScheme.Parameters.FullBytes, &iv); err != nil { + return nil, err + } + case pbparm.EncryptionScheme.Algorithm.Equal(oidAes128CBC): + size = 16 + if _, err = asn1.Unmarshal(pbparm.EncryptionScheme.Parameters.FullBytes, &iv); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("pkcs5: Unsupported cipher: %v", pbparm.EncryptionScheme.Algorithm) + } + + return unwrapPbes2Pbkdf2(&kdfParam, size, iv, hashNew, wrapper.Encrypted) +} + +func unwrapPbes2Pbkdf2(param *pbkdf2Param, size int, iv []byte, hashNew hashFunc, encrypted []byte) (key interface{}, err error) { + pass, err := getPassword() + if err != nil { + return nil, err + } + cryptoKey := pbkdf2.Key(pass, param.Salt, param.IterCount, size, hashNew) + + block, err := aes.NewCipher(cryptoKey) + if err != nil { + return nil, err + } + enc := cipher.NewCBCDecrypter(block, iv) + + plain := make([]byte, len(encrypted)) + enc.CryptBlocks(plain, encrypted) + + plain, err = checkPkcs7Padding(plain) + if err != nil { + return nil, err + } + + return x509.ParsePKCS8PrivateKey(plain) +} + +// Verify that PKCS#7 padding is correct on this plaintext message. +// Returns a new slice with the padding removed. +func checkPkcs7Padding(buf []byte) ([]byte, error) { + if len(buf) < 16 { + return nil, fmt.Errorf("Invalid padded buffer") + } + + padLen := int(buf[len(buf)-1]) + if padLen < 1 || padLen > 16 { + return nil, fmt.Errorf("Invalid padded buffer") + } + + if padLen > len(buf) { + return nil, fmt.Errorf("Invalid padded buffer") + } + + for pos := len(buf) - padLen; pos < len(buf); pos++ { + if int(buf[pos]) != padLen { + return nil, fmt.Errorf("Invalid padded buffer") + } + } + + return buf[:len(buf)-padLen], nil +} + +// For testing, a key can be set here. If this is empty, the key will +// be queried via prompt. +var KeyPassword = []byte{} + +// Prompt the user for a password, unless we have stored one for +// testing. +func getPassword() ([]byte, error) { + if len(KeyPassword) != 0 { + return KeyPassword, nil + } + + fmt.Printf("key password: ") + return terminal.ReadPassword(0) +} diff --git a/artifact/image/image.go b/artifact/image/image.go new file mode 100644 index 0000000..244cfed --- /dev/null +++ b/artifact/image/image.go @@ -0,0 +1,544 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 image + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "strconv" + "strings" + + "mynewt.apache.org/newt/util" +) + +const ( + IMAGE_MAGIC = 0x96f3b83d /* Image header magic */ + IMAGE_TRAILER_MAGIC = 0x6907 /* Image tlv info magic */ +) + +const ( + IMAGE_HEADER_SIZE = 32 + IMAGE_TRAILER_SIZE = 4 + IMAGE_TLV_SIZE = 4 /* Plus `value` field. */ +) + +/* + * Image header flags. + */ +const ( + IMAGE_F_PIC = 0x00000001 + IMAGE_F_NON_BOOTABLE = 0x00000002 /* non bootable image */ + IMAGE_F_ENCRYPTED = 0x00000004 /* encrypted image */ +) + +/* + * Image trailer TLV types. + */ +const ( + IMAGE_TLV_KEYHASH = 0x01 + IMAGE_TLV_SHA256 = 0x10 + IMAGE_TLV_RSA2048 = 0x20 + IMAGE_TLV_ECDSA224 = 0x21 + IMAGE_TLV_ECDSA256 = 0x22 + IMAGE_TLV_ENC_RSA = 0x30 + IMAGE_TLV_ENC_KEK = 0x31 +) + +var imageTlvTypeNameMap = map[uint8]string{ + IMAGE_TLV_KEYHASH: "KEYHASH", + IMAGE_TLV_SHA256: "SHA256", + IMAGE_TLV_RSA2048: "RSA2048", + IMAGE_TLV_ECDSA224: "ECDSA224", + IMAGE_TLV_ECDSA256: "ECDSA256", + IMAGE_TLV_ENC_RSA: "ENC_RSA", + IMAGE_TLV_ENC_KEK: "ENC_KEK", +} + +type ImageVersion struct { + Major uint8 + Minor uint8 + Rev uint16 + BuildNum uint32 +} + +type ImageHdr struct { + Magic uint32 + Pad1 uint32 + HdrSz uint16 + Pad2 uint16 + ImgSz uint32 + Flags uint32 + Vers ImageVersion + Pad3 uint32 +} + +type ImageTlvHdr struct { + Type uint8 + Pad uint8 + Len uint16 +} + +type ImageTlv struct { + Header ImageTlvHdr + Data []byte +} + +type ImageTrailer struct { + Magic uint16 + TlvTotLen uint16 +} + +type Image struct { + Header ImageHdr + Body []byte + Tlvs []ImageTlv +} + +type ImageOffsets struct { + Header int + Body int + Trailer int + Tlvs []int + TotalSize int +} + +func ImageTlvTypeName(tlvType uint8) string { + name, ok := imageTlvTypeNameMap[tlvType] + if !ok { + return "???" + } + + return name +} + +func ParseVersion(versStr string) (ImageVersion, error) { + var err error + var major uint64 + var minor uint64 + var rev uint64 + var buildNum uint64 + var ver ImageVersion + + components := strings.Split(versStr, ".") + major, err = strconv.ParseUint(components[0], 10, 8) + if err != nil { + return ver, util.FmtNewtError("Invalid version string %s", versStr) + } + if len(components) > 1 { + minor, err = strconv.ParseUint(components[1], 10, 8) + if err != nil { + return ver, util.FmtNewtError("Invalid version string %s", versStr) + } + } + if len(components) > 2 { + rev, err = strconv.ParseUint(components[2], 10, 16) + if err != nil { + return ver, util.FmtNewtError("Invalid version string %s", versStr) + } + } + if len(components) > 3 { + buildNum, err = strconv.ParseUint(components[3], 10, 32) + if err != nil { + return ver, util.FmtNewtError("Invalid version string %s", versStr) + } + } + + ver.Major = uint8(major) + ver.Minor = uint8(minor) + ver.Rev = uint16(rev) + ver.BuildNum = uint32(buildNum) + return ver, nil +} + +func (ver ImageVersion) String() string { + return fmt.Sprintf("%d.%d.%d.%d", + ver.Major, ver.Minor, ver.Rev, ver.BuildNum) +} + +func (h *ImageHdr) Map(offset int) map[string]interface{} { + return map[string]interface{}{ + "Magic": h.Magic, + "HdrSz": h.HdrSz, + "ImgSz": h.ImgSz, + "Flags": h.Flags, + "Vers": h.Vers.String(), + "offset": offset, + } +} + +func rawBodyMap(offset int) map[string]interface{} { + return map[string]interface{}{ + "offset": offset, + } +} + +func (t *ImageTrailer) Map(offset int) map[string]interface{} { + return map[string]interface{}{ + "Magic": t.Magic, + "TlvTotLen": t.TlvTotLen, + "offset": offset, + } +} + +func (t *ImageTlv) Map(offset int) map[string]interface{} { + return map[string]interface{}{ + "Type": t.Header.Type, + "typestr": ImageTlvTypeName(t.Header.Type), + "Len": t.Header.Len, + "offset": offset, + "data": hex.EncodeToString(t.Data), + } +} + +func (img *Image) Map() (map[string]interface{}, error) { + offs, err := img.Offsets() + if err != nil { + return nil, err + } + + m := map[string]interface{}{} + m["header"] = img.Header.Map(offs.Header) + m["body"] = rawBodyMap(offs.Body) + trailer := img.Trailer() + m["trailer"] = trailer.Map(offs.Trailer) + + tlvMaps := []map[string]interface{}{} + for i, tlv := range img.Tlvs { + tlvMaps = append(tlvMaps, tlv.Map(offs.Tlvs[i])) + } + m["tlvs"] = tlvMaps + + return m, nil +} + +func (img *Image) Json() (string, error) { + m, err := img.Map() + if err != nil { + return "", err + } + + b, err := json.MarshalIndent(m, "", " ") + if err != nil { + return "", util.ChildNewtError(err) + } + + return string(b), nil +} + +func (tlv *ImageTlv) Write(w io.Writer) (int, error) { + totalSize := 0 + + err := binary.Write(w, binary.LittleEndian, &tlv.Header) + if err != nil { + return totalSize, util.ChildNewtError(err) + } + totalSize += IMAGE_TLV_SIZE + + size, err := w.Write(tlv.Data) + if err != nil { + return totalSize, util.ChildNewtError(err) + } + totalSize += size + + return totalSize, nil +} + +func (i *Image) FindTlvs(tlvType uint8) []ImageTlv { + var tlvs []ImageTlv + + for _, tlv := range i.Tlvs { + if tlv.Header.Type == tlvType { + tlvs = append(tlvs, tlv) + } + } + + return tlvs +} + +func (i *Image) FindUniqueTlv(tlvType uint8) (*ImageTlv, error) { + tlvs := i.FindTlvs(tlvType) + if len(tlvs) == 0 { + return nil, nil + } + if len(tlvs) > 1 { + return nil, util.FmtNewtError("Image contains %d TLVs with type %d", + len(tlvs), tlvType) + } + + return &tlvs[0], nil +} + +func (i *Image) RemoveTlvsIf(pred func(tlv ImageTlv) bool) int { + numRmed := 0 + for idx := 0; idx < len(i.Tlvs); { + tlv := i.Tlvs[idx] + if pred(tlv) { + i.Tlvs = append(i.Tlvs[:idx], i.Tlvs[idx+1:]...) + numRmed++ + } else { + idx++ + } + } + + return numRmed +} + +func (img *Image) Trailer() ImageTrailer { + trailer := ImageTrailer{ + Magic: IMAGE_TRAILER_MAGIC, + TlvTotLen: IMAGE_TRAILER_SIZE, + } + for _, tlv := range img.Tlvs { + trailer.TlvTotLen += IMAGE_TLV_SIZE + tlv.Header.Len + } + + return trailer +} + +func (i *Image) Hash() ([]byte, error) { + tlv, err := i.FindUniqueTlv(IMAGE_TLV_SHA256) + if err != nil { + return nil, err + } + + if tlv == nil { + return nil, util.FmtNewtError("Image does not contain hash TLV") + } + + return tlv.Data, nil +} + +func (i *Image) WritePlusOffsets(w io.Writer) (ImageOffsets, error) { + offs := ImageOffsets{} + offset := 0 + + offs.Header = offset + + err := binary.Write(w, binary.LittleEndian, &i.Header) + if err != nil { + return offs, util.ChildNewtError(err) + } + offset += IMAGE_HEADER_SIZE + + offs.Body = offset + size, err := w.Write(i.Body) + if err != nil { + return offs, util.ChildNewtError(err) + } + offset += size + + trailer := i.Trailer() + offs.Trailer = offset + err = binary.Write(w, binary.LittleEndian, &trailer) + if err != nil { + return offs, util.ChildNewtError(err) + } + offset += IMAGE_TRAILER_SIZE + + for _, tlv := range i.Tlvs { + offs.Tlvs = append(offs.Tlvs, offset) + size, err := tlv.Write(w) + if err != nil { + return offs, util.ChildNewtError(err) + } + offset += size + } + + offs.TotalSize = offset + + return offs, nil +} + +func (i *Image) Offsets() (ImageOffsets, error) { + return i.WritePlusOffsets(ioutil.Discard) +} + +func (i *Image) TotalSize() (int, error) { + offs, err := i.Offsets() + if err != nil { + return 0, err + } + return offs.TotalSize, nil +} + +func (i *Image) Write(w io.Writer) (int, error) { + offs, err := i.WritePlusOffsets(w) + if err != nil { + return 0, err + } + + return offs.TotalSize, nil +} + +func (i *Image) WriteToFile(filename string) error { + f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) + if err != nil { + return util.ChildNewtError(err) + } + + if _, err := i.Write(f); err != nil { + return util.ChildNewtError(err) + } + + return nil +} + +func parseRawHeader(imgData []byte, offset int) (ImageHdr, int, error) { + var hdr ImageHdr + + r := bytes.NewReader(imgData) + r.Seek(int64(offset), io.SeekStart) + + if err := binary.Read(r, binary.LittleEndian, &hdr); err != nil { + return hdr, 0, util.FmtNewtError( + "Error reading image header: %s", err.Error()) + } + + if hdr.Magic != IMAGE_MAGIC { + return hdr, 0, util.FmtNewtError( + "Image magic incorrect; expected 0x%08x, got 0x%08x", + IMAGE_MAGIC, hdr.Magic) + } + + remLen := len(imgData) - offset + if remLen < int(hdr.HdrSz) { + return hdr, 0, util.FmtNewtError( + "Image header incomplete; expected %d bytes, got %d bytes", + hdr.HdrSz, remLen) + } + + return hdr, int(hdr.HdrSz), nil +} + +func parseRawBody(imgData []byte, hdr ImageHdr, + offset int) ([]byte, int, error) { + + imgSz := int(hdr.ImgSz) + remLen := len(imgData) - offset + + if remLen < imgSz { + return nil, 0, util.FmtNewtError( + "Image body incomplete; expected %d bytes, got %d bytes", + imgSz, remLen) + } + + return imgData[offset : offset+imgSz], imgSz, nil +} + +func parseRawTrailer(imgData []byte, offset int) (ImageTrailer, int, error) { + var trailer ImageTrailer + + r := bytes.NewReader(imgData) + r.Seek(int64(offset), io.SeekStart) + + if err := binary.Read(r, binary.LittleEndian, &trailer); err != nil { + return trailer, 0, util.FmtNewtError( + "Image contains invalid trailer at offset %d: %s", + offset, err.Error()) + } + + return trailer, IMAGE_TRAILER_SIZE, nil +} + +func parseRawTlv(imgData []byte, offset int) (ImageTlv, int, error) { + tlv := ImageTlv{} + + r := bytes.NewReader(imgData) + r.Seek(int64(offset), io.SeekStart) + + if err := binary.Read(r, binary.LittleEndian, &tlv.Header); err != nil { + return tlv, 0, util.FmtNewtError( + "Image contains invalid TLV at offset %d: %s", offset, err.Error()) + } + + tlv.Data = make([]byte, tlv.Header.Len) + if _, err := r.Read(tlv.Data); err != nil { + return tlv, 0, util.FmtNewtError( + "Image contains invalid TLV at offset %d: %s", offset, err.Error()) + } + + return tlv, IMAGE_TLV_SIZE + int(tlv.Header.Len), nil +} + +func ParseImage(imgData []byte) (Image, error) { + img := Image{} + offset := 0 + + hdr, size, err := parseRawHeader(imgData, offset) + if err != nil { + return img, err + } + offset += size + + body, size, err := parseRawBody(imgData, hdr, offset) + if err != nil { + return img, err + } + offset += size + + trailer, size, err := parseRawTrailer(imgData, offset) + if err != nil { + return img, err + } + offset += size + + var tlvs []ImageTlv + tlvLen := IMAGE_TRAILER_SIZE + for offset < len(imgData) { + tlv, size, err := parseRawTlv(imgData, offset) + if err != nil { + return img, err + } + + tlvs = append(tlvs, tlv) + offset += size + + tlvLen += IMAGE_TLV_SIZE + int(tlv.Header.Len) + } + + if int(trailer.TlvTotLen) != tlvLen { + return img, util.FmtNewtError( + "invalid image: trailer indicates TLV-length=%d; actual=%d", + trailer.TlvTotLen, tlvLen) + } + + img.Header = hdr + img.Body = body + img.Tlvs = tlvs + + return img, nil +} + +func ReadImage(filename string) (Image, error) { + ri := Image{} + + imgData, err := ioutil.ReadFile(filename) + if err != nil { + return ri, util.ChildNewtError(err) + } + + return ParseImage(imgData) +} diff --git a/artifact/image/key.go b/artifact/image/key.go new file mode 100644 index 0000000..9141f6e --- /dev/null +++ b/artifact/image/key.go @@ -0,0 +1,294 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 image + +import ( + "crypto/aes" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/asn1" + "encoding/base64" + "encoding/pem" + "io/ioutil" + + keywrap "github.com/NickBall/go-aes-key-wrap" + + "mynewt.apache.org/newt/util" +) + +type ImageSigKey struct { + // Only one of these members is non-nil. + Rsa *rsa.PrivateKey + Ec *ecdsa.PrivateKey +} + +func ParsePrivateKey(keyBytes []byte) (interface{}, error) { + var privKey interface{} + var err error + + block, data := pem.Decode(keyBytes) + if block != nil && block.Type == "EC PARAMETERS" { + /* + * Openssl prepends an EC PARAMETERS block before the + * key itself. If we see this first, just skip it, + * and go on to the data block. + */ + block, _ = pem.Decode(data) + } + if block != nil && block.Type == "RSA PRIVATE KEY" { + /* + * ParsePKCS1PrivateKey returns an RSA private key from its ASN.1 + * PKCS#1 DER encoded form. + */ + privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, util.FmtNewtError( + "Private key parsing failed: %s", err) + } + } + if block != nil && block.Type == "EC PRIVATE KEY" { + /* + * ParseECPrivateKey returns a EC private key + */ + privKey, err = x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, util.FmtNewtError( + "Private key parsing failed: %s", err) + } + } + if block != nil && block.Type == "PRIVATE KEY" { + // This indicates a PKCS#8 unencrypted private key. + // The particular type of key will be indicated within + // the key itself. + privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, util.FmtNewtError( + "Private key parsing failed: %s", err) + } + } + if block != nil && block.Type == "ENCRYPTED PRIVATE KEY" { + // This indicates a PKCS#8 key wrapped with PKCS#5 + // encryption. + privKey, err = parseEncryptedPrivateKey(block.Bytes) + if err != nil { + return nil, util.FmtNewtError("Unable to decode encrypted private key: %s", err) + } + } + if privKey == nil { + return nil, util.NewNewtError("Unknown private key format, EC/RSA private " + + "key in PEM format only.") + } + + return privKey, nil +} + +func ReadKey(filename string) (ImageSigKey, error) { + key := ImageSigKey{} + + keyBytes, err := ioutil.ReadFile(filename) + if err != nil { + return key, util.FmtNewtError("Error reading key file: %s", err) + } + + privKey, err := ParsePrivateKey(keyBytes) + if err != nil { + return key, err + } + + switch priv := privKey.(type) { + case *rsa.PrivateKey: + key.Rsa = priv + case *ecdsa.PrivateKey: + key.Ec = priv + default: + return key, util.NewNewtError("Unknown private key format") + } + + return key, nil +} + +func ReadKeys(filenames []string) ([]ImageSigKey, error) { + keys := make([]ImageSigKey, len(filenames)) + + for i, filename := range filenames { + key, err := ReadKey(filename) + if err != nil { + return nil, err + } + + keys[i] = key + } + + return keys, nil +} + +func (key *ImageSigKey) assertValid() { + if key.Rsa == nil && key.Ec == nil { + panic("invalid key; neither RSA nor ECC") + } + + if key.Rsa != nil && key.Ec != nil { + panic("invalid key; neither RSA nor ECC") + } +} + +func (key *ImageSigKey) sigKeyHash() ([]uint8, error) { + key.assertValid() + + if key.Rsa != nil { + pubkey, _ := asn1.Marshal(key.Rsa.PublicKey) + sum := sha256.Sum256(pubkey) + return sum[:4], nil + } else { + switch key.Ec.Curve.Params().Name { + case "P-224": + fallthrough + case "P-256": + pubkey, _ := x509.MarshalPKIXPublicKey(&key.Ec.PublicKey) + sum := sha256.Sum256(pubkey) + return sum[:4], nil + default: + return nil, util.NewNewtError("Unsupported ECC curve") + } + } +} + +func (key *ImageSigKey) sigLen() uint16 { + key.assertValid() + + if key.Rsa != nil { + return 256 + } else { + switch key.Ec.Curve.Params().Name { + case "P-224": + return 68 + case "P-256": + return 72 + default: + return 0 + } + } +} + +func (key *ImageSigKey) sigTlvType() uint8 { + key.assertValid() + + if key.Rsa != nil { + return IMAGE_TLV_RSA2048 + } else { + switch key.Ec.Curve.Params().Name { + case "P-224": + return IMAGE_TLV_ECDSA224 + case "P-256": + return IMAGE_TLV_ECDSA256 + default: + return 0 + } + } +} + +func parseEncKeyPem(keyBytes []byte, plainSecret []byte) ([]byte, error) { + b, _ := pem.Decode(keyBytes) + if b == nil { + return nil, nil + } + + if b.Type != "PUBLIC KEY" && b.Type != "RSA PUBLIC KEY" { + return nil, util.NewNewtError("Invalid PEM file") + } + + pub, err := x509.ParsePKIXPublicKey(b.Bytes) + if err != nil { + return nil, util.FmtNewtError( + "Error parsing pubkey file: %s", err.Error()) + } + + var pubk *rsa.PublicKey + switch pub.(type) { + case *rsa.PublicKey: + pubk = pub.(*rsa.PublicKey) + default: + return nil, util.FmtNewtError( + "Error parsing pubkey file: %s", err.Error()) + } + + rng := rand.Reader + cipherSecret, err := rsa.EncryptOAEP( + sha256.New(), rng, pubk, plainSecret, nil) + if err != nil { + return nil, util.FmtNewtError( + "Error from encryption: %s\n", err.Error()) + } + + return cipherSecret, nil +} + +func parseEncKeyBase64(keyBytes []byte, plainSecret []byte) ([]byte, error) { + kek, err := base64.StdEncoding.DecodeString(string(keyBytes)) + if err != nil { + return nil, util.FmtNewtError( + "Error decoding kek: %s", err.Error()) + } + if len(kek) != 16 { + return nil, util.FmtNewtError( + "Unexpected key size: %d != 16", len(kek)) + } + + cipher, err := aes.NewCipher(kek) + if err != nil { + return nil, util.FmtNewtError( + "Error creating keywrap cipher: %s", err.Error()) + } + + cipherSecret, err := keywrap.Wrap(cipher, plainSecret) + if err != nil { + return nil, util.FmtNewtError("Error key-wrapping: %s", err.Error()) + } + + return cipherSecret, nil +} + +func ReadEncKey(filename string, plainSecret []byte) ([]byte, error) { + keyBytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, util.FmtNewtError( + "Error reading pubkey file: %s", err.Error()) + } + + // Try reading as PEM (asymetric key). + cipherSecret, err := parseEncKeyPem(keyBytes, plainSecret) + if err != nil { + return nil, err + } + if cipherSecret != nil { + return cipherSecret, nil + } + + // Not PEM; assume this is a base64 encoded symetric key + cipherSecret, err = parseEncKeyBase64(keyBytes, plainSecret) + if err != nil { + return nil, err + } + + return cipherSecret, nil +} diff --git a/artifact/image/keys_test.go b/artifact/image/keys_test.go new file mode 100644 index 0000000..577d9cd --- /dev/null +++ b/artifact/image/keys_test.go @@ -0,0 +1,244 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 image_test + +import ( + "io/ioutil" + "os" + "path" + "testing" +) + +func TestRSA(t *testing.T) { + signatureTest(t, rsaPkcs1Private) +} + +func TestPlainRSAPKCS8(t *testing.T) { + signatureTest(t, rsaPkcs8Private) +} + +func TestEcdsa(t *testing.T) { + signatureTest(t, ecdsaPrivate) +} + +func TestPlainEcdsaPkcs8(t *testing.T) { + signatureTest(t, ecdsaPkcs8Private) +} + +func TestEncryptedRSA(t *testing.T) { + image.KeyPassword = []byte("sample") + signatureTest(t, rsaEncryptedPrivate) + image.KeyPassword = []byte{} +} + +func TestEncryptedEcdsa(t *testing.T) { + image.KeyPassword = []byte("sample") + signatureTest(t, ecdsaEncryptedPrivate) + image.KeyPassword = []byte{} +} + +func signatureTest(t *testing.T, privateKey []byte) { + tmpdir, err := ioutil.TempDir("", "newttest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + // Create a source image. Format doesn't really matter that + // much, since the header will be placed on it by the image + // tool. + + simpleName := path.Join(tmpdir, "simple.bin") + hashedName := path.Join(tmpdir, "simple-hashed.bin") + signedName := path.Join(tmpdir, "simple-signed.bin") + keyName := path.Join(tmpdir, "private.pem") + + tmp := make([]byte, 256) + for i := 0; i < len(tmp); i++ { + tmp[i] = byte(i & 0xFF) + } + err = ioutil.WriteFile(simpleName, tmp, 0644) + if err != nil { + t.Fatal(err) + } + + img, err := image.NewImage(simpleName, hashedName) + if err != nil { + t.Fatal(err) + } + + img.SetVersion("1.5") + if err != nil { + t.Fatal(err) + } + + img.Generate(nil) + if err != nil { + t.Fatal(err) + } + + // Now try with a signature. + err = ioutil.WriteFile(keyName, privateKey, 0644) + if err != nil { + t.Fatal(err) + } + + img, err = image.NewImage(simpleName, signedName) + if err != nil { + t.Fatal(err) + } + + err = img.SetSigningKey(keyName, 0) + if err != nil { + t.Fatal(err) + } + + err = img.SetVersion("1.6") + if err != nil { + t.Fatal(err) + } + + err = img.Generate(nil) + if err != nil { + t.Fatal(err) + } +} + +// An RSA private key in the old PKCS1 format. +var rsaPkcs1Private = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA6q2Q/VoFf6U5xm35ynls+HDbHKwfIbBr27PtFJxlS9YT0xKJ +bcZScPTVizTlft0wfp2TctX/vGd/Y/X3qo5ckRmz+lKUeHm46i4k6rtOBbhBz2id +hwrO7/ylzwaf8lxn2dj/9ikoYQKFtBb/cKu8wyuvW3gs/ou51AVEF8aKTrl5Expy +PrhSlh97er2zUmm8NAoo259I5yHK1SvR9kCw2gNXSDQLpFlK2WikdmEbIu0N+cvN +WM4ONAhffkasznrEOoLPSI66RDrzYhi/Ks9t+N2buEOXao19fDRcSHgZLKT8e6W6 +uK7WxRiEzNbajzgDddbZFqWlcpE7sqPNHFBijwIDAQABAoIBAQDdXx7fLpTzROvM +F5/C9GnrraGzWVYAlIgZ9o8Umzceo3GN8PV8fND1xq7Novc9he8h8QjPEbksg0Dz +DWo0FBiTs3hIELAHOWNKXH7sggVmddp2iUvXwEVWsq/CK5CjsbExGXbSQR7a6+Mt +72fEY+wq+0Fuel2PPETuEI2cE+gRuyspIcO7asmMvLRkxLi2EXU0s4JlqV9UfxKQ +aqn0PHlRXa5SIzys3mVhXuoe45T50+VKX0DIfu/RuV8njNkkMx74DeEVvf5W4MJW +vHrRBHoK6KoMrqiwafyPLW/Rh6fMYAdPrffMVuuThtG7Hp83VBVX1HxFhI4Jrf3S +Hf63hmSZAoGBAO2R/vYBl57qgWEKiMQaU8rzctRbP0TtTsqNdISPMnHV1Tn/rNAU +m0N7/6IBDb+IlndXDQwIsW/DTXhF2XJSu7n6GXua8B1LF+zuVWUsFfmE3+eLz7B8 +x8G/OkSnOTfRZCYWEoEvzhynn1dlADQ+x49I/XmKqccvAhY71glk6WULAoGBAPzi +IYo9G+ktlNj9/3OciX7aTCiIIMDyPYtYS6wi59gwH9IswaicHYK4w2fDpTWKlzEE +18dKF4puuI5GxnKCwHBiWxGhij063cZKKMqA64X41csK+mumux/PAb2gKbGSzzoF +mSgkKXJ+sZ4ytlgsijEAHV85Sw7j+xy8A0qnCWMNAoGAeCDR7q1hcM8duucrvxWc +90vg7bZyKLVimROsLneGR3+cAWbiiJlS5W3nFpE31XkItLHE/CfNKTl1i/KuAJwL +JwBrMFBpSDa3k2v0rGL9fZ2N5rSQwapnC/ZZTWvNiAcOgB+7Ha4BqAWuke+VidWQ +7Ug4O+Q882Y2xO1ezoNDbX8CgYBq228KyAm8PXuRObsw8iuTg9D8q5ETlwj0kcng +IhvP2X4IxMrMYbOCompHtX9hIYADwaUgXCmYYHLyA+wlRSTmGFmdGKKefvppqLqV +32YmhWBp3Oi2hoy5wzJcG4qis4OHZAg00xsEe464Z3tvxNpcHE1NCJuz3hglKzlE +2VJ5HQKBgQDRisWDbdnOEp7LTXp3Aa33PF1Rx/pkFk4Wb+2Hk977O1OxsAin2cKM +S5HCltHvON2sCmSQUIxMXXKaNPJiGL3UZJxWZDj38zSg0vO+msmemS1Yjt0xCpbO +pkl0kvKb/NVlsY4w9kquvql+t9e1rUu9Ug28TKEsSjc9SFrcnVPoNA== +-----END RSA PRIVATE KEY----- +`) + +// An RSA private key in PKCS8 format, with no encryption. +var rsaPkcs8Private = []byte(`-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC+FjuXqPSPucsQ +adxY4nw+9kTgAdsXRIPxq4Q//wkfjEjYhDczN+/rafi0hApuRh7PN7VMGOsDGGR1 +edyertiLt3SfUHAZROIqZ0VAoKGtxgXmnC+s+mMujAv9Ssntbmbi5tNxDcltdWjA +SdBn7tbIMVVofKaMMugyuXCglxebMm8yxtkSgUvE1E6zZERnteDJTPo8dBCiqkvU +hf+vG9s1j9lNDMjrZ+d5CHIFmBxJ/WFa6m49lNBFb1Ba43bKdj6mkK05rZ4VWMXU +evy3Z/UUgU4VPJpoB+GIKy82iOrtjiU7s/6aDkvZ2e+fgxKksN0pzFE9azeA73QS +bamp28E/AgMBAAECggEBAJ78+4UDFOKt1JF66YkSjjcfRkZSZwyUCwP0oF3ik5/m +dvtZws29KJevgAyEMDFBxv0srB/k65QgL84uSgATYB2kKRAjeE86VSyASeUfNXui +GEdlNV8p4hEJo/GMP06uu7FmvU1e6a36uM20L3LuyoiQ8s29DJRQ8/ORNQmstlrg +J32FZSjTF1mElGPSc1koxhWvl1hE7UGE9pxsSfdsvPNhCIWwAOnVnIv49xG8EWaK +CkHhEVVdZW8IvO9GYR5U0BJcgzNmdNkS8HVQBIxZtboGAAuPI32EC7siDomKmCF6 +rEcs40f/J/RlK6lrTyKKfqWb4DPtRrOSh9cmjrFFZlECgYEA6mZIANLXJd7CINZ9 +fjotI+FxH8BDOZF7l8xTnOk1e3Me1ia7t2GMcIL+frfG/zMBiDFq0IQuUYScRK1v +pAILjJKFiU6yY8vH6FZ3mXqiiag6RPa+q89DaUsO0uXRUjQvhtTd5Yy6r8Eac1ya +y6XC5T5sCJ6HgaF3qlheap+5FkkCgYEAz5qSLShV5oekuj1R0fs+h/Yn7VW9Q0sj +px8jOD4MWc8gPZ9gZe0UPTvofOLrM3eAetP4egSif99AE9iD8EbiBzAt16OX7EN8 +d7xNiIN922Ep3pubcD6f1vglaI7Thrca/p52g6kWPip6+PWFd1acU6u31Uj0Xvgz +VFiafstF+0cCgYEAw2sOcJFXCZ2Tnyjzav85jwZu95ek9CPUNJQGyXSsQAWUGdok ++hf7q/mqDx9Maoqtpkv8z2bD7vZuCdvGjaee1U16wyS3GPhV69/ayjwxsi5slf5Y +rIiZnPkUnMM5Jh2X2gMyFCSlp82ILdFwxIOn3tOR4gW411w0lfIilSYgevECgYA3 +JAgVZHREcdzH9seHrWLze+co+6/0cr26guO46YogRIp8s5tIF0tb5FCg8yijl+cR +OMHzrs12h1aertCEfl9Ep4BVmUcd4uLpbqNtUfeY0FrtnIkRrCCKWYieF+mJC5No +86/o0n1s752QCK51fxSwiJigVutJWkVP7uTCLr2cuwKBgQCJPWMcWmSuRlLOVWnO +jPFoa02Bb83n8GrRpQkpkZZofHextwfo2dd1sZF72zghRsbdC6e0Zj1GrekJOYXO +8AXmCpyKlXJU7iH5tPGSo68uFN05R6mINbTNmEIQBNTKv8UoKT+nHcTycFrVtarX +A8EPW2xB86m+Bjq/GNyRgfbPMg== +-----END PRIVATE KEY----- +`) + +// An ECDSA key in the X.509 internal private key format. +var ecdsaPrivate = []byte(`-----BEGIN EC PRIVATE KEY----- +MGgCAQEEHF64kDx3pZyVvezbqYMIxlLbtuPQmI85k4GRy1mgBwYFK4EEACGhPAM6 +AASRtolOCTLQYkDefkIF02tUXR92MKHrbtH4WK/8bfTSFVkaygTPdJbpNthK2wae +oX9ZeFHS1pcOfQ== +-----END EC PRIVATE KEY----- +`) + +// An ECDSA key in PKCS#8 format, no encryption. +var ecdsaPkcs8Private = []byte(`-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgHKeDq4UU6M+c+pMm +j0AQZlBs7f4r67668eDCUB8aDR2hRANCAATyZPzsx+xn9JtlxdspevTrYisiMTjl +YuBJCrV1FZj2HkplEgO+ZIMuD7eRvyTEBS2bw6F1aCeKOMUmYVImAbpc +-----END PRIVATE KEY----- +`) + +// A password-protected RSA private key in PKCS#5/8 format. The +// password for this key is "sample". +var rsaEncryptedPrivate = []byte(`-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIRMifqJThk8kCAggA +MB0GCWCGSAFlAwQBKgQQTMUBoFpzjJ5UNRnCIeqf4QSCBNDkQvXnUNmss8erKiDo +Uqs2tf9ZD8MjDThLBmF/gV1dg1q6aDY+3fI2E4yLXJb2PmKcUq82YZ0FDeoCvJRJ +BCurzM9slur5akpNBTFoFwtFsdHz7nKNS4MHUul22rGBnVFUUNTySmpjl/m+dxWO +fa6tWpGTAr7tsCy9gF5PxpSw7NR/NpIL0PmpydHWhTs1tl2csqBqK6Tp014Kefi/ +pmmeb2eRl5cmprxW32rW2QBMtv4z91SsbnlVdz4r8txTG+3S4td9v9jD5kqcIiC2 +KQHrbH9y7okUk/ISsp9ANKPJt10fbYDxORiMK57XssXy1enGjpkIIrUGz1TMydkD +USfwqkmPuIrrzOXnbxU4ef2wC/pA/h9Smby3WWYo8725/1kZyIediNDcgi/Qgrs4 +1VQAYzsD6duwyUNSo+tgmYVFGvZhsottus3fMWe/Ay1biJ6z6Vk8gqKWI1VV/REJ +zK/I9hgKGxj2N2Ff6E/YkcwQenHWj/iDWLjvokyOBnPFNqzzM2Qqo1XFpzj4EO5D +0WD4EzZYvUhk3lZZNydvXiuy8RrCVLLJMS08XgOqQaiFqqxj2hjRwv3nBesk7iA8 +5Tv8GMa5QkNrISCnp4/uGBh+v/CjwVRqPTcK3/mctPN2nLhI6H4pF4Y6apXkz1TN +NMQqxaxmVVg8fyLaS4/xfUr8LAmiEtOwvs0XOhcqCTvvlsO4N+yec4VD4gmsTDY9 +/2b/+YwSlGMpA+GQQbg0FraaF8NyJRG1mSER6WiUGGM1cuKK44nzBbykQbZwzDSA +kkhjDaadkhv/NPKAUR3sNy2GXVaNL/ItCpQUHRKKcIPp0HhdXsl0YebuwRlHjw/6 +UOdzNYe23e40X/Xl3vmOKRbzhLP/qO2DV21o0wI4ujF8Xu5h1h8s49HPp58G1ldy +/hJ6durYKX8T5khiR2iXYewoy0YObuccV//Ov1/ySOp/x0/QuCl/swvs8Jf7awnu +rpRrHPArpCvMmXmt5Y+TFYXFjkJGwsxTew5TcwBebBlIET2XNbo2pbz4WqJ3eVlK +CNZVDEZ8mMrGT00FBi759Vfw9rhrnqXnLlNtJZ5VCXFUw8Tos302sLaQWXzHYyf8 +4awM8G9PSu5Q9lFcN9od4H95YrAAv/l8F+pcGgEKD8ZuzsgFIalqgx5wzmUMDcPM +NKV5u9mtHjI92ru6NB8rGesM6sy6kBGvpotsDWawpV2SoCrkbyEkk+kXaGS+fsG7 +D2H37GfktN8R5Ktc0Uf/JJiNfDzq8lk1J4r7LBQlWUbhKbfGMYxt+7Xo0GsqAsLp +PKSUwx+hTZb3BmW6s4Q6vivI1MdQbWVT1zh41StvfRSNlo70iOFxOM0lU1jjY989 +UKo+gcolddvZbMNwip0ILPO3dsa+he1jJ/gbo9qBHLy7plfsBLLakZP1Nu6xdlqQ +TSSobaE8uxUMZk+wMWClA9AOZ1TcUr2yRV5GVj/bxG9ab+H37vF9F8vFE+jjJ7yN +6pjdohm4gXeSVx7ON4SeZLsVwNYkCVYS89E81qLx1jP9F57+6IUGDZN5EMC0aJLT +ny75MCCLT00KD7BFsb0KDLXxp++eu/L2hinorT3p6dXp/9mUoxmy6wJqEyqCFniZ +N2GZN7+LDTIbHUxCijVWamU2DQ== +-----END ENCRYPTED PRIVATE KEY----- +`) + +// A password-protected ECDSA private key in PKCS#5/8 format. The +// password for this key is "sample" +var ecdsaEncryptedPrivate = []byte(`-----BEGIN ENCRYPTED PRIVATE KEY----- +MIHeMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAjlKrDSKNg9QQICCAAw +HQYJYIZIAWUDBAEqBBDliPNzQTNpdlppTcYpmuhWBIGQVhfWaVSzUvi/qIZLiZVn +Nulfw5jDOlbn3UBX9kp/Z9Pro582Q0kjzLfm5UahvDINEJWxL4pc/28UnGQTBr0Q +nSEg+RbqpuD099C38H0Gq/YkIM+RDG4aiQrkmzHXyVsHshIbG+z2LsLTIwmU69/Z +v0nX6/hGErVR8YWcrOne086rCvfJVrxyO5+EUqrkLhEr +-----END ENCRYPTED PRIVATE KEY----- +`) diff --git a/artifact/image/v1.go b/artifact/image/v1.go new file mode 100644 index 0000000..bab86f6 --- /dev/null +++ b/artifact/image/v1.go @@ -0,0 +1,487 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +// This file implements parsing and generation of version-1 images. Much of +// this code duplicates the v2 code. The expectation is that this file will be +// removed when version 1 is oficially retired (soon). + +package image + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "io" + "io/ioutil" + + "mynewt.apache.org/newt/util" +) + +const IMAGEv1_MAGIC = 0x96f3b83c /* Image header magic */ + +const ( + IMAGEv1_F_PIC = 0x00000001 + IMAGEv1_F_SHA256 = 0x00000002 /* Image contains hash TLV */ + IMAGEv1_F_PKCS15_RSA2048_SHA256 = 0x00000004 /* PKCS15 w/RSA2048 and SHA256 */ + IMAGEv1_F_ECDSA224_SHA256 = 0x00000008 /* ECDSA224 over SHA256 */ + IMAGEv1_F_NON_BOOTABLE = 0x00000010 /* non bootable image */ + IMAGEv1_F_ECDSA256_SHA256 = 0x00000020 /* ECDSA256 over SHA256 */ + IMAGEv1_F_PKCS1_PSS_RSA2048_SHA256 = 0x00000040 /* RSA-PSS w/RSA2048 and SHA256 */ +) + +const ( + IMAGEv1_TLV_SHA256 = 1 + IMAGEv1_TLV_RSA2048 = 2 + IMAGEv1_TLV_ECDSA224 = 3 + IMAGEv1_TLV_ECDSA256 = 4 +) + +// Set this to enable RSA-PSS for RSA signatures, instead of PKCS#1 +// v1.5. Eventually, this should be the default. +var UseRsaPss = false + +type ImageHdrV1 struct { + Magic uint32 + TlvSz uint16 + KeyId uint8 + Pad1 uint8 + HdrSz uint16 + Pad2 uint16 + ImgSz uint32 + Flags uint32 + Vers ImageVersion + Pad3 uint32 +} + +type ImageV1 struct { + Header ImageHdrV1 + Body []byte + Tlvs []ImageTlv +} + +func (img *ImageV1) FindTlvs(tlvType uint8) []ImageTlv { + var tlvs []ImageTlv + + for _, tlv := range img.Tlvs { + if tlv.Header.Type == tlvType { + tlvs = append(tlvs, tlv) + } + } + + return tlvs +} + +func (img *ImageV1) Hash() ([]byte, error) { + tlvs := img.FindTlvs(IMAGEv1_TLV_SHA256) + if len(tlvs) == 0 { + return nil, util.FmtNewtError("Image does not contain hash TLV") + } + if len(tlvs) > 1 { + return nil, util.FmtNewtError("Image contains %d hash TLVs", len(tlvs)) + } + + return tlvs[0].Data, nil +} + +func (img *ImageV1) WritePlusOffsets(w io.Writer) (ImageOffsets, error) { + offs := ImageOffsets{} + offset := 0 + + offs.Header = offset + + err := binary.Write(w, binary.LittleEndian, &img.Header) + if err != nil { + return offs, util.ChildNewtError(err) + } + offset += IMAGE_HEADER_SIZE + + offs.Body = offset + size, err := w.Write(img.Body) + if err != nil { + return offs, util.ChildNewtError(err) + } + offset += size + + for _, tlv := range img.Tlvs { + offs.Tlvs = append(offs.Tlvs, offset) + size, err := tlv.Write(w) + if err != nil { + return offs, util.ChildNewtError(err) + } + offset += size + } + + offs.TotalSize = offset + + return offs, nil +} + +func (img *ImageV1) Offsets() (ImageOffsets, error) { + return img.WritePlusOffsets(ioutil.Discard) +} + +func (img *ImageV1) TotalSize() (int, error) { + offs, err := img.Offsets() + if err != nil { + return 0, err + } + return offs.TotalSize, nil +} + +func (img *ImageV1) Write(w io.Writer) (int, error) { + offs, err := img.WritePlusOffsets(w) + if err != nil { + return 0, err + } + + return offs.TotalSize, nil +} + +func (key *ImageSigKey) sigHdrTypeV1() (uint32, error) { + key.assertValid() + + if key.Rsa != nil { + if UseRsaPss { + return IMAGEv1_F_PKCS1_PSS_RSA2048_SHA256, nil + } else { + return IMAGEv1_F_PKCS15_RSA2048_SHA256, nil + } + } else { + switch key.Ec.Curve.Params().Name { + case "P-224": + return IMAGEv1_F_ECDSA224_SHA256, nil + case "P-256": + return IMAGEv1_F_ECDSA256_SHA256, nil + default: + return 0, util.FmtNewtError("Unsupported ECC curve") + } + } +} + +func (key *ImageSigKey) sigTlvTypeV1() uint8 { + key.assertValid() + + if key.Rsa != nil { + return IMAGEv1_TLV_RSA2048 + } else { + switch key.Ec.Curve.Params().Name { + case "P-224": + return IMAGEv1_TLV_ECDSA224 + case "P-256": + return IMAGEv1_TLV_ECDSA256 + default: + return 0 + } + } +} + +func generateV1SigRsa(key *rsa.PrivateKey, hash []byte) ([]byte, error) { + var signature []byte + var err error + + if UseRsaPss { + opts := rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthEqualsHash, + } + signature, err = rsa.SignPSS( + rand.Reader, key, crypto.SHA256, hash, &opts) + } else { + signature, err = rsa.SignPKCS1v15( + rand.Reader, key, crypto.SHA256, hash) + } + if err != nil { + return nil, util.FmtNewtError("Failed to compute signature: %s", err) + } + + return signature, nil +} + +func generateV1SigTlvRsa(key ImageSigKey, hash []byte) (ImageTlv, error) { + sig, err := generateV1SigRsa(key.Rsa, hash) + if err != nil { + return ImageTlv{}, err + } + + return ImageTlv{ + Header: ImageTlvHdr{ + Type: key.sigTlvTypeV1(), + Pad: 0, + Len: 256, /* 2048 bits */ + }, + Data: sig, + }, nil +} + +func generateV1SigTlvEc(key ImageSigKey, hash []byte) (ImageTlv, error) { + sig, err := generateSigEc(key.Ec, hash) + if err != nil { + return ImageTlv{}, err + } + + sigLen := key.sigLen() + if len(sig) > int(sigLen) { + return ImageTlv{}, util.FmtNewtError("Something is really wrong\n") + } + + b := &bytes.Buffer{} + + if _, err := b.Write(sig); err != nil { + return ImageTlv{}, + util.FmtNewtError("Failed to append sig: %s", err.Error()) + } + + pad := make([]byte, int(sigLen)-len(sig)) + if _, err := b.Write(pad); err != nil { + return ImageTlv{}, util.FmtNewtError( + "Failed to serialize image trailer: %s", err.Error()) + } + + return ImageTlv{ + Header: ImageTlvHdr{ + Type: key.sigTlvTypeV1(), + Pad: 0, + Len: sigLen + uint16(len(pad)), + }, + Data: b.Bytes(), + }, nil +} + +func generateV1SigTlv(key ImageSigKey, hash []byte) (ImageTlv, error) { + key.assertValid() + + if key.Rsa != nil { + return generateV1SigTlvRsa(key, hash) + } else { + return generateV1SigTlvEc(key, hash) + } +} + +func calcHashV1(initialHash []byte, hdr ImageHdrV1, + plainBody []byte) ([]byte, error) { + + hash := sha256.New() + + add := func(itf interface{}) error { + if err := binary.Write(hash, binary.LittleEndian, itf); err != nil { + return util.FmtNewtError("Failed to hash data: %s", err.Error()) + } + + return nil + } + + if initialHash != nil { + if err := add(initialHash); err != nil { + return nil, err + } + } + + if err := add(hdr); err != nil { + return nil, err + } + + extra := hdr.HdrSz - IMAGE_HEADER_SIZE + if extra > 0 { + b := make([]byte, extra) + if err := add(b); err != nil { + return nil, err + } + } + + if err := add(plainBody); err != nil { + return nil, err + } + + return hash.Sum(nil), nil +} + +func (ic *ImageCreator) CreateV1() (ImageV1, error) { + ri := ImageV1{} + + if len(ic.SigKeys) > 1 { + return ri, util.FmtNewtError( + "V1 image format only allows one key, %d keys specified", + len(ic.SigKeys)) + } + + // First the header + hdr := ImageHdrV1{ + Magic: IMAGEv1_MAGIC, + TlvSz: 0, // Filled in later. + KeyId: 0, + Pad1: 0, + HdrSz: IMAGE_HEADER_SIZE, + Pad2: 0, + ImgSz: uint32(len(ic.Body)), + Flags: IMAGEv1_F_SHA256, + Vers: ic.Version, + Pad3: 0, + } + + if !ic.Bootable { + hdr.Flags |= IMAGEv1_F_NON_BOOTABLE + } + + if ic.HeaderSize != 0 { + /* + * Pad the header out to the given size. There will + * just be zeros between the header and the start of + * the image when it is padded. + */ + if ic.HeaderSize < IMAGE_HEADER_SIZE { + return ri, util.FmtNewtError("Image header must be at "+ + "least %d bytes", IMAGE_HEADER_SIZE) + } + + hdr.HdrSz = uint16(ic.HeaderSize) + } + + if len(ic.SigKeys) > 0 { + keyFlag, err := ic.SigKeys[0].sigHdrTypeV1() + if err != nil { + return ri, err + } + hdr.Flags |= keyFlag + hdr.TlvSz = 4 + ic.SigKeys[0].sigLen() + } + hdr.TlvSz += 4 + 32 + + if hdr.HdrSz > IMAGE_HEADER_SIZE { + // Pad the header out to the given size. There will + // just be zeros between the header and the start of + // the image when it is padded. + extra := ic.HeaderSize - IMAGE_HEADER_SIZE + if extra < 0 { + return ri, util.FmtNewtError("Image header must be at "+ + "least %d bytes", IMAGE_HEADER_SIZE) + } + + hdr.HdrSz = uint16(ic.HeaderSize) + for i := 0; i < extra; i++ { + ri.Body = append(ri.Body, 0) + } + } + + hashBytes, err := calcHashV1(ic.InitialHash, hdr, ic.Body) + if err != nil { + return ri, err + } + + util.StatusMessage(util.VERBOSITY_VERBOSE, + "Computed Hash for image as %s\n", hex.EncodeToString(hashBytes)) + + /* + * Followed by data. + */ + dataBuf := make([]byte, 1024) + r := bytes.NewReader(ic.Body) + w := bytes.Buffer{} + for { + cnt, err := r.Read(dataBuf) + if err != nil && err != io.EOF { + return ri, util.FmtNewtError( + "Failed to read from image body: %s", err.Error()) + } + if cnt == 0 { + break + } + + if _, err = w.Write(dataBuf[0:cnt]); err != nil { + return ri, util.FmtNewtError( + "Failed to write to image body: %s", err.Error()) + } + } + ri.Body = w.Bytes() + + // Hash TLV. + tlv := ImageTlv{ + Header: ImageTlvHdr{ + Type: IMAGEv1_TLV_SHA256, + Pad: 0, + Len: uint16(len(hashBytes)), + }, + Data: hashBytes, + } + ri.Tlvs = append(ri.Tlvs, tlv) + + if len(ic.SigKeys) > 0 { + tlv, err := generateV1SigTlv(ic.SigKeys[0], hashBytes) + if err != nil { + return ri, err + } + ri.Tlvs = append(ri.Tlvs, tlv) + } + + offs, err := ri.Offsets() + if err != nil { + return ri, err + } + hdr.TlvSz = uint16(offs.TotalSize - offs.Tlvs[0]) + + ri.Header = hdr + + return ri, nil +} + +func GenerateV1Image(opts ImageCreateOpts) (ImageV1, error) { + ic := NewImageCreator() + + srcBin, err := ioutil.ReadFile(opts.SrcBinFilename) + if err != nil { + return ImageV1{}, util.FmtNewtError( + "Can't read app binary: %s", err.Error()) + } + + ic.Body = srcBin + ic.Version = opts.Version + ic.SigKeys = opts.SigKeys + + if opts.LoaderHash != nil { + ic.InitialHash = opts.LoaderHash + ic.Bootable = false + } else { + ic.Bootable = true + } + + if opts.SrcEncKeyFilename != "" { + plainSecret := make([]byte, 16) + if _, err := rand.Read(plainSecret); err != nil { + return ImageV1{}, util.FmtNewtError( + "Random generation error: %s\n", err) + } + + cipherSecret, err := ReadEncKey(opts.SrcEncKeyFilename, plainSecret) + if err != nil { + return ImageV1{}, err + } + + ic.PlainSecret = plainSecret + ic.CipherSecret = cipherSecret + } + + ri, err := ic.CreateV1() + if err != nil { + return ImageV1{}, err + } + + return ri, nil +} diff --git a/artifact/manifest/manifest.go b/artifact/manifest/manifest.go new file mode 100644 index 0000000..62d8c06 --- /dev/null +++ b/artifact/manifest/manifest.go @@ -0,0 +1,95 @@ +package manifest + +import ( + "encoding/json" + "io" + "io/ioutil" + + "mynewt.apache.org/newt/artifact/flash" + "mynewt.apache.org/newt/util" +) + +/* + * Data that's going to go to build manifest file + */ +type ManifestSizeArea struct { + Name string `json:"name"` + Size uint32 `json:"size"` +} + +type ManifestSizeSym struct { + Name string `json:"name"` + Areas []*ManifestSizeArea `json:"areas"` +} + +type ManifestSizeFile struct { + Name string `json:"name"` + Syms []*ManifestSizeSym `json:"sym"` +} + +type ManifestSizePkg struct { + Name string `json:"name"` + Files []*ManifestSizeFile `json:"files"` +} + +type ManifestPkg struct { + Name string `json:"name"` + Repo string `json:"repo"` +} + +type ManifestRepo struct { + Name string `json:"name"` + Commit string `json:"commit"` + Dirty bool `json:"dirty,omitempty"` + URL string `json:"url,omitempty"` +} + +type Manifest struct { + Name string `json:"name"` + Date string `json:"build_time"` + Version string `json:"build_version"` + BuildID string `json:"id"` + Image string `json:"image"` + ImageHash string `json:"image_hash"` + Loader string `json:"loader"` + LoaderHash string `json:"loader_hash"` + Pkgs []*ManifestPkg `json:"pkgs"` + LoaderPkgs []*ManifestPkg `json:"loader_pkgs,omitempty"` + TgtVars []string `json:"target"` + Repos []*ManifestRepo `json:"repos"` + FlashAreas []flash.FlashArea `json:"flash_map"` + + PkgSizes []*ManifestSizePkg `json:"pkgsz"` + LoaderPkgSizes []*ManifestSizePkg `json:"loader_pkgsz,omitempty"` +} + +func ReadManifest(path string) (Manifest, error) { + m := Manifest{} + + content, err := ioutil.ReadFile(path) + if err != nil { + return m, util.ChildNewtError(err) + } + + if err := json.Unmarshal(content, &m); err != nil { + return m, util.FmtNewtError( + "Failure decoding manifest with path \"%s\": %s", + path, err.Error()) + } + + return m, nil +} + +func (m *Manifest) Write(w io.Writer) (int, error) { + buffer, err := json.MarshalIndent(m, "", " ") + if err != nil { + return 0, util.FmtNewtError("Cannot encode manifest: %s", err.Error()) + } + + cnt, err := w.Write(buffer) + if err != nil { + return 0, util.FmtNewtError("Cannot write manifest: %s", err.Error()) + } + + return cnt, nil +}