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:29 UTC

[mynewt-imgmod] branch master updated (8098e32 -> be4ad90)

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.


    from 8098e32  Merge pull request #1 from ccollins476ad/encrypt-flag
     new 95e000c  Rename old "larva" files
     new 60423a3  Warn when `hashable` is used on an encrypted image
     new 743f256  Fix case: addtlvs --> addTlvs
     new 4777a82  Distinguish partial {en|de}crypt from full
     new 0dc0104  Depend on latest mynewt.apache.org/newt
     new be4ad90  Merge pull request #2 from ccollins476ad/crypt-full

The 9 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.


Summary of changes:
 cli/image_cmds.go          | 131 +++++++++++++++++++++++++++++++++++++++++----
 cli/mfg_cmds.go            |   6 +--
 go.mod                     |   2 +-
 go.sum                     |   2 +
 iimg/{lvimg.go => iimg.go} |  57 ++++++++++++++++++++
 imfg/{lvmfg.go => imfg.go} |   0
 6 files changed, 184 insertions(+), 14 deletions(-)
 rename iimg/{lvimg.go => iimg.go} (81%)
 rename imfg/{lvmfg.go => imfg.go} (100%)


[mynewt-imgmod] 07/09: Distinguish partial {en|de}crypt from full

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 4777a82eadf96850cb200c2d54798c40788d0ec3
Author: Christopher Collins <cc...@apache.org>
AuthorDate: Wed May 29 10:08:09 2019 -0700

    Distinguish partial {en|de}crypt from full
    
    When we calculate a build's SHA256, we provide two inputs to the hash
    function:
    1. Image header
    2. Unencrypted image body
    
    The image header contains an "encrypted" flag
    (`IMAGE_F_ENCRYPTED`).  This has an interesting implication: when we
    decrypt a build, its hash is no longer valid.
    
    There are two use cases for decrypting an image:
    
    1. Create an unencrypted version of the image.
    
    For this use case, the decrypted image should be well formed and usable
    as an unencrypted image.
    
    2. Re-sign an image with a new key.
    
    For this use case, the procedure typically looks like this:
    
    a. Start with a signed and encrypted image.
    b. Decrypt image (`imgmod image decrypt`).
    c. Remove signature TLVs (`imgmod image rmsigs`).
    d. Re-sign image (`imgmod image sign`).
    e. Re-encrypt image (`imgmod image encrypt`).
    
    In this use case, it is critical that step b (decrypt) does *not* clear
    the `IMAGE_F_ENCRYPTED` flag from the image header.  This flag must
    remain set so that the signature produced in step d is valid.
    
    So we need two sets of {en|de}crypt commands:
    * Full
    * Partial
    
    The "full" versions apply to use case 1.
    The "partial" versions apply to use case 2.
    
    The old commands (`image encrypt`, `image decrypt`) are the partial
    versions.  These remain unchanged.
    
    The new commands (`image encryptfull` `image decryptfull`) are the full
    versions.
---
 cli/image_cmds.go | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++---
 iimg/iimg.go      |  57 +++++++++++++++++++++++++++
 2 files changed, 168 insertions(+), 5 deletions(-)

diff --git a/cli/image_cmds.go b/cli/image_cmds.go
index 8dec88d..76f7c54 100644
--- a/cli/image_cmds.go
+++ b/cli/image_cmds.go
@@ -29,9 +29,9 @@ import (
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 
+	"mynewt.apache.org/imgmod/iimg"
 	"mynewt.apache.org/newt/artifact/image"
 	"mynewt.apache.org/newt/artifact/sec"
-	"mynewt.apache.org/imgmod/iimg"
 	"mynewt.apache.org/newt/util"
 )
 
@@ -439,6 +439,40 @@ func runDecryptCmd(cmd *cobra.Command, args []string) {
 	}
 }
 
+func runDecryptFullCmd(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.DecryptImageFull(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)
@@ -473,6 +507,40 @@ func runEncryptCmd(cmd *cobra.Command, args []string) {
 	}
 }
 
