You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mynewt.apache.org by cc...@apache.org on 2019/03/22 23:02:39 UTC

[mynewt-imgmod] branch master created (now c6855f4)

This is an automated email from the ASF dual-hosted git repository.

ccollins pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-imgmod.git.


      at c6855f4  imgmod - initial commit

This branch includes the following new commits:

     new c6855f4  imgmod - initial commit

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[mynewt-imgmod] 01/01: imgmod - initial commit

Posted by cc...@apache.org.
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-imgmod.git

commit c6855f438be74c531f65b53cdb6fa3641a27c6d1
Author: Christopher Collins <cc...@apache.org>
AuthorDate: Fri Mar 22 16:01:53 2019 -0700

    imgmod - initial commit
---
 cli/image_cmds.go | 597 +++++++++++++++++++++++++++++++++++++++++++++++++++
 cli/mfg_cmds.go   | 623 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 cli/util.go       | 111 ++++++++++
 go.mod            |   7 +
 go.sum            |  29 +++
 iimg/lvimg.go     | 189 +++++++++++++++++
 imfg/lvmfg.go     | 249 ++++++++++++++++++++++
 imgmod.go         | 100 +++++++++
 8 files changed, 1905 insertions(+)

diff --git a/cli/image_cmds.go b/cli/image_cmds.go
new file mode 100644
index 0000000..83b4e8f
--- /dev/null
+++ b/cli/image_cmds.go
@@ -0,0 +1,597 @@
+/**
+ * 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 cli
+
+import (
+	"encoding/binary"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"sort"
+
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+
+	"mynewt.apache.org/newt/artifact/image"
+	"mynewt.apache.org/newt/artifact/sec"
+	"mynewt.apache.org/imgmod/iimg"
+	"mynewt.apache.org/newt/util"
+)
+
+func tlvStr(tlv image.ImageTlv) string {
+	return fmt.Sprintf("%s,0x%02x",
+		image.ImageTlvTypeName(tlv.Header.Type),
+		tlv.Header.Type)
+}
+
+func readImage(filename string) (image.Image, error) {
+	img, err := image.ReadImage(filename)
+	if err != nil {
+		return img, err
+	}
+
+	log.Debugf("Successfully read image %s", filename)
+	return img, nil
+}
+
+func writeImage(img image.Image, filename string) error {
+	if err := iimg.VerifyImage(img); err != nil {
+		return err
+	}
+
+	if err := img.WriteToFile(filename); err != nil {
+		return err
+	}
+
+	util.StatusMessage(util.VERBOSITY_DEFAULT, "Wrote image %s\n", filename)
+	return nil
+}
+
+func parseTlvArgs(typeArg string, filenameArg string) (image.ImageTlv, error) {
+	tlvType, err := util.AtoiNoOct(typeArg)
+	if err != nil || tlvType < 0 {
+		return image.ImageTlv{}, util.FmtNewtError(
+			"Invalid TLV type integer: %s", typeArg)
+	}
+
+	data, err := ioutil.ReadFile(filenameArg)
+	if err != nil {
+		return image.ImageTlv{}, util.FmtNewtError(
+			"Error reading TLV data file: %s", err.Error())
+	}
+
+	return image.ImageTlv{
+		Header: image.ImageTlvHdr{
+			Type: uint8(tlvType),
+			Pad:  0,
+			Len:  uint16(len(data)),
+		},
+		Data: data,
+	}, nil
+}
+
+func runShowCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 1 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	img, err := readImage(args[0])
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	s, err := img.Json()
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+	fmt.Printf("%s\n", s)
+}
+
+func runBriefCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 1 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	img, err := readImage(args[0])
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	offsets, err := img.Offsets()
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	fmt.Printf("%8d| Header\n", offsets.Header)
+	fmt.Printf("%8d| Body\n", offsets.Body)
+	fmt.Printf("%8d| Trailer\n", offsets.Trailer)
+	for i, tlv := range img.Tlvs {
+		fmt.Printf("%8d| TLV%d: type=%s(%d)\n",
+			offsets.Tlvs[i], i, image.ImageTlvTypeName(tlv.Header.Type),
+			tlv.Header.Type)
+	}
+	fmt.Printf("Total=%d\n", offsets.TotalSize)
+}
+
+func runSignCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 2 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	inFilename := args[0]
+	outFilename, err := CalcOutFilename(inFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	img, err := readImage(inFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	keys, err := sec.ReadKeys(args[1:])
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	hash, err := img.Hash()
+	if err != nil {
+		ImgmodUsage(cmd, util.FmtNewtError(
+			"Failed to read hash from specified image: %s", err.Error()))
+	}
+
+	tlvs, err := image.BuildSigTlvs(keys, hash)
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	img.Tlvs = append(img.Tlvs, tlvs...)
+
+	if err := writeImage(img, outFilename); err != nil {
+		ImgmodUsage(nil, err)
+	}
+}
+
+func runAddTlvsCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 3 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	inFilename := args[0]
+	outFilename, err := CalcOutFilename(inFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	img, err := readImage(inFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	tlvArgs := args[1:]
+	if len(tlvArgs)%2 != 0 {
+		ImgmodUsage(cmd, util.FmtNewtError(
+			"Invalid argument count; each TLV requires two arguments"))
+	}
+
+	tlvs := []image.ImageTlv{}
+	for i := 0; i < len(tlvArgs); i += 2 {
+		tlv, err := parseTlvArgs(tlvArgs[i], tlvArgs[i+1])
+		if err != nil {
+			ImgmodUsage(cmd, err)
+		}
+
+		tlvs = append(tlvs, tlv)
+	}
+
+	img.Tlvs = append(img.Tlvs, tlvs...)
+
+	if err := writeImage(img, outFilename); err != nil {
+		ImgmodUsage(nil, err)
+	}
+}
+
+func runRmtlvsCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 2 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	inFilename := args[0]
+	outFilename, err := CalcOutFilename(inFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	img, err := readImage(inFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	tlvIndices := []int{}
+	idxMap := map[int]struct{}{}
+	for _, arg := range args[1:] {
+		idx, err := util.AtoiNoOct(arg)
+		if err != nil {
+			ImgmodUsage(cmd, util.FmtNewtError("Invalid TLV index: %s", arg))
+		}
+
+		if idx < 0 || idx >= len(img.Tlvs) {
+			ImgmodUsage(nil, util.FmtNewtError(
+				"TLV index %s out of range; "+
+					"must be in range [0, %d] for this image",
+				arg, len(img.Tlvs)-1))
+		}
+
+		if _, ok := idxMap[idx]; ok {
+			ImgmodUsage(nil, util.FmtNewtError(
+				"TLV index %d specified more than once", idx))
+		}
+		idxMap[idx] = struct{}{}
+
+		tlvIndices = append(tlvIndices, idx)
+	}
+
+	// Remove TLVs in reverse order to preserve index mapping.
+	sort.Sort(sort.Reverse(sort.IntSlice(tlvIndices)))
+	for _, idx := range tlvIndices {
+		tlv := img.Tlvs[idx]
+		util.StatusMessage(util.VERBOSITY_DEFAULT,
+			"Removing TLV%d: %s\n", idx, tlvStr(tlv))
+
+		img.Tlvs = append(img.Tlvs[0:idx], img.Tlvs[idx+1:]...)
+	}
+
+	if err := writeImage(img, outFilename); err != nil {
+		ImgmodUsage(nil, err)
+	}
+}
+
+func runRmsigsCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 1 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	inFilename := args[0]
+	outFilename, err := CalcOutFilename(inFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	img, err := readImage(inFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	cnt := img.RemoveTlvsIf(func(tlv image.ImageTlv) bool {
+		return tlv.Header.Type == image.IMAGE_TLV_KEYHASH ||
+			tlv.Header.Type == image.IMAGE_TLV_RSA2048 ||
+			tlv.Header.Type == image.IMAGE_TLV_ECDSA224 ||
+			tlv.Header.Type == image.IMAGE_TLV_ECDSA256
+	})
+
+	log.Debugf("Removed %d existing signatures", cnt)
+
+	if err := writeImage(img, outFilename); err != nil {
+		ImgmodUsage(nil, err)
+	}
+}
+
+func runHashableCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 1 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	if OptOutFilename == "" {
+		ImgmodUsage(cmd, util.FmtNewtError("--outfile (-o) option required"))
+	}
+
+	inFilename := args[0]
+	outFilename := OptOutFilename
+
+	img, err := readImage(inFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	f, err := os.Create(outFilename)
+	if err != nil {
+		ImgmodUsage(nil, util.ChildNewtError(err))
+	}
+	defer f.Close()
+
+	if err := binary.Write(f, binary.LittleEndian, &img.Header); err != nil {
+		ImgmodUsage(nil, util.FmtNewtError(
+			"Error writing image header: %s", err.Error()))
+	}
+	_, err = f.Write(img.Body)
+	if err != nil {
+		ImgmodUsage(nil, util.FmtNewtError(
+			"Error writing image body: %s", err.Error()))
+	}
+
+	util.StatusMessage(util.VERBOSITY_DEFAULT,
+		"Wrote hashable content to %s\n", outFilename)
+}
+
+func runAddsigCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 4 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	imgFilename := args[0]
+	keyFilename := args[1]
+	sigFilename := args[2]
+
+	sigType, err := util.AtoiNoOct(args[3])
+	if err != nil || sigType < 0 || sigType > 255 ||
+		!image.ImageTlvTypeIsSig(uint8(sigType)) {
+
+		ImgmodUsage(cmd, util.FmtNewtError(
+			"Invalid signature type: %s", args[3]))
+	}
+
+	outFilename, err := CalcOutFilename(imgFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	img, err := readImage(imgFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	keyData, err := ioutil.ReadFile(keyFilename)
+	if err != nil {
+		ImgmodUsage(cmd, util.FmtNewtError(
+			"Error reading key file: %s", err.Error()))
+	}
+
+	sigData, err := ioutil.ReadFile(sigFilename)
+	if err != nil {
+		ImgmodUsage(cmd, util.FmtNewtError(
+			"Error reading signature file: %s", err.Error()))
+	}
+
+	// ECDSA256 signatures need to be padded out to >=72 bytes.
+	if sigType == image.IMAGE_TLV_ECDSA256 {
+		sigData, err = iimg.PadEcdsa256Sig(sigData)
+		if err != nil {
+			ImgmodUsage(nil, err)
+		}
+	}
+
+	// Build and append key hash TLV.
+	keyHashTlv := image.BuildKeyHashTlv(keyData)
+	util.StatusMessage(util.VERBOSITY_DEFAULT, "Adding TLV%d (%s)\n",
+		len(img.Tlvs), tlvStr(keyHashTlv))
+	img.Tlvs = append(img.Tlvs, keyHashTlv)
+
+	// Build and append signature TLV.
+	sigTlv := image.ImageTlv{
+		Header: image.ImageTlvHdr{
+			Type: uint8(sigType),
+			Len:  uint16(len(sigData)),
+		},
+		Data: sigData,
+	}
+	util.StatusMessage(util.VERBOSITY_DEFAULT, "Adding TLV%d (%s)\n",
+		len(img.Tlvs), tlvStr(sigTlv))
+	img.Tlvs = append(img.Tlvs, sigTlv)
+
+	if err := writeImage(img, outFilename); err != nil {
+		ImgmodUsage(nil, err)
+	}
+}
+
+func runDecryptCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 2 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	imgFilename := args[0]
+	keyFilename := args[1]
+
+	outFilename, err := CalcOutFilename(imgFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	img, err := readImage(imgFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	keyBytes, err := ioutil.ReadFile(keyFilename)
+	if err != nil {
+		ImgmodUsage(cmd, util.FmtNewtError(
+			"Error reading key file: %s", err.Error()))
+	}
+
+	img, err = iimg.DecryptImage(img, keyBytes)
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	if err := writeImage(img, outFilename); err != nil {
+		ImgmodUsage(nil, err)
+	}
+}
+
+func runEncryptCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 2 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	imgFilename := args[0]
+	keyFilename := args[1]
+
+	outFilename, err := CalcOutFilename(imgFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	img, err := readImage(imgFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	keyBytes, err := ioutil.ReadFile(keyFilename)
+	if err != nil {
+		ImgmodUsage(cmd, util.FmtNewtError(
+			"Error reading key file: %s", err.Error()))
+	}
+
+	img, err = iimg.EncryptImage(img, keyBytes)
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	if err := writeImage(img, outFilename); err != nil {
+		ImgmodUsage(nil, err)
+	}
+}
+
+func AddImageCommands(cmd *cobra.Command) {
+	imageCmd := &cobra.Command{
+		Use:   "image",
+		Short: "Shows and manipulates Mynewt image (.img) files",
+		Run: func(cmd *cobra.Command, args []string) {
+			cmd.Usage()
+		},
+	}
+	cmd.AddCommand(imageCmd)
+
+	showCmd := &cobra.Command{
+		Use:   "show <img-file>",
+		Short: "Displays JSON describing a Mynewt image file",
+		Run:   runShowCmd,
+	}
+	imageCmd.AddCommand(showCmd)
+
+	briefCmd := &cobra.Command{
+		Use:   "brief <img-file>",
+		Short: "Displays brief text description of a Mynewt image file",
+		Run:   runBriefCmd,
+	}
+	imageCmd.AddCommand(briefCmd)
+
+	signCmd := &cobra.Command{
+		Use:   "sign <img-file> <priv-key-pem> [priv-key-pem...]",
+		Short: "Appends signatures to a Mynewt image file",
+		Run:   runSignCmd,
+	}
+
+	signCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o", "",
+		"File to write to")
+	signCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
+		"Replace input file")
+
+	imageCmd.AddCommand(signCmd)
+
+	addtlvsCmd := &cobra.Command{
+		Use: "addtlvs <img-file> <tlv-type> <data-filename> " +
+			"[tlv-type] [data-filename] [...]",
+		Short: "Adds the specified TLVs to a Mynewt image file",
+		Run:   runAddTlvsCmd,
+	}
+
+	addtlvsCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o", "",
+		"File to write to")
+	addtlvsCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
+		"Replace input file")
+
+	imageCmd.AddCommand(addtlvsCmd)
+
+	rmtlvsCmd := &cobra.Command{
+		Use:   "rmtlvs <img-file> <tlv-index> [tlv-index] [...]",
+		Short: "Removes the specified TLVs from a Mynewt image file",
+		Run:   runRmtlvsCmd,
+	}
+
+	rmtlvsCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o", "",
+		"File to write to")
+	rmtlvsCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
+		"Replace input file")
+
+	imageCmd.AddCommand(rmtlvsCmd)
+
+	rmsigsCmd := &cobra.Command{
+		Use:   "rmsigs",
+		Short: "Removes all signatures from a Mynewt image file",
+		Run:   runRmsigsCmd,
+	}
+
+	rmsigsCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o", "",
+		"File to write to")
+	rmsigsCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
+		"Replace input file")
+
+	imageCmd.AddCommand(rmsigsCmd)
+
+	hashableCmd := &cobra.Command{
+		Use:   "hashable <img-file>",
+		Short: "Removes all signatures from a Mynewt image file",
+		Run:   runHashableCmd,
+	}
+
+	hashableCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o",
+		"", "File to write to")
+
+	imageCmd.AddCommand(hashableCmd)
+
+	addsigCmd := &cobra.Command{
+		Use:   "addsig <image> <pub-key-der> <sig-der> <sig-tlv-type>",
+		Short: "Adds a signature to a Mynewt image file",
+		Run:   runAddsigCmd,
+	}
+
+	addsigCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o",
+		"", "File to write to")
+	addsigCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
+		"Replace input file")
+
+	imageCmd.AddCommand(addsigCmd)
+
+	decryptCmd := &cobra.Command{
+		Use:   "decrypt <image> <priv-key-der>",
+		Short: "Decrypts an encrypted Mynewt image file",
+		Run:   runDecryptCmd,
+	}
+
+	decryptCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o",
+		"", "File to write to")
+	decryptCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
+		"Replace input file")
+
+	imageCmd.AddCommand(decryptCmd)
+
+	encryptCmd := &cobra.Command{
+		Use:   "encrypt <image> <priv-key-der>",
+		Short: "Encrypts a Mynewt image file",
+		Run:   runEncryptCmd,
+	}
+
+	encryptCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o",
+		"", "File to write to")
+	encryptCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
+		"Replace input file")
+
+	imageCmd.AddCommand(encryptCmd)
+}
diff --git a/cli/mfg_cmds.go b/cli/mfg_cmds.go
new file mode 100644
index 0000000..a89902b
--- /dev/null
+++ b/cli/mfg_cmds.go
@@ -0,0 +1,623 @@
+/**
+ * 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 cli
+
+import (
+	"encoding/hex"
+	"fmt"
+	"io/ioutil"
+	"os"
+
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+
+	"mynewt.apache.org/newt/artifact/flash"
+	"mynewt.apache.org/newt/artifact/manifest"
+	"mynewt.apache.org/newt/artifact/mfg"
+	"mynewt.apache.org/newt/artifact/misc"
+	"mynewt.apache.org/newt/artifact/sec"
+	"mynewt.apache.org/imgmod/imfg"
+	"mynewt.apache.org/newt/util"
+)
+
+const MAX_SIG_LEN = 1024 // Bytes.
+
+func readMfgBin(filename string) ([]byte, error) {
+	bin, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, util.FmtChildNewtError(err,
+			"Failed to read manufacturing image: %s", err.Error())
+	}
+
+	return bin, nil
+}
+
+func readManifest(mfgDir string) (manifest.MfgManifest, error) {
+	return manifest.ReadMfgManifest(mfgDir + "/" + mfg.MANIFEST_FILENAME)
+}
+
+func extractFlashAreas(mman manifest.MfgManifest) ([]flash.FlashArea, error) {
+	areas := flash.SortFlashAreasByDevOff(mman.FlashAreas)
+
+	if len(areas) == 0 {
+		ImgmodUsage(nil, util.FmtNewtError(
+			"Boot loader manifest does not contain flash map"))
+	}
+
+	overlaps, conflicts := flash.DetectErrors(areas)
+	if len(overlaps) > 0 || len(conflicts) > 0 {
+		return nil, util.NewNewtError(flash.ErrorText(overlaps, conflicts))
+	}
+
+	if err := imfg.VerifyAreas(areas); err != nil {
+		return nil, err
+	}
+
+	log.Debugf("Successfully read flash areas: %+v", areas)
+	return areas, nil
+}
+
+func createNameBlobMap(binDir string,
+	areas []flash.FlashArea) (imfg.NameBlobMap, error) {
+
+	mm := imfg.NameBlobMap{}
+
+	for _, area := range areas {
+		filename := fmt.Sprintf("%s/%s.bin", binDir, area.Name)
+		bin, err := readMfgBin(filename)
+		if err != nil {
+			if !util.IsNotExist(err) {
+				return nil, util.ChildNewtError(err)
+			}
+		} else {
+			mm[area.Name] = bin
+		}
+	}
+
+	return mm, nil
+}
+
+func runMfgShowCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 2 {
+		ImgmodUsage(cmd, nil)
+	}
+	inFilename := args[0]
+
+	metaEndOff, err := util.AtoiNoOct(args[1])
+	if err != nil {
+		ImgmodUsage(cmd, util.FmtNewtError(
+			"invalid meta offset \"%s\"", args[1]))
+	}
+
+	bin, err := readMfgBin(inFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	m, err := mfg.Parse(bin, metaEndOff, 0xff)
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	if m.Meta == nil {
+		util.StatusMessage(util.VERBOSITY_DEFAULT,
+			"Manufacturing image %s does not contain an MMR\n", inFilename)
+	} else {
+		s, err := m.Meta.Json(metaEndOff)
+		if err != nil {
+			ImgmodUsage(nil, err)
+		}
+		util.StatusMessage(util.VERBOSITY_DEFAULT,
+			"Manufacturing image %s contains an MMR with "+
+				"the following properties:\n%s\n", inFilename, s)
+	}
+}
+
+func runSplitCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 2 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	mfgDir := args[0]
+	outDir := args[1]
+
+	mm, err := readManifest(mfgDir)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	areas, err := extractFlashAreas(mm)
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	binPath := fmt.Sprintf("%s/%s", mfgDir, mm.BinPath)
+	bin, err := readMfgBin(binPath)
+	if err != nil {
+		ImgmodUsage(cmd, util.FmtNewtError(
+			"Failed to read \"%s\": %s", binPath, err.Error()))
+	}
+
+	nbmap, err := imfg.Split(bin, mm.Device, areas, 0xff)
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	if err := os.Mkdir(outDir, os.ModePerm); err != nil {
+		ImgmodUsage(nil, util.ChildNewtError(err))
+	}
+
+	for name, data := range nbmap {
+		filename := fmt.Sprintf("%s/%s.bin", outDir, name)
+		if err := WriteFile(data, filename); err != nil {
+			ImgmodUsage(nil, err)
+		}
+	}
+
+	mfgDstDir := fmt.Sprintf("%s/mfg", outDir)
+	if err := CopyDir(mfgDir, mfgDstDir); err != nil {
+		ImgmodUsage(nil, err)
+	}
+}
+
+func runJoinCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 2 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	splitDir := args[0]
+	outDir := args[1]
+
+	if util.NodeExist(outDir) {
+		ImgmodUsage(nil, util.FmtNewtError(
+			"Destination \"%s\" already exists", outDir))
+	}
+
+	mm, err := readManifest(splitDir + "/mfg")
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+	areas, err := extractFlashAreas(mm)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	nbmap, err := createNameBlobMap(splitDir, areas)
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	bin, err := imfg.Join(nbmap, 0xff, areas)
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	m, err := mfg.Parse(bin, mm.Meta.EndOffset, 0xff)
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	infos, err := ioutil.ReadDir(splitDir + "/mfg")
+	if err != nil {
+		ImgmodUsage(nil, util.FmtNewtError(
+			"Error reading source mfg directory: %s", err.Error()))
+	}
+	for _, info := range infos {
+		if info.Name() != mfg.MFG_IMG_FILENAME {
+			src := splitDir + "/mfg/" + info.Name()
+			dst := outDir + "/" + info.Name()
+			if info.IsDir() {
+				err = CopyDir(src, dst)
+			} else {
+				err = CopyFile(src, dst)
+			}
+			if err != nil {
+				ImgmodUsage(nil, err)
+			}
+		}
+	}
+
+	finalBin, err := m.Bytes(0xff)
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	binPath := fmt.Sprintf("%s/%s", outDir, mfg.MFG_IMG_FILENAME)
+	if err := WriteFile(finalBin, binPath); err != nil {
+		ImgmodUsage(nil, err)
+	}
+}
+
+func genSwapKeyCmd(cmd *cobra.Command, args []string, isKek bool) {
+	if len(args) < 3 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	mfgimgFilename := args[0]
+	okeyFilename := args[1]
+	nkeyFilename := args[2]
+
+	outFilename, err := CalcOutFilename(mfgimgFilename)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	bin, err := readMfgBin(mfgimgFilename)
+	if err != nil {
+		ImgmodUsage(cmd, util.FmtNewtError(
+			"Failed to read mfgimg file: %s", err.Error()))
+	}
+
+	okey, err := ioutil.ReadFile(okeyFilename)
+	if err != nil {
+		ImgmodUsage(cmd, util.FmtNewtError(
+			"Failed to read old key der: %s", err.Error()))
+	}
+
+	nkey, err := ioutil.ReadFile(nkeyFilename)
+	if err != nil {
+		ImgmodUsage(cmd, util.FmtNewtError(
+			"Failed to read new key der: %s", err.Error()))
+	}
+
+	if isKek {
+		err = imfg.ReplaceKek(bin, okey, nkey)
+	} else {
+		err = imfg.ReplaceIsk(bin, okey, nkey)
+	}
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	if err := WriteFile(bin, outFilename); err != nil {
+		ImgmodUsage(nil, err)
+	}
+}
+
+func runSwapIskCmd(cmd *cobra.Command, args []string) {
+	genSwapKeyCmd(cmd, args, false)
+}
+
+func runSwapKekCmd(cmd *cobra.Command, args []string) {
+	genSwapKeyCmd(cmd, args, true)
+}
+
+func runMfgHashableCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 1 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	if OptOutFilename == "" {
+		ImgmodUsage(cmd, util.FmtNewtError("--outfile (-o) option required"))
+	}
+
+	mfgDir := args[0]
+	outFilename := OptOutFilename
+
+	// Read manifest and mfgimg.bin.
+	mman, err := readManifest(mfgDir)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	binPath := fmt.Sprintf("%s/%s", mfgDir, mman.BinPath)
+	bin, err := readMfgBin(binPath)
+	if err != nil {
+		ImgmodUsage(cmd, util.FmtNewtError(
+			"Failed to read \"%s\": %s", binPath, err.Error()))
+	}
+
+	metaOff := -1
+	if mman.Meta != nil {
+		metaOff = mman.Meta.EndOffset
+	}
+	m, err := mfg.Parse(bin, metaOff, 0xff)
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+	// Zero-out hash so that the hash can be recalculated.
+	m.Meta.ClearHash()
+
+	// Write hashable content to disk.
+	newBin, err := m.Bytes(0xff)
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+	if err := WriteFile(newBin, outFilename); err != nil {
+		ImgmodUsage(nil, err)
+	}
+}
+
+func runRehashCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 1 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	mfgDir := args[0]
+
+	outDir, err := CalcOutFilename(mfgDir)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	// Read manifest and mfgimg.bin.
+	mman, err := readManifest(mfgDir)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	binPath := fmt.Sprintf("%s/%s", mfgDir, mman.BinPath)
+	bin, err := readMfgBin(binPath)
+	if err != nil {
+		ImgmodUsage(cmd, util.FmtNewtError(
+			"Failed to read \"%s\": %s", binPath, err.Error()))
+	}
+
+	// Calculate accurate hash.
+	metaOff := -1
+	if mman.Meta != nil {
+		metaOff = mman.Meta.EndOffset
+	}
+	m, err := mfg.Parse(bin, metaOff, 0xff)
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	if err := m.RecalcHash(0xff); err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	hash, err := m.Hash()
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	// Update manifest.
+	mman.MfgHash = misc.HashString(hash)
+
+	// Write new artifacts.
+	if err := EnsureOutDir(mfgDir, outDir); err != nil {
+		ImgmodUsage(nil, err)
+	}
+	binPath = fmt.Sprintf("%s/%s", outDir, mman.BinPath)
+
+	newBin, err := m.Bytes(0xff)
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+	if err := WriteFile(newBin, binPath); err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	json, err := mman.MarshalJson()
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	manPath := fmt.Sprintf("%s/%s", outDir, mfg.MANIFEST_FILENAME)
+	if err := WriteFile(json, manPath); err != nil {
+		ImgmodUsage(nil, err)
+	}
+}
+
+func runRmsigsMfgCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 1 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	mfgDir := args[0]
+
+	outDir, err := CalcOutFilename(mfgDir)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	// Read manifest.
+	mman, err := readManifest(mfgDir)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	// Update manifest.
+	mman.Signatures = nil
+
+	// Write new artifacts.
+	if err := EnsureOutDir(mfgDir, outDir); err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	json, err := mman.MarshalJson()
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	manPath := fmt.Sprintf("%s/%s", outDir, mfg.MANIFEST_FILENAME)
+	if err := WriteFile(json, manPath); err != nil {
+		ImgmodUsage(nil, err)
+	}
+}
+
+func runAddsigMfgCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 3 {
+		ImgmodUsage(cmd, nil)
+	}
+
+	mfgDir := args[0]
+	keyFilename := args[1]
+	sigFilename := args[2]
+
+	outDir, err := CalcOutFilename(mfgDir)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	// Read manifest.
+	mman, err := readManifest(mfgDir)
+	if err != nil {
+		ImgmodUsage(cmd, err)
+	}
+
+	// Read public key.
+	keyBytes, err := ioutil.ReadFile(keyFilename)
+	if err != nil {
+		ImgmodUsage(cmd, util.FmtNewtError(
+			"Error reading key file: %s", err.Error()))
+	}
+
+	// Read signature.
+	sig, err := ioutil.ReadFile(sigFilename)
+	if err != nil {
+		ImgmodUsage(cmd, util.FmtChildNewtError(err,
+			"Failed to read signature: %s", err.Error()))
+	}
+	if len(sig) > MAX_SIG_LEN {
+		ImgmodUsage(nil, util.FmtNewtError(
+			"signature larger than arbitrary maximum length (%d > %d)",
+			len(sig), MAX_SIG_LEN))
+	}
+
+	// Update manifest.
+	mman.Signatures = append(mman.Signatures, manifest.MfgManifestSig{
+		Key: hex.EncodeToString(sec.RawKeyHash(keyBytes)),
+		Sig: hex.EncodeToString(sig),
+	})
+
+	// Write new artifacts.
+	if err := EnsureOutDir(mfgDir, outDir); err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	json, err := mman.MarshalJson()
+	if err != nil {
+		ImgmodUsage(nil, err)
+	}
+
+	manPath := fmt.Sprintf("%s/%s", outDir, mfg.MANIFEST_FILENAME)
+	if err := WriteFile(json, manPath); err != nil {
+		ImgmodUsage(nil, err)
+	}
+}
+
+func AddMfgCommands(cmd *cobra.Command) {
+	mfgCmd := &cobra.Command{
+		Use:   "mfg",
+		Short: "Manipulates Mynewt manufacturing images",
+		Run: func(cmd *cobra.Command, args []string) {
+			cmd.Usage()
+		},
+	}
+	cmd.AddCommand(mfgCmd)
+
+	showCmd := &cobra.Command{
+		Use:   "show <mfgimg.bin> <meta-end-offset>",
+		Short: "Displays JSON describing a manufacturing image",
+		Run:   runMfgShowCmd,
+	}
+
+	mfgCmd.AddCommand(showCmd)
+
+	splitCmd := &cobra.Command{
+		Use:   "split <mfgimage-dir> <out-dir>",
+		Short: "Splits a Mynewt mfg section into several files",
+		Run:   runSplitCmd,
+	}
+
+	mfgCmd.AddCommand(splitCmd)
+
+	joinCmd := &cobra.Command{
+		Use:   "join <split-dir> <out-dir>",
+		Short: "Joins a split mfg section into a single file",
+		Run:   runJoinCmd,
+	}
+
+	mfgCmd.AddCommand(joinCmd)
+
+	swapIskCmd := &cobra.Command{
+		Use:   "swapisk <mfgimg-bin> <cur-key-der> <new-key-der>",
+		Short: "Replaces an image-signing key in a manufacturing image",
+		Run:   runSwapIskCmd,
+	}
+
+	swapIskCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o",
+		"", "File to write to")
+	swapIskCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
+		"Replace input file")
+
+	mfgCmd.AddCommand(swapIskCmd)
+
+	swapKekCmd := &cobra.Command{
+		Use:   "swapkek <mfgimg-bin> <cur-key-der> <new-key-der>",
+		Short: "Replaces a key-encrypting key in a manufacturing image",
+		Run:   runSwapKekCmd,
+	}
+
+	swapKekCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o",
+		"", "File to write to")
+	swapKekCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
+		"Replace input file")
+
+	mfgCmd.AddCommand(swapKekCmd)
+
+	hashableCmd := &cobra.Command{
+		Use:   "hashable <mfgimage-dir>",
+		Short: "Extracts the hashable / signable content of an mfgimage",
+		Run:   runMfgHashableCmd,
+	}
+	hashableCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o",
+		"", "File to write to")
+
+	mfgCmd.AddCommand(hashableCmd)
+
+	rehashCmd := &cobra.Command{
+		Use:   "rehash <mfgimage-dir>",
+		Short: "Replaces an outdated mfgimage hash with an accurate one",
+		Run:   runRehashCmd,
+	}
+	rehashCmd.PersistentFlags().StringVarP(&OptOutFilename, "outdir", "o",
+		"", "Directory to write to")
+	rehashCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
+		"Replace input files")
+
+	mfgCmd.AddCommand(rehashCmd)
+
+	rmsigsCmd := &cobra.Command{
+		Use:   "rmsigs <mfgimage-dir>",
+		Short: "Removes all signatures from an mfgimage's manifest",
+		Run:   runRmsigsMfgCmd,
+	}
+	rmsigsCmd.PersistentFlags().StringVarP(&OptOutFilename, "outdir", "o",
+		"", "Directory to write to")
+	rmsigsCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
+		"Replace input files")
+
+	mfgCmd.AddCommand(rmsigsCmd)
+
+	addsigCmd := &cobra.Command{
+		Use:   "addsig <mfgimage-dir> <pub-key-der> <sig-der>",
+		Short: "Adds a signature to an mfgimage's manifest",
+		Run:   runAddsigMfgCmd,
+	}
+	addsigCmd.PersistentFlags().StringVarP(&OptOutFilename, "outdir", "o",
+		"", "Directory to write to")
+	addsigCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
+		"Replace input files")
+
+	mfgCmd.AddCommand(addsigCmd)
+}
diff --git a/cli/util.go b/cli/util.go
new file mode 100644
index 0000000..a2729ad
--- /dev/null
+++ b/cli/util.go
@@ -0,0 +1,111 @@
+/**
+ * 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 cli
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+
+	"mynewt.apache.org/newt/util"
+)
+
+var OptOutFilename string
+var OptInPlace bool
+
+func ImgmodUsage(cmd *cobra.Command, err error) {
+	if err != nil {
+		sErr := err.(*util.NewtError)
+		log.Debugf("%s", sErr.StackTrace)
+		fmt.Fprintf(os.Stderr, "Error: %s\n", sErr.Text)
+	}
+
+	if cmd != nil {
+		fmt.Printf("%s - ", cmd.Name())
+		cmd.Help()
+	}
+	os.Exit(1)
+}
+
+func CalcOutFilename(inFilename string) (string, error) {
+	if OptOutFilename != "" {
+		if OptInPlace {
+			return "", util.FmtNewtError(
+				"Only one of --outfile (-o) or --inplace (-i) options allowed")
+		}
+
+		return OptOutFilename, nil
+	}
+
+	if !OptInPlace {
+		return "", util.FmtNewtError(
+			"--outfile (-o) or --inplace (-i) option required")
+	}
+
+	return inFilename, nil
+}
+
+func CopyDir(src string, dst string) error {
+	if err := util.CopyDir(src, dst); err != nil {
+		return util.FmtNewtError(
+			"Failed to copy directory \"%s\" to \"%s\": %s",
+			src, dst, err.Error())
+	}
+
+	util.StatusMessage(util.VERBOSITY_DEFAULT,
+		"Copied directory \"%s\" to \"%s\"\n", src, dst)
+	return nil
+}
+
+func EnsureOutDir(inDir, outDir string) error {
+	if inDir != outDir {
+		// Not an in-place operation; copy input directory.
+		if err := CopyDir(inDir, outDir); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func CopyFile(src string, dst string) error {
+	if err := util.CopyFile(src, dst); err != nil {
+		return util.FmtNewtError(
+			"Failed to copy file \"%s\" to \"%s\": %s",
+			src, dst, err.Error())
+	}
+
+	util.StatusMessage(util.VERBOSITY_DEFAULT,
+		"Copied file \"%s\" to \"%s\"\n", src, dst)
+	return nil
+}
+
+func WriteFile(data []byte, filename string) error {
+	if err := ioutil.WriteFile(filename, data, os.ModePerm); err != nil {
+		return util.FmtNewtError(
+			"Failed to write file \"%s\": %s", filename, err.Error())
+	}
+
+	util.StatusMessage(util.VERBOSITY_DEFAULT, "Wrote file \"%s\"\n", filename)
+	return nil
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..a8820c4
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,7 @@
+module mynewt.apache.org/imgmod
+
+require (
+	github.com/sirupsen/logrus v1.4.0
+	github.com/spf13/cobra v0.0.3
+	mynewt.apache.org/newt v0.0.0-20190314184600-c5632c20daf3
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..d545ff0
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,29 @@
+github.com/NickBall/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5 h1:5BIUS5hwyLM298mOf8e8TEgD3cCYqc86uaJdQCYZo/o=
+github.com/NickBall/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5/go.mod h1:w5D10RxC0NmPYxmQ438CC1S07zaC1zpvuNW7s5sUk2Q=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.4.0 h1:yKenngtzGh+cUSSh6GWbxW2abRqhYUSR/t/6+2QqNvE=
+github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
+github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
+github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/sys v0.0.0-20180707002001-3c6ecd8f22c6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+mynewt.apache.org/newt v0.0.0-20190314184600-c5632c20daf3 h1:U9HanOWwCKSPTCZu7ti0MlmMlkWpTiR856BR2bRzHaU=
+mynewt.apache.org/newt v0.0.0-20190314184600-c5632c20daf3/go.mod h1:lFsPYOHxMMWA11pydOeh0GVFiXtx0A9VnzOQ6SiRR88=
diff --git a/iimg/lvimg.go b/iimg/lvimg.go
new file mode 100644
index 0000000..bacefe9
--- /dev/null
+++ b/iimg/lvimg.go
@@ -0,0 +1,189 @@
+/**
+ * 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 iimg
+
+import (
+	"encoding/hex"
+	"fmt"
+	"strings"
+
+	"mynewt.apache.org/newt/artifact/image"
+	"mynewt.apache.org/newt/artifact/sec"
+	"mynewt.apache.org/newt/util"
+)
+
+func GetDupSigs(img image.Image) []string {
+	m := map[string]struct{}{}
+	var dups []string
+
+	for _, tlv := range img.Tlvs {
+		if tlv.Header.Type == image.IMAGE_TLV_KEYHASH {
+			h := hex.EncodeToString(tlv.Data)
+			if _, ok := m[h]; ok {
+				dups = append(dups, h)
+			} else {
+				m[h] = struct{}{}
+			}
+		}
+	}
+
+	return dups
+}
+
+func DetectInvalidSigTlvs(img image.Image) error {
+	var errStrs []string
+	addErr := func(format string, args ...interface{}) {
+		s := fmt.Sprintf(format, args...)
+		errStrs = append(errStrs, s)
+	}
+
+	prevIsHash := false
+	for i, tlv := range img.Tlvs {
+		curIsHash := tlv.Header.Type == image.IMAGE_TLV_KEYHASH
+		curIsSig := image.ImageTlvTypeIsSig(tlv.Header.Type)
+		isLast := i == len(img.Tlvs)-1
+
+		if prevIsHash && !curIsSig {
+			prevTlv := img.Tlvs[i-1]
+			addErr("TLV%d (%s) not immediately followed by signature TLV",
+				i-1, image.ImageTlvTypeName(prevTlv.Header.Type))
+		} else if curIsHash && isLast {
+			addErr("TLV%d (%s) not immediately followed by signature TLV",
+				i, image.ImageTlvTypeName(tlv.Header.Type))
+		} else if !prevIsHash && curIsSig {
+			addErr("TLV%d (%s) not immediately preceded by key hash TLV",
+				i, image.ImageTlvTypeName(tlv.Header.Type))
+		}
+
+		prevIsHash = curIsHash
+	}
+
+	if len(errStrs) > 0 {
+		return util.FmtNewtError("%s", strings.Join(errStrs, "\n"))
+	}
+
+	return nil
+}
+
+func VerifyImage(img image.Image) error {
+	if len(img.Tlvs) == 0 || img.Tlvs[0].Header.Type != image.IMAGE_TLV_SHA256 {
+		return util.FmtNewtError("First TLV must be SHA256")
+	}
+
+	if err := DetectInvalidSigTlvs(img); err != nil {
+		return err
+	}
+
+	if dups := GetDupSigs(img); len(dups) > 0 {
+		s := "Duplicate signatures detected:"
+		for _, d := range dups {
+			s += fmt.Sprintf("\n    %s", d)
+		}
+
+		return util.FmtNewtError("%s", s)
+	}
+
+	return nil
+}
+
+func PadEcdsa256Sig(sig []byte) ([]byte, error) {
+	if len(sig) < 70 {
+		return nil, util.FmtNewtError(
+			"Invalid ECDSA256 signature; length (%d) less than 70", len(sig))
+	}
+
+	if len(sig) < 72 {
+		sig = append(sig, []byte{0x00, 0x00}...)
+	}
+
+	return sig, nil
+}
+
+// XXX: Only RSA supported for now.
+func ExtractSecret(img *image.Image) ([]byte, error) {
+	tlvs := img.RemoveTlvsWithType(image.IMAGE_TLV_ENC_RSA)
+	if len(tlvs) != 1 {
+		return nil, util.FmtNewtError(
+			"Image contains invalid count of ENC_RSA TLVs: %d; must contain 1",
+			len(tlvs))
+	}
+
+	return tlvs[0].Data, nil
+}
+
+// XXX: Only RSA supported for now.
+func DecryptImage(img image.Image, privKeBytes []byte) (image.Image, error) {
+	cipherSecret, err := ExtractSecret(&img)
+	if err != nil {
+		return img, err
+	}
+
+	privKe, err := sec.ParsePrivKeDer(privKeBytes)
+	if err != nil {
+		return img, err
+	}
+
+	plainSecret, err := sec.DecryptSecretRsa(privKe, cipherSecret)
+	if err != nil {
+		return img, err
+	}
+
+	body, err := sec.EncryptAES(img.Body, plainSecret)
+	if err != nil {
+		return img, err
+	}
+
+	img.Body = body
+	return img, nil
+}
+
+func EncryptImage(img image.Image, pubKeBytes []byte) (image.Image, error) {
+	tlvp, err := img.FindUniqueTlv(image.IMAGE_TLV_ENC_RSA)
+	if err != nil {
+		return img, err
+	}
+	if tlvp != nil {
+		return img, util.FmtNewtError("Image already contains an ENC_RSA TLV")
+	}
+
+	plainSecret, err := image.GeneratePlainSecret()
+	if err != nil {
+		return img, err
+	}
+
+	cipherSecret, err := image.GenerateCipherSecret(pubKeBytes, plainSecret)
+	if err != nil {
+		return img, err
+	}
+
+	body, err := sec.EncryptAES(img.Body, plainSecret)
+	if err != nil {
+		return img, err
+	}
+	img.Body = body
+
+	tlv, err := image.GenerateEncTlv(cipherSecret)
+	if err != nil {
+		return img, err
+	}
+	img.Tlvs = append(img.Tlvs, tlv)
+
+	return img, nil
+}
diff --git a/imfg/lvmfg.go b/imfg/lvmfg.go
new file mode 100644
index 0000000..9472ee1
--- /dev/null
+++ b/imfg/lvmfg.go
@@ -0,0 +1,249 @@
+/**
+ * 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 imfg
+
+import (
+	"bytes"
+	"encoding/binary"
+	"fmt"
+	"sort"
+	"strings"
+
+	"mynewt.apache.org/newt/artifact/flash"
+	"mynewt.apache.org/newt/artifact/mfg"
+	"mynewt.apache.org/newt/util"
+)
+
+type NameBlobMap map[string][]byte
+
+func (to NameBlobMap) Union(from NameBlobMap) {
+	for k, v := range from {
+		to[k] = v
+	}
+}
+
+func errInvalidArea(areaName string, format string,
+	args ...interface{}) error {
+
+	suffix := fmt.Sprintf(format, args...)
+	return util.FmtNewtError("Invalid flash area \"%s\": %s", areaName, suffix)
+}
+
+func verifyArea(area flash.FlashArea, minOffset int) error {
+	if area.Offset < minOffset {
+		return errInvalidArea(area.Name, "invalid offset %d; expected >= %d",
+			area.Offset, minOffset)
+	}
+
+	if area.Size < 0 {
+		return errInvalidArea(area.Name, "invalid size %d", area.Size)
+	}
+
+	return nil
+}
+
+// `areas` must be sorted by device ID, then by offset.
+func VerifyAreas(areas []flash.FlashArea) error {
+	prevDevice := -1
+	off := 0
+	for _, area := range areas {
+		if area.Device != prevDevice {
+			off = 0
+		}
+
+		if err := verifyArea(area, off); err != nil {
+			return err
+		}
+		off += area.Size
+	}
+
+	return nil
+}
+
+func Split(mfgBin []byte, deviceNum int,
+	areas []flash.FlashArea, eraseVal byte) (NameBlobMap, error) {
+
+	mm := NameBlobMap{}
+
+	for _, area := range areas {
+		if _, ok := mm[area.Name]; ok {
+			return nil, util.FmtNewtError(
+				"two or more flash areas with same name: \"%s\"", area.Name)
+		}
+
+		if area.Device == deviceNum {
+			var areaBin []byte
+			if area.Offset < len(mfgBin) {
+				end := area.Offset + area.Size
+				overflow := end - len(mfgBin)
+				if overflow > 0 {
+					end -= overflow
+				}
+				areaBin = mfgBin[area.Offset:end]
+			}
+
+			mm[area.Name] = StripPadding(areaBin, eraseVal)
+		}
+	}
+
+	return mm, nil
+}
+
+// `areas` must be sorted by device ID, then by offset.
+func Join(mm NameBlobMap, eraseVal byte,
+	areas []flash.FlashArea) ([]byte, error) {
+
+	// Keep track of which areas we haven't seen yet.
+	unseen := map[string]struct{}{}
+	for name, _ := range mm {
+		unseen[name] = struct{}{}
+	}
+
+	joined := []byte{}
+	for _, area := range areas {
+		bin := mm[area.Name]
+
+		// Only include this area if it belongs to the mfg image we are
+		// joining.
+		if bin != nil {
+			delete(unseen, area.Name)
+
+			// Pad remainder of previous area in this section.
+			padSize := area.Offset - len(joined)
+			if padSize > 0 {
+				joined = mfg.AddPadding(joined, eraseVal, padSize)
+			}
+
+			// Append data to joined binary.
+			binstr := ""
+			if len(bin) >= 4 {
+				binstr = fmt.Sprintf("%x", bin[:4])
+			}
+			util.StatusMessage(util.VERBOSITY_DEFAULT,
+				"inserting %s (%s) at offset %d (0x%x)\n",
+				area.Name, binstr, len(joined), len(joined))
+			joined = append(joined, bin...)
+		}
+	}
+
+	// Ensure we processed every area in the map.
+	if len(unseen) > 0 {
+		names := []string{}
+		for name, _ := range unseen {
+			names = append(names, name)
+		}
+		sort.Strings(names)
+
+		return nil, util.FmtNewtError(
+			"unprocessed flash areas: %s", strings.Join(names, ", "))
+	}
+
+	// Strip padding from the end of the joined binary.
+	joined = StripPadding(joined, eraseVal)
+
+	return joined, nil
+}
+
+func replaceKey(mfgBin []byte, okey []byte, nkey []byte) (int, error) {
+	if len(okey) > len(mfgBin) {
+		return 0, util.FmtNewtError(
+			"key longer than flash section (%d > %d)", len(okey), len(mfgBin))
+	}
+
+	idx := bytes.Index(mfgBin, okey)
+	if idx == -1 {
+		return 0, util.FmtNewtError("old key not present in flash section")
+	}
+
+	lastIdx := bytes.LastIndex(mfgBin, okey)
+	if idx != lastIdx {
+		return 0, util.FmtNewtError(
+			"multiple instances of old key in flash section")
+	}
+
+	util.StatusMessage(util.VERBOSITY_VERBOSE,
+		"Replacing key at offset %d\n", idx)
+
+	copy(mfgBin[idx:idx+len(okey)], nkey)
+
+	return idx, nil
+}
+
+func ReplaceIsk(mfgBin []byte, okey []byte, nkey []byte) error {
+	if len(nkey) != len(okey) {
+		return util.FmtNewtError(
+			"key lengths differ (%d != %d)", len(nkey), len(okey))
+	}
+
+	if _, err := replaceKey(mfgBin, okey, nkey); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func ReplaceKek(mfgBin []byte, okey []byte, nkey []byte) error {
+	if len(nkey) > len(okey) {
+		return util.FmtNewtError(
+			"new key longer than old key (%d > %d)", len(nkey), len(okey))
+	}
+
+	keyIdx, err := replaceKey(mfgBin, okey, nkey)
+	if err != nil {
+		return err
+	}
+
+	// The key length is an unsigned int immediately prior to the key.
+	var kl uint32
+	klIdx := keyIdx - 4
+	buf := bytes.NewBuffer(mfgBin[klIdx : klIdx+4])
+	if err := binary.Read(buf, binary.LittleEndian, &kl); err != nil {
+		return util.ChildNewtError(err)
+	}
+
+	if int(kl) != len(okey) {
+		return util.FmtNewtError(
+			"embedded key length (off=%d) has unexpected value; "+
+				"want=%d have=%d",
+			klIdx, len(okey), kl)
+	}
+
+	buf = &bytes.Buffer{}
+	kl = uint32(len(nkey))
+	if err := binary.Write(buf, binary.LittleEndian, kl); err != nil {
+		return util.ChildNewtError(err)
+	}
+
+	copy(mfgBin[klIdx:klIdx+4], buf.Bytes())
+
+	return nil
+}
+
+func StripPadding(b []byte, eraseVal byte) []byte {
+	var pad int
+	for pad = 0; pad < len(b); pad++ {
+		off := len(b) - pad - 1
+		if b[off] != eraseVal {
+			break
+		}
+	}
+
+	return b[:len(b)-pad]
+}
diff --git a/imgmod.go b/imgmod.go
new file mode 100644
index 0000000..1677525
--- /dev/null
+++ b/imgmod.go
@@ -0,0 +1,100 @@
+/**
+ * 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 main
+
+import (
+	"fmt"
+
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+
+	"mynewt.apache.org/imgmod/cli"
+	"mynewt.apache.org/newt/util"
+)
+
+var ImgmodLogLevel log.Level
+var imgmodSilent bool
+var imgmodQuiet bool
+var imgmodVerbose bool
+var imgmodVersion = "0.0.2"
+
+func main() {
+	imgmodHelpText := ""
+	imgmodHelpEx := ""
+
+	logLevelStr := ""
+	imgmodCmd := &cobra.Command{
+		Use:     "imgmod",
+		Short:   "imgmod is a tool to view and modify Mynewt image files",
+		Long:    imgmodHelpText,
+		Example: imgmodHelpEx,
+		PersistentPreRun: func(cmd *cobra.Command, args []string) {
+			verbosity := util.VERBOSITY_DEFAULT
+			if imgmodSilent {
+				verbosity = util.VERBOSITY_SILENT
+			} else if imgmodQuiet {
+				verbosity = util.VERBOSITY_QUIET
+			} else if imgmodVerbose {
+				verbosity = util.VERBOSITY_VERBOSE
+			}
+
+			logLevel, err := log.ParseLevel(logLevelStr)
+			if err != nil {
+				cli.ImgmodUsage(nil, util.ChildNewtError(err))
+			}
+			ImgmodLogLevel = logLevel
+
+			if err := util.Init(ImgmodLogLevel, "", verbosity); err != nil {
+				cli.ImgmodUsage(nil, err)
+			}
+		},
+
+		Run: func(cmd *cobra.Command, args []string) {
+			cmd.Help()
+		},
+	}
+
+	imgmodCmd.PersistentFlags().BoolVarP(&imgmodVerbose, "verbose", "v", false,
+		"Enable verbose output when executing commands")
+	imgmodCmd.PersistentFlags().BoolVarP(&imgmodQuiet, "quiet", "q", false,
+		"Be quiet; only display error output")
+	imgmodCmd.PersistentFlags().BoolVarP(&imgmodSilent, "silent", "s", false,
+		"Be silent; don't output anything")
+	imgmodCmd.PersistentFlags().StringVarP(&logLevelStr, "loglevel", "l",
+		"WARN", "Log level")
+
+	versHelpText := `Display the imgmod version number`
+	versHelpEx := "  imgmod version"
+	versCmd := &cobra.Command{
+		Use:     "version",
+		Short:   "Display the imgmod version number",
+		Long:    versHelpText,
+		Example: versHelpEx,
+		Run: func(cmd *cobra.Command, args []string) {
+			fmt.Printf("%s\n", imgmodVersion)
+		},
+	}
+	imgmodCmd.AddCommand(versCmd)
+
+	cli.AddImageCommands(imgmodCmd)
+	cli.AddMfgCommands(imgmodCmd)
+
+	imgmodCmd.Execute()
+}