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/06/10 23:55:30 UTC

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

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()
+}