+func runEncryptFullCmd(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.EncryptImageFull(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",
@@ -576,8 +644,13 @@ func AddImageCommands(cmd *cobra.Command) {
 
 	decryptCmd := &cobra.Command{
 		Use:   "decrypt <image> <priv-key-der>",
-		Short: "Decrypts an encrypted Mynewt image file",
-		Run:   runDecryptCmd,
+		Short: "Decrypts an encrypted Mynewt image file (partial)",
+		Long: "Decrypts the body of an encrypted Mynewt image file and " +
+			"removes the encryption TLVs.  This command does not change the " +
+			"image header and does not recalculate the image hash.  This " +
+			"command is useful for re-signing an image with a new key prior " +
+			"to re-encrypting.",
+		Run: runDecryptCmd,
 	}
 
 	decryptCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o",
@@ -585,12 +658,29 @@ func AddImageCommands(cmd *cobra.Command) {
 	decryptCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
 		"Replace input file")
 
-	imageCmd.AddCommand(decryptCmd)
+	decryptFullCmd := &cobra.Command{
+		Use:   "decryptfull <image> <priv-key-der>",
+		Short: "Decrypts an encrypted Mynewt image file (full)",
+		Long: "Decrypts the body of an encrypted Mynewt image file, " +
+			"removes the encryption TLVs, clears the 'encrypted' flag in " +
+			"the image header, and recalculates the image hash.",
+		Run: runDecryptFullCmd,
+	}
+
+	decryptFullCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o",
+		"", "File to write to")
+	decryptFullCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
+		"Replace input file")
+
+	imageCmd.AddCommand(decryptFullCmd)
 
 	encryptCmd := &cobra.Command{
 		Use:   "encrypt <image> <priv-key-der>",
 		Short: "Encrypts a Mynewt image file",
-		Run:   runEncryptCmd,
+		Long: "Encrypts the body of an encrypted Mynewt image file and " +
+			"adds encryption TLVs.  This command does not change the " +
+			"image header and does not recalculate the image hash.",
+		Run: runEncryptCmd,
 	}
 
 	encryptCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o",
@@ -599,4 +689,20 @@ func AddImageCommands(cmd *cobra.Command) {
 		"Replace input file")
 
 	imageCmd.AddCommand(encryptCmd)
+
+	encryptFullCmd := &cobra.Command{
+		Use:   "encryptfull <image> <priv-key-der>",
+		Short: "Encrypts an encrypted Mynewt image file (full)",
+		Long: "Encrypts the body of an encrypted Mynewt image file, " +
+			"adds encryption TLVs, sets the 'encrypted' flag in " +
+			"the image header, and recalculates the image hash.",
+		Run: runEncryptFullCmd,
+	}
+
+	encryptFullCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o",
+		"", "File to write to")
+	encryptFullCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
+		"Replace input file")
+
+	imageCmd.AddCommand(encryptFullCmd)
 }
diff --git a/iimg/iimg.go b/iimg/iimg.go
index 54ca5ef..7c28ed7 100644
--- a/iimg/iimg.go
+++ b/iimg/iimg.go
@@ -152,8 +152,45 @@ func DecryptImage(img image.Image, privKeBytes []byte) (image.Image, error) {
 
 	img.Body = body
 
+	return img, nil
+}
+
+func recalcHash(img image.Image) (image.Image, error) {
+	hash, err := img.CalcHash()
+	if err != nil {
+		return img, err
+	}
+
+	img.RemoveTlvsWithType(image.IMAGE_TLV_SHA256)
+	img.Tlvs = append(img.Tlvs, image.ImageTlv{
+		Header: image.ImageTlvHdr{
+			Type: image.IMAGE_TLV_SHA256,
+			Pad:  0,
+			Len:  uint16(len(hash)),
+		},
+		Data: hash,
+	})
+
+	return img, nil
+}
+
+func DecryptImageFull(img image.Image,
+	privKeBytes []byte) (image.Image, error) {
+
+	var err error
+	img, err = DecryptImage(img, privKeBytes)
+	if err != nil {
+		return img, err
+	}
+
 	img.Header.Flags &^= image.IMAGE_F_ENCRYPTED
 
+	// The hash needs to be recalculated now that the header has changed.
+	img, err = recalcHash(img)
+	if err != nil {
+		return img, err
+	}
+
 	return img, nil
 }
 
@@ -192,3 +229,23 @@ func EncryptImage(img image.Image, pubKeBytes []byte) (image.Image, error) {
 
 	return img, nil
 }
+
+func EncryptImageFull(img image.Image,
+	pubKeBytes []byte) (image.Image, error) {
+
+	var err error
+	img, err = EncryptImage(img, pubKeBytes)
+	if err != nil {
+		return img, err
+	}
+
+	img.Header.Flags |= image.IMAGE_F_ENCRYPTED
+
+	// The hash needs to be recalculated now that the header has changed.
+	img, err = recalcHash(img)
+	if err != nil {
+		return img, err
+	}
+
+	return img, nil
+}


[mynewt-imgmod] 02/09: Set / clear "encrypt" flag in image header

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 f5de3f98578364fbf50887818b02834f1b6f9238
Author: Christopher Collins <cc...@apache.org>
AuthorDate: Fri May 10 14:24:19 2019 -0700

    Set / clear "encrypt" flag in image header
---
 iimg/lvimg.go | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/iimg/lvimg.go b/iimg/lvimg.go
index bacefe9..54ca5ef 100644
--- a/iimg/lvimg.go
+++ b/iimg/lvimg.go
@@ -151,6 +151,9 @@ func DecryptImage(img image.Image, privKeBytes []byte) (image.Image, error) {
 	}
 
 	img.Body = body
+
+	img.Header.Flags &^= image.IMAGE_F_ENCRYPTED
+
 	return img, nil
 }
 
@@ -185,5 +188,7 @@ func EncryptImage(img image.Image, pubKeBytes []byte) (image.Image, error) {
 	}
 	img.Tlvs = append(img.Tlvs, tlv)
 
+	img.Header.Flags |= image.IMAGE_F_ENCRYPTED
+
 	return img, nil
 }


[mynewt-imgmod] 04/09: Rename old "larva" files

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 95e000cf48adc477150a7e020cd711fdc1ae201f
Author: Christopher Collins <cc...@apache.org>
AuthorDate: Fri May 24 11:29:02 2019 -0700

    Rename old "larva" files
    
    renamed:    iimg/lvimg.go -> iimg/iimg.go
    renamed:    imfg/lvmfg.go -> imfg/imfg.go
    
    "lv" was short for larva (this tool's previous name).  Now that the tool
    is called imgmod, replace "lv" with "i".
---
 iimg/{lvimg.go => iimg.go} | 0
 imfg/{lvmfg.go => imfg.go} | 0
 2 files changed, 0 insertions(+), 0 deletions(-)

diff --git a/iimg/lvimg.go b/iimg/iimg.go
similarity index 100%
rename from iimg/lvimg.go
rename to iimg/iimg.go
diff --git a/imfg/lvmfg.go b/imfg/imfg.go
similarity index 100%
rename from imfg/lvmfg.go
rename to imfg/imfg.go


[mynewt-imgmod] 09/09: Merge pull request #2 from ccollins476ad/crypt-full

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 be4ad904ce7a51552ccaea36a2eb9e81a0e53ca2
Merge: 8098e32 0dc0104
Author: ccollins476ad <cc...@apache.org>
AuthorDate: Mon Jun 10 16:55:25 2019 -0700

    Merge pull request #2 from ccollins476ad/crypt-full
    
    Distinguish partial {en|de}crypt from full

 cli/image_cmds.go          | 131 +++++++++++++++++++++++++++++++++++++++++----
 cli/mfg_cmds.go            |   6 +--
 go.mod                     |   2 +-
 go.sum                     |   2 +
 iimg/{lvimg.go => iimg.go} |  57 ++++++++++++++++++++
 imfg/{lvmfg.go => imfg.go} |   0
 6 files changed, 184 insertions(+), 14 deletions(-)


[mynewt-imgmod] 08/09: Depend on latest mynewt.apache.org/newt

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 0dc0104f127e0e0c8199143fdceb8c10a212a3aa
Author: Christopher Collins <cc...@apache.org>
AuthorDate: Wed May 29 10:10:58 2019 -0700

    Depend on latest mynewt.apache.org/newt
---
 cli/mfg_cmds.go | 6 +++---
 go.mod          | 2 +-
 go.sum          | 2 ++
 3 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/cli/mfg_cmds.go b/cli/mfg_cmds.go
index a89902b..afc8461 100644
--- a/cli/mfg_cmds.go
+++ b/cli/mfg_cmds.go
@@ -28,12 +28,12 @@ import (
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 
+	"mynewt.apache.org/imgmod/imfg"
 	"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"
 )
 
@@ -220,7 +220,7 @@ func runJoinCmd(cmd *cobra.Command, args []string) {
 			"Error reading source mfg directory: %s", err.Error()))
 	}
 	for _, info := range infos {
-		if info.Name() != mfg.MFG_IMG_FILENAME {
+		if info.Name() != mfg.MFG_BIN_IMG_FILENAME {
 			src := splitDir + "/mfg/" + info.Name()
 			dst := outDir + "/" + info.Name()
 			if info.IsDir() {
@@ -239,7 +239,7 @@ func runJoinCmd(cmd *cobra.Command, args []string) {
 		ImgmodUsage(nil, err)
 	}
 
-	binPath := fmt.Sprintf("%s/%s", outDir, mfg.MFG_IMG_FILENAME)
+	binPath := fmt.Sprintf("%s/%s", outDir, mfg.MFG_BIN_IMG_FILENAME)
 	if err := WriteFile(finalBin, binPath); err != nil {
 		ImgmodUsage(nil, err)
 	}
diff --git a/go.mod b/go.mod
index a8820c4..a1505df 100644
--- a/go.mod
+++ b/go.mod
@@ -3,5 +3,5 @@ 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
+	mynewt.apache.org/newt v0.0.0-20190529170335-75b2c282a77d
 )
diff --git a/go.sum b/go.sum
index d545ff0..ef709dd 100644
--- a/go.sum
+++ b/go.sum
@@ -27,3 +27,5 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTu
 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=
+mynewt.apache.org/newt v0.0.0-20190529170335-75b2c282a77d h1:dnv0EBSSD88NahC0Pow+5DOH7gy4D5xj5n1iCIxA2NY=
+mynewt.apache.org/newt v0.0.0-20190529170335-75b2c282a77d/go.mod h1:lFsPYOHxMMWA11pydOeh0GVFiXtx0A9VnzOQ6SiRR88=


[mynewt-imgmod] 05/09: Warn when `hashable` is used on an encrypted image

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 60423a36f9f6da68ee3850e270d5b7f89d3cc43f
Author: Christopher Collins <cc...@apache.org>
AuthorDate: Fri May 24 12:31:49 2019 -0700

    Warn when `hashable` is used on an encrypted image
    
    An image's hash is calculated from the following two inputs:
    1. Image header.
    2. Unencrypted image body.
    
    The `image hashable` command extracts the header and body from a
    complete image.  If the image body is encrypted, then the result of this
    command will not generate the correct hash.
    
    This commit adds a warning in case `image hashable` is used on an
    encrypyted image.
---
 cli/image_cmds.go | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/cli/image_cmds.go b/cli/image_cmds.go
index 83b4e8f..4a926a4 100644
--- a/cli/image_cmds.go
+++ b/cli/image_cmds.go
@@ -310,6 +310,11 @@ func runHashableCmd(cmd *cobra.Command, args []string) {
 		ImgmodUsage(cmd, err)
 	}
 
+	if (img.Header.Flags & image.IMAGE_F_ENCRYPTED) != 0 {
+		util.StatusMessage(util.VERBOSITY_QUIET,
+			"* Warning: extracting hashable content from an encrypted image\n")
+	}
+
 	f, err := os.Create(outFilename)
 	if err != nil {
 		ImgmodUsage(nil, util.ChildNewtError(err))


[mynewt-imgmod] 03/09: Merge pull request #1 from ccollins476ad/encrypt-flag

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 8098e32c819365ca087ed6dd815dcbc24dbd4ed2
Merge: c6855f4 f5de3f9
Author: ccollins476ad <cc...@apache.org>
AuthorDate: Fri May 10 14:26:12 2019 -0700

    Merge pull request #1 from ccollins476ad/encrypt-flag
    
    Set / clear "encrypt" flag in image header

 iimg/lvimg.go | 5 +++++
 1 file changed, 5 insertions(+)


[mynewt-imgmod] 06/09: Fix case: addtlvs --> addTlvs

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 743f256839e319c274ebf664b742a5b06fd82c37
Author: Christopher Collins <cc...@apache.org>
AuthorDate: Fri May 24 12:35:41 2019 -0700

    Fix case: addtlvs --> addTlvs
---
 cli/image_cmds.go | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/cli/image_cmds.go b/cli/image_cmds.go
index 4a926a4..8dec88d 100644
--- a/cli/image_cmds.go
+++ b/cli/image_cmds.go
@@ -510,19 +510,19 @@ func AddImageCommands(cmd *cobra.Command) {
 
 	imageCmd.AddCommand(signCmd)
 
-	addtlvsCmd := &cobra.Command{
-		Use: "addtlvs <img-file> <tlv-type> <data-filename> " +
+	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", "",
+	addTlvsCmd.PersistentFlags().StringVarP(&OptOutFilename, "outfile", "o", "",
 		"File to write to")
-	addtlvsCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
+	addTlvsCmd.PersistentFlags().BoolVarP(&OptInPlace, "inplace", "i", false,
 		"Replace input file")
 
-	imageCmd.AddCommand(addtlvsCmd)
+	imageCmd.AddCommand(addTlvsCmd)
 
 	rmtlvsCmd := &cobra.Command{
 		Use:   "rmtlvs <img-file> <tlv-index> [tlv-index] [...]",


[mynewt-imgmod] 01/09: 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()
+}