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/21 21:25:39 UTC

[mynewt-artifact] 15/23: Add "verify" functions to API

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-artifact.git

commit c15c49efe5926ba4ad41fd902061ff23a1b76a06
Author: Christopher Collins <cc...@apache.org>
AuthorDate: Thu Jun 20 14:07:44 2019 -0700

    Add "verify" functions to API
    
    After reading an image or mfgimage, the `Verify()` and
    `VerifyManifest()` functions can be used to check the object's
    integrity.
---
 image/create.go          |  35 ++++----
 image/image.go           | 121 +++++++++++++++++++---------
 image/keys_test.go       |  61 +-------------
 image/v1.go              |  57 ++++++-------
 image/verify.go          |  87 ++++++++++++++++++++
 manifest/manifest.go     |  25 ++++--
 manifest/mfg_manifest.go |  95 +++++++++++++++++++---
 mfg/map_meta.go          |  15 ++--
 mfg/meta.go              |  87 ++++++++++++++++----
 mfg/mfg.go               | 128 +++++++++++++++++++++++++-----
 mfg/verify.go            | 202 +++++++++++++++++++++++++++++++++++++++++++++++
 sec/encrypt.go           |  10 +--
 sec/key.go               | 112 +++++++++++++++-----------
 sec/sig.go               |  55 +++++++++++++
 14 files changed, 822 insertions(+), 268 deletions(-)

diff --git a/image/create.go b/image/create.go
index cbbc410..f51d77b 100644
--- a/image/create.go
+++ b/image/create.go
@@ -28,12 +28,11 @@ import (
 	"crypto/sha256"
 	"encoding/asn1"
 	"encoding/binary"
-	"encoding/hex"
 	"io/ioutil"
 	"math/big"
 
-	"mynewt.apache.org/newt/artifact/sec"
-	"mynewt.apache.org/newt/util"
+	"github.com/apache/mynewt-artifact/errors"
+	"github.com/apache/mynewt-artifact/sec"
 )
 
 type ImageCreator struct {
@@ -100,7 +99,7 @@ func GenerateEncTlv(cipherSecret []byte) (ImageTlv, error) {
 	} else if len(cipherSecret) == 24 {
 		encType = IMAGE_TLV_ENC_KEK
 	} else {
-		return ImageTlv{}, util.FmtNewtError("Invalid enc TLV size ")
+		return ImageTlv{}, errors.Errorf("invalid enc TLV size: %d", len(cipherSecret))
 	}
 
 	return ImageTlv{
@@ -120,7 +119,7 @@ func GenerateSigRsa(key sec.SignKey, hash []byte) ([]byte, error) {
 	signature, err := rsa.SignPSS(
 		rand.Reader, key.Rsa, crypto.SHA256, hash, &opts)
 	if err != nil {
-		return nil, util.FmtNewtError("Failed to compute signature: %s", err)
+		return nil, errors.Wrapf(err, "failed to compute signature")
 	}
 
 	return signature, nil
@@ -129,7 +128,7 @@ func GenerateSigRsa(key sec.SignKey, hash []byte) ([]byte, error) {
 func GenerateSigEc(key sec.SignKey, hash []byte) ([]byte, error) {
 	r, s, err := ecdsa.Sign(rand.Reader, key.Ec, hash)
 	if err != nil {
-		return nil, util.FmtNewtError("Failed to compute signature: %s", err)
+		return nil, errors.Wrapf(err, "failed to compute signature")
 	}
 
 	ECDSA := ECDSASig{
@@ -139,12 +138,12 @@ func GenerateSigEc(key sec.SignKey, hash []byte) ([]byte, error) {
 
 	signature, err := asn1.Marshal(ECDSA)
 	if err != nil {
-		return nil, util.FmtNewtError("Failed to construct signature: %s", err)
+		return nil, errors.Wrapf(err, "failed to construct signature")
 	}
 
 	sigLen := key.SigLen()
 	if len(signature) > int(sigLen) {
-		return nil, util.FmtNewtError("Something is really wrong\n")
+		return nil, errors.Errorf("signature truncated")
 	}
 
 	pad := make([]byte, int(sigLen)-len(signature))
@@ -210,8 +209,7 @@ func BuildSigTlvs(keys []sec.SignKey, hash []byte) ([]ImageTlv, error) {
 func GeneratePlainSecret() ([]byte, error) {
 	plainSecret := make([]byte, 16)
 	if _, err := rand.Read(plainSecret); err != nil {
-		return nil, util.FmtNewtError(
-			"Random generation error: %s\n", err)
+		return nil, errors.Wrapf(err, "random generation error")
 	}
 
 	return plainSecret, nil
@@ -238,7 +236,7 @@ func GenerateCipherSecret(pubKeBytes []byte,
 		return sec.EncryptSecretAes(aesPubKe, plainSecret)
 	}
 
-	return nil, util.FmtNewtError("Invalid image-crypt key")
+	return nil, errors.Errorf("invalid image-crypt key")
 }
 
 func GenerateImage(opts ImageCreateOpts) (Image, error) {
@@ -246,8 +244,7 @@ func GenerateImage(opts ImageCreateOpts) (Image, error) {
 
 	srcBin, err := ioutil.ReadFile(opts.SrcBinFilename)
 	if err != nil {
-		return Image{}, util.FmtNewtError(
-			"Can't read app binary: %s", err.Error())
+		return Image{}, errors.Wrapf(err, "Can't read app binary")
 	}
 
 	ic.Body = srcBin
@@ -269,8 +266,7 @@ func GenerateImage(opts ImageCreateOpts) (Image, error) {
 
 		pubKeBytes, err := ioutil.ReadFile(opts.SrcEncKeyFilename)
 		if err != nil {
-			return Image{}, util.FmtNewtError(
-				"Error reading pubkey file: %s", err.Error())
+			return Image{}, errors.Wrapf(err, "error reading pubkey file")
 		}
 
 		cipherSecret, err := GenerateCipherSecret(pubKeBytes, plainSecret)
@@ -301,7 +297,7 @@ func calcHash(initialHash []byte, hdr ImageHdr, pad []byte,
 			return err
 		}
 		if err := binary.Write(hash, binary.LittleEndian, itf); err != nil {
-			return util.FmtNewtError("Failed to hash data: %s", err.Error())
+			return errors.Wrapf(err, "failed to hash data")
 		}
 
 		return nil
@@ -364,8 +360,8 @@ func (ic *ImageCreator) Create() (Image, error) {
 		// between the header and the start of the image when it is padded.
 		extra := ic.HeaderSize - IMAGE_HEADER_SIZE
 		if extra < 0 {
-			return img, util.FmtNewtError("Image header must be at "+
-				"least %d bytes", IMAGE_HEADER_SIZE)
+			return img, errors.Errorf(
+				"image header must be at least %d bytes", IMAGE_HEADER_SIZE)
 		}
 
 		img.Header.HdrSz = uint16(ic.HeaderSize)
@@ -388,9 +384,6 @@ func (ic *ImageCreator) Create() (Image, error) {
 		img.Body = append(img.Body, ic.Body...)
 	}
 
-	util.StatusMessage(util.VERBOSITY_VERBOSE,
-		"Computed Hash for image as %s\n", hex.EncodeToString(hashBytes))
-
 	// Hash TLV.
 	tlv := ImageTlv{
 		Header: ImageTlvHdr{
diff --git a/image/image.go b/image/image.go
index 9855223..5c489c5 100644
--- a/image/image.go
+++ b/image/image.go
@@ -31,7 +31,8 @@ import (
 	"strconv"
 	"strings"
 
-	"mynewt.apache.org/newt/util"
+	"github.com/apache/mynewt-artifact/errors"
+	"github.com/apache/mynewt-artifact/sec"
 )
 
 const (
@@ -128,6 +129,11 @@ type ImageOffsets struct {
 	TotalSize int
 }
 
+func ImageTlvTypeIsValid(tlvType uint8) bool {
+	_, ok := imageTlvTypeNameMap[tlvType]
+	return ok
+}
+
 func ImageTlvTypeName(tlvType uint8) string {
 	name, ok := imageTlvTypeNameMap[tlvType]
 	if !ok {
@@ -155,24 +161,24 @@ func ParseVersion(versStr string) (ImageVersion, error) {
 	components := strings.Split(versStr, ".")
 	major, err = strconv.ParseUint(components[0], 10, 8)
 	if err != nil {
-		return ver, util.FmtNewtError("Invalid version string %s", versStr)
+		return ver, errors.Errorf("invalid version string %s", versStr)
 	}
 	if len(components) > 1 {
 		minor, err = strconv.ParseUint(components[1], 10, 8)
 		if err != nil {
-			return ver, util.FmtNewtError("Invalid version string %s", versStr)
+			return ver, errors.Errorf("invalid version string %s", versStr)
 		}
 	}
 	if len(components) > 2 {
 		rev, err = strconv.ParseUint(components[2], 10, 16)
 		if err != nil {
-			return ver, util.FmtNewtError("Invalid version string %s", versStr)
+			return ver, errors.Errorf("invalid version string %s", versStr)
 		}
 	}
 	if len(components) > 3 {
 		buildNum, err = strconv.ParseUint(components[3], 10, 32)
 		if err != nil {
-			return ver, util.FmtNewtError("Invalid version string %s", versStr)
+			return ver, errors.Errorf("invalid version string %s", versStr)
 		}
 	}
 
@@ -252,7 +258,7 @@ func (img *Image) Json() (string, error) {
 
 	b, err := json.MarshalIndent(m, "", "    ")
 	if err != nil {
-		return "", util.ChildNewtError(err)
+		return "", errors.Wrapf(err, "failed to marshal image")
 	}
 
 	return string(b), nil
@@ -263,13 +269,13 @@ func (tlv *ImageTlv) Write(w io.Writer) (int, error) {
 
 	err := binary.Write(w, binary.LittleEndian, &tlv.Header)
 	if err != nil {
-		return totalSize, util.ChildNewtError(err)
+		return totalSize, errors.Wrapf(err, "failed to write image TLV header")
 	}
 	totalSize += IMAGE_TLV_SIZE
 
 	size, err := w.Write(tlv.Data)
 	if err != nil {
-		return totalSize, util.ChildNewtError(err)
+		return totalSize, errors.Wrapf(err, "failed to write image TLV data")
 	}
 	totalSize += size
 
@@ -294,7 +300,7 @@ func (i *Image) FindUniqueTlv(tlvType uint8) (*ImageTlv, error) {
 		return nil, nil
 	}
 	if len(tlvs) > 1 {
-		return nil, util.FmtNewtError("Image contains %d TLVs with type %d",
+		return nil, errors.Errorf("image contains %d TLVs with type %d",
 			len(tlvs), tlvType)
 	}
 
@@ -338,11 +344,12 @@ func (img *Image) Trailer() ImageTrailer {
 func (i *Image) Hash() ([]byte, error) {
 	tlv, err := i.FindUniqueTlv(IMAGE_TLV_SHA256)
 	if err != nil {
-		return nil, err
+		return nil, errors.Wrapf(err, "failed to retrieve image hash")
 	}
 
 	if tlv == nil {
-		return nil, util.FmtNewtError("Image does not contain hash TLV")
+		return nil, errors.Errorf(
+			"failed to retrieve image hash: image does not contain hash TLV")
 	}
 
 	return tlv.Data, nil
@@ -360,20 +367,20 @@ func (i *Image) WritePlusOffsets(w io.Writer) (ImageOffsets, error) {
 
 	err := binary.Write(w, binary.LittleEndian, &i.Header)
 	if err != nil {
-		return offs, util.ChildNewtError(err)
+		return offs, errors.Wrapf(err, "failed to write image header")
 	}
 	offset += IMAGE_HEADER_SIZE
 
 	err = binary.Write(w, binary.LittleEndian, i.Pad)
 	if err != nil {
-		return offs, util.ChildNewtError(err)
+		return offs, errors.Wrapf(err, "failed to write image padding")
 	}
 	offset += len(i.Pad)
 
 	offs.Body = offset
 	size, err := w.Write(i.Body)
 	if err != nil {
-		return offs, util.ChildNewtError(err)
+		return offs, errors.Wrapf(err, "failed to write image body")
 	}
 	offset += size
 
@@ -381,7 +388,7 @@ func (i *Image) WritePlusOffsets(w io.Writer) (ImageOffsets, error) {
 	offs.Trailer = offset
 	err = binary.Write(w, binary.LittleEndian, &trailer)
 	if err != nil {
-		return offs, util.ChildNewtError(err)
+		return offs, errors.Wrapf(err, "failed to write image trailer")
 	}
 	offset += IMAGE_TRAILER_SIZE
 
@@ -389,7 +396,7 @@ func (i *Image) WritePlusOffsets(w io.Writer) (ImageOffsets, error) {
 		offs.Tlvs = append(offs.Tlvs, offset)
 		size, err := tlv.Write(w)
 		if err != nil {
-			return offs, util.ChildNewtError(err)
+			return offs, errors.Wrapf(err, "failed to write image TLV")
 		}
 		offset += size
 	}
@@ -423,16 +430,47 @@ func (i *Image) Write(w io.Writer) (int, error) {
 func (i *Image) WriteToFile(filename string) error {
 	f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
 	if err != nil {
-		return util.ChildNewtError(err)
+		return errors.Wrapf(err, "failed to open image destination file")
 	}
 
 	if _, err := i.Write(f); err != nil {
-		return util.ChildNewtError(err)
+		return errors.Wrapf(err, "failed to write image")
 	}
 
 	return nil
 }
 
+func (img *Image) CollectSigs() ([]sec.Sig, error) {
+	var sigs []sec.Sig
+
+	var keyHashTlv *ImageTlv
+	for i, _ := range img.Tlvs {
+		t := &img.Tlvs[i]
+
+		if t.Header.Type == IMAGE_TLV_KEYHASH {
+			if keyHashTlv != nil {
+				return nil, errors.Errorf(
+					"image contains keyhash tlv without subsequent signature")
+			}
+			keyHashTlv = t
+		} else if ImageTlvTypeIsSig(t.Header.Type) {
+			if keyHashTlv == nil {
+				return nil, errors.Errorf(
+					"image contains signature tlv without preceding keyhash")
+			}
+
+			sigs = append(sigs, sec.Sig{
+				KeyHash: keyHashTlv.Data,
+				Data:    t.Data,
+			})
+
+			keyHashTlv = nil
+		}
+	}
+
+	return sigs, nil
+}
+
 func parseRawHeader(imgData []byte, offset int) (ImageHdr, int, error) {
 	var hdr ImageHdr
 
@@ -440,20 +478,19 @@ func parseRawHeader(imgData []byte, offset int) (ImageHdr, int, error) {
 	r.Seek(int64(offset), io.SeekStart)
 
 	if err := binary.Read(r, binary.LittleEndian, &hdr); err != nil {
-		return hdr, 0, util.FmtNewtError(
-			"Error reading image header: %s", err.Error())
+		return hdr, 0, errors.Wrapf(err, "error reading image header")
 	}
 
 	if hdr.Magic != IMAGE_MAGIC {
-		return hdr, 0, util.FmtNewtError(
-			"Image magic incorrect; expected 0x%08x, got 0x%08x",
+		return hdr, 0, errors.Errorf(
+			"image magic incorrect; expected 0x%08x, got 0x%08x",
 			uint32(IMAGE_MAGIC), hdr.Magic)
 	}
 
 	remLen := len(imgData) - offset
 	if remLen < int(hdr.HdrSz) {
-		return hdr, 0, util.FmtNewtError(
-			"Image header incomplete; expected %d bytes, got %d bytes",
+		return hdr, 0, errors.Errorf(
+			"image header incomplete; expected %d bytes, got %d bytes",
 			hdr.HdrSz, remLen)
 	}
 
@@ -467,8 +504,8 @@ func parseRawBody(imgData []byte, hdr ImageHdr,
 	remLen := len(imgData) - offset
 
 	if remLen < imgSz {
-		return nil, 0, util.FmtNewtError(
-			"Image body incomplete; expected %d bytes, got %d bytes",
+		return nil, 0, errors.Errorf(
+			"image body incomplete; expected %d bytes, got %d bytes",
 			imgSz, remLen)
 	}
 
@@ -482,9 +519,8 @@ func parseRawTrailer(imgData []byte, offset int) (ImageTrailer, int, error) {
 	r.Seek(int64(offset), io.SeekStart)
 
 	if err := binary.Read(r, binary.LittleEndian, &trailer); err != nil {
-		return trailer, 0, util.FmtNewtError(
-			"Image contains invalid trailer at offset %d: %s",
-			offset, err.Error())
+		return trailer, 0, errors.Wrapf(err,
+			"image contains invalid trailer at offset %d", offset)
 	}
 
 	return trailer, IMAGE_TRAILER_SIZE, nil
@@ -497,14 +533,14 @@ func parseRawTlv(imgData []byte, offset int) (ImageTlv, int, error) {
 	r.Seek(int64(offset), io.SeekStart)
 
 	if err := binary.Read(r, binary.LittleEndian, &tlv.Header); err != nil {
-		return tlv, 0, util.FmtNewtError(
-			"Image contains invalid TLV at offset %d: %s", offset, err.Error())
+		return tlv, 0, errors.Wrapf(err,
+			"image contains invalid TLV at offset %d", offset)
 	}
 
 	tlv.Data = make([]byte, tlv.Header.Len)
 	if _, err := r.Read(tlv.Data); err != nil {
-		return tlv, 0, util.FmtNewtError(
-			"Image contains invalid TLV at offset %d: %s", offset, err.Error())
+		return tlv, 0, errors.Wrapf(err,
+			"image contains invalid TLV at offset %d", offset)
 	}
 
 	return tlv, IMAGE_TLV_SIZE + int(tlv.Header.Len), nil
@@ -532,6 +568,15 @@ func ParseImage(imgData []byte) (Image, error) {
 	}
 	offset += size
 
+	totalLen := IMAGE_HEADER_SIZE + len(body) + int(trailer.TlvTotLen)
+	if len(imgData) < totalLen {
+		return img, errors.Errorf("image data truncated: have=%d want=%d",
+			len(imgData), totalLen)
+	}
+
+	// Trim excess data following image trailer.
+	imgData = imgData[:totalLen]
+
 	var tlvs []ImageTlv
 	tlvLen := IMAGE_TRAILER_SIZE
 	for offset < len(imgData) {
@@ -541,13 +586,17 @@ func ParseImage(imgData []byte) (Image, error) {
 		}
 
 		tlvs = append(tlvs, tlv)
+
 		offset += size
+		if offset > len(imgData) {
+			return img, errors.Errorf("TLVs extend beyond end of image")
+		}
 
-		tlvLen += IMAGE_TLV_SIZE + int(tlv.Header.Len)
+		tlvLen += size
 	}
 
 	if int(trailer.TlvTotLen) != tlvLen {
-		return img, util.FmtNewtError(
+		return img, errors.Errorf(
 			"invalid image: trailer indicates TLV-length=%d; actual=%d",
 			trailer.TlvTotLen, tlvLen)
 	}
@@ -564,7 +613,7 @@ func ReadImage(filename string) (Image, error) {
 
 	imgData, err := ioutil.ReadFile(filename)
 	if err != nil {
-		return ri, util.ChildNewtError(err)
+		return ri, errors.Wrapf(err, "failed to read image from file")
 	}
 
 	return ParseImage(imgData)
diff --git a/image/keys_test.go b/image/keys_test.go
index 769848a..0119098 100644
--- a/image/keys_test.go
+++ b/image/keys_test.go
@@ -24,8 +24,8 @@ import (
 	"os"
 	"testing"
 
-	"mynewt.apache.org/newt/artifact/image"
-	"mynewt.apache.org/newt/artifact/sec"
+	"mynewt.apache.org/artifact/image"
+	"mynewt.apache.org/artifact/sec"
 )
 
 func TestRSA(t *testing.T) {
@@ -44,18 +44,6 @@ func TestPlainEcdsaPkcs8(t *testing.T) {
 	signatureTest(t, ecdsaPkcs8Private)
 }
 
-func TestEncryptedRSA(t *testing.T) {
-	sec.KeyPassword = []byte("sample")
-	signatureTest(t, rsaEncryptedPrivate)
-	sec.KeyPassword = []byte{}
-}
-
-func TestEncryptedEcdsa(t *testing.T) {
-	sec.KeyPassword = []byte("sample")
-	signatureTest(t, ecdsaEncryptedPrivate)
-	sec.KeyPassword = []byte{}
-}
-
 func signatureTest(t *testing.T, privateKey []byte) {
 	tmpdir, err := ioutil.TempDir("", "newttest")
 	if err != nil {
@@ -170,48 +158,3 @@ j0AQZlBs7f4r67668eDCUB8aDR2hRANCAATyZPzsx+xn9JtlxdspevTrYisiMTjl
 YuBJCrV1FZj2HkplEgO+ZIMuD7eRvyTEBS2bw6F1aCeKOMUmYVImAbpc
 -----END PRIVATE KEY-----
 `)
-
-// A password-protected RSA private key in PKCS#5/8 format.  The
-// password for this key is "sample".
-var rsaEncryptedPrivate = []byte(`-----BEGIN ENCRYPTED PRIVATE KEY-----
-MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIRMifqJThk8kCAggA
-MB0GCWCGSAFlAwQBKgQQTMUBoFpzjJ5UNRnCIeqf4QSCBNDkQvXnUNmss8erKiDo
-Uqs2tf9ZD8MjDThLBmF/gV1dg1q6aDY+3fI2E4yLXJb2PmKcUq82YZ0FDeoCvJRJ
-BCurzM9slur5akpNBTFoFwtFsdHz7nKNS4MHUul22rGBnVFUUNTySmpjl/m+dxWO
-fa6tWpGTAr7tsCy9gF5PxpSw7NR/NpIL0PmpydHWhTs1tl2csqBqK6Tp014Kefi/
-pmmeb2eRl5cmprxW32rW2QBMtv4z91SsbnlVdz4r8txTG+3S4td9v9jD5kqcIiC2
-KQHrbH9y7okUk/ISsp9ANKPJt10fbYDxORiMK57XssXy1enGjpkIIrUGz1TMydkD
-USfwqkmPuIrrzOXnbxU4ef2wC/pA/h9Smby3WWYo8725/1kZyIediNDcgi/Qgrs4
-1VQAYzsD6duwyUNSo+tgmYVFGvZhsottus3fMWe/Ay1biJ6z6Vk8gqKWI1VV/REJ
-zK/I9hgKGxj2N2Ff6E/YkcwQenHWj/iDWLjvokyOBnPFNqzzM2Qqo1XFpzj4EO5D
-0WD4EzZYvUhk3lZZNydvXiuy8RrCVLLJMS08XgOqQaiFqqxj2hjRwv3nBesk7iA8
-5Tv8GMa5QkNrISCnp4/uGBh+v/CjwVRqPTcK3/mctPN2nLhI6H4pF4Y6apXkz1TN
-NMQqxaxmVVg8fyLaS4/xfUr8LAmiEtOwvs0XOhcqCTvvlsO4N+yec4VD4gmsTDY9
-/2b/+YwSlGMpA+GQQbg0FraaF8NyJRG1mSER6WiUGGM1cuKK44nzBbykQbZwzDSA
-kkhjDaadkhv/NPKAUR3sNy2GXVaNL/ItCpQUHRKKcIPp0HhdXsl0YebuwRlHjw/6
-UOdzNYe23e40X/Xl3vmOKRbzhLP/qO2DV21o0wI4ujF8Xu5h1h8s49HPp58G1ldy
-/hJ6durYKX8T5khiR2iXYewoy0YObuccV//Ov1/ySOp/x0/QuCl/swvs8Jf7awnu
-rpRrHPArpCvMmXmt5Y+TFYXFjkJGwsxTew5TcwBebBlIET2XNbo2pbz4WqJ3eVlK
-CNZVDEZ8mMrGT00FBi759Vfw9rhrnqXnLlNtJZ5VCXFUw8Tos302sLaQWXzHYyf8
-4awM8G9PSu5Q9lFcN9od4H95YrAAv/l8F+pcGgEKD8ZuzsgFIalqgx5wzmUMDcPM
-NKV5u9mtHjI92ru6NB8rGesM6sy6kBGvpotsDWawpV2SoCrkbyEkk+kXaGS+fsG7
-D2H37GfktN8R5Ktc0Uf/JJiNfDzq8lk1J4r7LBQlWUbhKbfGMYxt+7Xo0GsqAsLp
-PKSUwx+hTZb3BmW6s4Q6vivI1MdQbWVT1zh41StvfRSNlo70iOFxOM0lU1jjY989
-UKo+gcolddvZbMNwip0ILPO3dsa+he1jJ/gbo9qBHLy7plfsBLLakZP1Nu6xdlqQ
-TSSobaE8uxUMZk+wMWClA9AOZ1TcUr2yRV5GVj/bxG9ab+H37vF9F8vFE+jjJ7yN
-6pjdohm4gXeSVx7ON4SeZLsVwNYkCVYS89E81qLx1jP9F57+6IUGDZN5EMC0aJLT
-ny75MCCLT00KD7BFsb0KDLXxp++eu/L2hinorT3p6dXp/9mUoxmy6wJqEyqCFniZ
-N2GZN7+LDTIbHUxCijVWamU2DQ==
------END ENCRYPTED PRIVATE KEY-----
-`)
-
-// A password-protected ECDSA private key in PKCS#5/8 format.  The
-// password for this key is "sample"
-var ecdsaEncryptedPrivate = []byte(`-----BEGIN ENCRYPTED PRIVATE KEY-----
-MIHeMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAjlKrDSKNg9QQICCAAw
-HQYJYIZIAWUDBAEqBBDliPNzQTNpdlppTcYpmuhWBIGQVhfWaVSzUvi/qIZLiZVn
-Nulfw5jDOlbn3UBX9kp/Z9Pro582Q0kjzLfm5UahvDINEJWxL4pc/28UnGQTBr0Q
-nSEg+RbqpuD099C38H0Gq/YkIM+RDG4aiQrkmzHXyVsHshIbG+z2LsLTIwmU69/Z
-v0nX6/hGErVR8YWcrOne086rCvfJVrxyO5+EUqrkLhEr
------END ENCRYPTED PRIVATE KEY-----
-`)
diff --git a/image/v1.go b/image/v1.go
index 67a451f..0b82086 100644
--- a/image/v1.go
+++ b/image/v1.go
@@ -30,12 +30,11 @@ import (
 	"crypto/rsa"
 	"crypto/sha256"
 	"encoding/binary"
-	"encoding/hex"
 	"io"
 	"io/ioutil"
 
-	"mynewt.apache.org/newt/artifact/sec"
-	"mynewt.apache.org/newt/util"
+	"github.com/apache/mynewt-artifact/errors"
+	"github.com/apache/mynewt-artifact/sec"
 )
 
 const IMAGEv1_MAGIC = 0x96f3b83c /* Image header magic */
@@ -95,10 +94,10 @@ func (img *ImageV1) FindTlvs(tlvType uint8) []ImageTlv {
 func (img *ImageV1) Hash() ([]byte, error) {
 	tlvs := img.FindTlvs(IMAGEv1_TLV_SHA256)
 	if len(tlvs) == 0 {
-		return nil, util.FmtNewtError("Image does not contain hash TLV")
+		return nil, errors.Errorf("image does not contain hash TLV")
 	}
 	if len(tlvs) > 1 {
-		return nil, util.FmtNewtError("Image contains %d hash TLVs", len(tlvs))
+		return nil, errors.Errorf("image contains %d hash TLVs", len(tlvs))
 	}
 
 	return tlvs[0].Data, nil
@@ -112,14 +111,14 @@ func (img *ImageV1) WritePlusOffsets(w io.Writer) (ImageOffsets, error) {
 
 	err := binary.Write(w, binary.LittleEndian, &img.Header)
 	if err != nil {
-		return offs, util.ChildNewtError(err)
+		return offs, errors.Wrapf(err, "failed to write image header")
 	}
 	offset += IMAGE_HEADER_SIZE
 
 	offs.Body = offset
 	size, err := w.Write(img.Body)
 	if err != nil {
-		return offs, util.ChildNewtError(err)
+		return offs, errors.Wrapf(err, "failed to write image body")
 	}
 	offset += size
 
@@ -127,7 +126,7 @@ func (img *ImageV1) WritePlusOffsets(w io.Writer) (ImageOffsets, error) {
 		offs.Tlvs = append(offs.Tlvs, offset)
 		size, err := tlv.Write(w)
 		if err != nil {
-			return offs, util.ChildNewtError(err)
+			return offs, errors.Wrapf(err, "failed to write image TLV")
 		}
 		offset += size
 	}
@@ -174,7 +173,7 @@ func sigHdrTypeV1(key sec.SignKey) (uint32, error) {
 		case "P-256":
 			return IMAGEv1_F_ECDSA256_SHA256, nil
 		default:
-			return 0, util.FmtNewtError("Unsupported ECC curve")
+			return 0, errors.Errorf("unsupported ECC curve")
 		}
 	}
 }
@@ -211,7 +210,7 @@ func generateV1SigRsa(key *rsa.PrivateKey, hash []byte) ([]byte, error) {
 			rand.Reader, key, crypto.SHA256, hash)
 	}
 	if err != nil {
-		return nil, util.FmtNewtError("Failed to compute signature: %s", err)
+		return nil, errors.Wrapf(err, "failed to compute signature")
 	}
 
 	return signature, nil
@@ -241,20 +240,19 @@ func generateV1SigTlvEc(key sec.SignKey, hash []byte) (ImageTlv, error) {
 
 	sigLen := key.SigLen()
 	if len(sig) > int(sigLen) {
-		return ImageTlv{}, util.FmtNewtError("Something is really wrong\n")
+		return ImageTlv{}, errors.Errorf("signature truncated")
 	}
 
 	b := &bytes.Buffer{}
 
 	if _, err := b.Write(sig); err != nil {
-		return ImageTlv{},
-			util.FmtNewtError("Failed to append sig: %s", err.Error())
+		return ImageTlv{}, errors.Wrapf(err, "failed to append sig")
 	}
 
 	pad := make([]byte, int(sigLen)-len(sig))
 	if _, err := b.Write(pad); err != nil {
-		return ImageTlv{}, util.FmtNewtError(
-			"Failed to serialize image trailer: %s", err.Error())
+		return ImageTlv{}, errors.Wrapf(err,
+			"failed to serialize image trailer")
 	}
 
 	return ImageTlv{
@@ -284,7 +282,7 @@ func calcHashV1(initialHash []byte, hdr ImageHdrV1,
 
 	add := func(itf interface{}) error {
 		if err := binary.Write(hash, binary.LittleEndian, itf); err != nil {
-			return util.FmtNewtError("Failed to hash data: %s", err.Error())
+			return errors.Wrapf(err, "failed to hash data")
 		}
 
 		return nil
@@ -319,8 +317,8 @@ func (ic *ImageCreator) CreateV1() (ImageV1, error) {
 	ri := ImageV1{}
 
 	if len(ic.SigKeys) > 1 {
-		return ri, util.FmtNewtError(
-			"V1 image format only allows one key, %d keys specified",
+		return ri, errors.Errorf(
+			"v1 image format only allows one key, %d keys specified",
 			len(ic.SigKeys))
 	}
 
@@ -349,8 +347,8 @@ func (ic *ImageCreator) CreateV1() (ImageV1, error) {
 		 * the image when it is padded.
 		 */
 		if ic.HeaderSize < IMAGE_HEADER_SIZE {
-			return ri, util.FmtNewtError("Image header must be at "+
-				"least %d bytes", IMAGE_HEADER_SIZE)
+			return ri, errors.Errorf(
+				"image header must be at least %d bytes", IMAGE_HEADER_SIZE)
 		}
 
 		hdr.HdrSz = uint16(ic.HeaderSize)
@@ -372,8 +370,8 @@ func (ic *ImageCreator) CreateV1() (ImageV1, error) {
 		// the image when it is padded.
 		extra := ic.HeaderSize - IMAGE_HEADER_SIZE
 		if extra < 0 {
-			return ri, util.FmtNewtError("Image header must be at "+
-				"least %d bytes", IMAGE_HEADER_SIZE)
+			return ri, errors.Errorf(
+				"image header must be at least %d bytes", IMAGE_HEADER_SIZE)
 		}
 
 		hdr.HdrSz = uint16(ic.HeaderSize)
@@ -387,9 +385,6 @@ func (ic *ImageCreator) CreateV1() (ImageV1, error) {
 		return ri, err
 	}
 
-	util.StatusMessage(util.VERBOSITY_VERBOSE,
-		"Computed Hash for image as %s\n", hex.EncodeToString(hashBytes))
-
 	/*
 	 * Followed by data.
 	 */
@@ -399,16 +394,14 @@ func (ic *ImageCreator) CreateV1() (ImageV1, error) {
 	for {
 		cnt, err := r.Read(dataBuf)
 		if err != nil && err != io.EOF {
-			return ri, util.FmtNewtError(
-				"Failed to read from image body: %s", err.Error())
+			return ri, errors.Wrapf(err, "failed to read from image body")
 		}
 		if cnt == 0 {
 			break
 		}
 
 		if _, err = w.Write(dataBuf[0:cnt]); err != nil {
-			return ri, util.FmtNewtError(
-				"Failed to write to image body: %s", err.Error())
+			return ri, errors.Wrapf(err, "failed to write to image body")
 		}
 	}
 	ri.Body = w.Bytes()
@@ -448,8 +441,7 @@ func GenerateV1Image(opts ImageCreateOpts) (ImageV1, error) {
 
 	srcBin, err := ioutil.ReadFile(opts.SrcBinFilename)
 	if err != nil {
-		return ImageV1{}, util.FmtNewtError(
-			"Can't read app binary: %s", err.Error())
+		return ImageV1{}, errors.Wrapf(err, "can't read app binary")
 	}
 
 	ic.Body = srcBin
@@ -471,8 +463,7 @@ func GenerateV1Image(opts ImageCreateOpts) (ImageV1, error) {
 
 		pubKeBytes, err := ioutil.ReadFile(opts.SrcEncKeyFilename)
 		if err != nil {
-			return ImageV1{}, util.FmtNewtError(
-				"Error reading pubkey file: %s", err.Error())
+			return ImageV1{}, errors.Wrapf(err, "error reading pubkey file")
 		}
 		cipherSecret, err := GenerateCipherSecret(pubKeBytes, plainSecret)
 		if err != nil {
diff --git a/image/verify.go b/image/verify.go
new file mode 100644
index 0000000..91ddf28
--- /dev/null
+++ b/image/verify.go
@@ -0,0 +1,87 @@
+package image
+
+import (
+	"bytes"
+	"encoding/hex"
+
+	"github.com/apache/mynewt-artifact/errors"
+	"github.com/apache/mynewt-artifact/manifest"
+)
+
+// VerifyManifest compares an image's structure to its manifest.  It returns
+// an error if the image doesn't match the manifest.
+func (img *Image) VerifyManifest(man manifest.Manifest) error {
+	ver, err := ParseVersion(man.Version)
+	if err != nil {
+		return errors.Wrapf(err, "manifest contains invalid `version` field")
+	}
+
+	if ver.Major != img.Header.Vers.Major ||
+		ver.Minor != img.Header.Vers.Minor ||
+		ver.Rev != img.Header.Vers.Rev ||
+		ver.BuildNum != img.Header.Vers.BuildNum {
+
+		return errors.Errorf(
+			"manifest version different from image header: man=%s img=%s",
+			ver.String(), img.Header.Vers.String())
+	}
+
+	var imgHash string
+	if hash, err := img.Hash(); err == nil {
+		imgHash = hex.EncodeToString(hash)
+	}
+
+	// A manifest contains two image hashes: `id` and `image_hash`.  Check
+	// both.
+
+	checkHash := func(manHash string) error {
+		if imgHash != manHash {
+			return errors.Errorf(
+				"manifest image hash different from image TLV: man=%s img=%s",
+				manHash, imgHash)
+		}
+		return nil
+	}
+
+	if err := checkHash(man.BuildID); err != nil {
+		return err
+	}
+	if err := checkHash(man.ImageHash); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Verify checks an image's structure and internal consistency.  It returns
+// an error if the image is incorrect.
+func (img *Image) Verify() error {
+	// Verify the hash.
+
+	haveHash, err := img.Hash()
+	if err != nil {
+		return err
+	}
+
+	wantHash, err := img.CalcHash()
+	if err != nil {
+		return err
+	}
+
+	if !bytes.Equal(haveHash, wantHash) {
+		return errors.Errorf(
+			"image manifest contains incorrect hash: have=%x want=%x",
+			haveHash, wantHash)
+	}
+
+	// Verify that each TLV has a valid "type" field.
+	for _, t := range img.Tlvs {
+		if !ImageTlvTypeIsValid(t.Header.Type) {
+			return errors.Errorf(
+				"image contains TLV with invalid `type` field: %d",
+				t.Header.Type)
+		}
+	}
+
+	return nil
+}
diff --git a/manifest/manifest.go b/manifest/manifest.go
index 95c76c6..3a7298a 100644
--- a/manifest/manifest.go
+++ b/manifest/manifest.go
@@ -23,8 +23,9 @@ import (
 	"encoding/json"
 	"io"
 	"io/ioutil"
+	"strings"
 
-	"mynewt.apache.org/newt/util"
+	"github.com/apache/mynewt-artifact/errors"
 )
 
 /*
@@ -86,13 +87,12 @@ func ReadManifest(path string) (Manifest, error) {
 
 	content, err := ioutil.ReadFile(path)
 	if err != nil {
-		return m, util.ChildNewtError(err)
+		return m, errors.Wrapf(err, "failed to read manifest file")
 	}
 
 	if err := json.Unmarshal(content, &m); err != nil {
-		return m, util.FmtNewtError(
-			"Failure decoding manifest with path \"%s\": %s",
-			path, err.Error())
+		return m, errors.Wrapf(
+			err, "failure decoding manifest with path \"%s\"", path)
 	}
 
 	return m, nil
@@ -101,13 +101,24 @@ func ReadManifest(path string) (Manifest, error) {
 func (m *Manifest) Write(w io.Writer) (int, error) {
 	buffer, err := json.MarshalIndent(m, "", "  ")
 	if err != nil {
-		return 0, util.FmtNewtError("Cannot encode manifest: %s", err.Error())
+		return 0, errors.Wrapf(err, "Cannot encode manifest")
 	}
 
 	cnt, err := w.Write(buffer)
 	if err != nil {
-		return 0, util.FmtNewtError("Cannot write manifest: %s", err.Error())
+		return 0, errors.Wrapf(err, "Cannot write manifest")
 	}
 
 	return cnt, nil
 }
+
+func (m *Manifest) FindTargetVar(key string) string {
+	for _, tv := range m.TgtVars {
+		parts := strings.SplitN(tv, "=", 2)
+		if len(parts) == 2 && parts[0] == key {
+			return parts[1]
+		}
+	}
+
+	return ""
+}
diff --git a/manifest/mfg_manifest.go b/manifest/mfg_manifest.go
index f2c506c..3ebd22a 100644
--- a/manifest/mfg_manifest.go
+++ b/manifest/mfg_manifest.go
@@ -20,11 +20,13 @@
 package manifest
 
 import (
+	"encoding/hex"
 	"encoding/json"
 	"io/ioutil"
 
-	"mynewt.apache.org/newt/artifact/flash"
-	"mynewt.apache.org/newt/util"
+	"github.com/apache/mynewt-artifact/errors"
+	"github.com/apache/mynewt-artifact/flash"
+	"github.com/apache/mynewt-artifact/sec"
 )
 
 type MfgManifestTarget struct {
@@ -48,7 +50,6 @@ type MfgManifestMeta struct {
 	Hash      bool                 `json:"hash_present"`
 	FlashMap  bool                 `json:"flash_map_present"`
 	Mmrs      []MfgManifestMetaMmr `json:"mmrs,omitempty"`
-	// XXX: refhash
 }
 
 type MfgManifestSig struct {
@@ -66,6 +67,7 @@ type MfgManifest struct {
 	BinPath    string            `json:"bin_path"`
 	HexPath    string            `json:"hex_path"`
 	Bsp        string            `json:"bsp"`
+	EraseVal   byte              `json:"erase_val"`
 	Signatures []MfgManifestSig  `json:"signatures,omitempty"`
 	FlashAreas []flash.FlashArea `json:"flash_map"`
 
@@ -73,18 +75,29 @@ type MfgManifest struct {
 	Meta    *MfgManifestMeta    `json:"meta,omitempty"`
 }
 
-func ReadMfgManifest(path string) (MfgManifest, error) {
-	m := MfgManifest{}
+func ParseMfgManifest(jsonText []byte) (MfgManifest, error) {
+	m := MfgManifest{
+		// Backwards compatibility: assume 0xff if unspecified.
+		EraseVal: 0xff,
+	}
+
+	if err := json.Unmarshal(jsonText, &m); err != nil {
+		return m, errors.Wrapf(err, "failure decoding mfg manifest")
+	}
 
+	return m, nil
+}
+
+func ReadMfgManifest(path string) (MfgManifest, error) {
 	content, err := ioutil.ReadFile(path)
 	if err != nil {
-		return m, util.ChildNewtError(err)
+		return MfgManifest{}, errors.Wrapf(err,
+			"failed to read mfg manifest file")
 	}
 
-	if err := json.Unmarshal(content, &m); err != nil {
-		return m, util.FmtNewtError(
-			"Failure decoding mfg manifest with path \"%s\": %s",
-			path, err.Error())
+	m, err := ParseMfgManifest(content)
+	if err != nil {
+		return m, errors.Wrapf(err, "path=%s", path)
 	}
 
 	return m, nil
@@ -93,9 +106,67 @@ func ReadMfgManifest(path string) (MfgManifest, error) {
 func (m *MfgManifest) MarshalJson() ([]byte, error) {
 	buffer, err := json.MarshalIndent(m, "", "  ")
 	if err != nil {
-		return nil, util.FmtNewtError(
-			"Cannot encode mfg manifest: %s", err.Error())
+		return nil, errors.Wrapf(err, "cannot encode mfg manifest")
 	}
 
 	return buffer, nil
 }
+
+func (m *MfgManifest) FindFlashArea(device int, offset int) *flash.FlashArea {
+	for i, _ := range m.FlashAreas {
+		fa := &m.FlashAreas[i]
+		if fa.Device == device && fa.Offset == offset {
+			return fa
+		}
+	}
+
+	return nil
+}
+
+func (m *MfgManifest) FindFlashAreaName(name string) *flash.FlashArea {
+	for i, _ := range m.FlashAreas {
+		fa := &m.FlashAreas[i]
+		if fa.Name == name {
+			return fa
+		}
+	}
+
+	return nil
+}
+
+func (ms *MfgManifestSig) SecSig() (sec.Sig, error) {
+	keyHash, err := hex.DecodeString(ms.Key)
+	if err != nil {
+		return sec.Sig{}, errors.Errorf(
+			"invalid hex-encoded key hash: %s", ms.Key)
+	}
+
+	data, err := hex.DecodeString(ms.Sig)
+	if err != nil {
+		return sec.Sig{}, errors.Errorf(
+			"invalid hex-encoded signature: %s", ms.Sig)
+	}
+
+	return sec.Sig{
+		KeyHash: keyHash,
+		Data:    data,
+	}, nil
+}
+
+func (m *MfgManifest) SecSigs() ([]sec.Sig, error) {
+	var sigs []sec.Sig
+	for _, ms := range m.Signatures {
+		s, err := ms.SecSig()
+		if err != nil {
+			return nil, err
+		}
+
+		sigs = append(sigs, s)
+	}
+
+	return sigs, nil
+}
+
+func (mt *MfgManifestTarget) IsBoot() bool {
+	return mt.BinPath != ""
+}
diff --git a/mfg/map_meta.go b/mfg/map_meta.go
index b16c37e..19b1ee8 100644
--- a/mfg/map_meta.go
+++ b/mfg/map_meta.go
@@ -25,7 +25,7 @@ import (
 	"encoding/hex"
 	"encoding/json"
 
-	"mynewt.apache.org/newt/util"
+	"github.com/apache/mynewt-artifact/errors"
 )
 
 func (t *MetaTlv) bodyMap() (map[string]interface{}, error) {
@@ -33,8 +33,7 @@ func (t *MetaTlv) bodyMap() (map[string]interface{}, error) {
 
 	readBody := func(dst interface{}) error {
 		if err := binary.Read(r, binary.LittleEndian, dst); err != nil {
-			return util.FmtNewtError(
-				"Error parsing TLV data: %s", err.Error())
+			return errors.Wrapf(err, "error parsing TLV data")
 		}
 		return nil
 	}
@@ -62,8 +61,7 @@ func (t *MetaTlv) bodyMap() (map[string]interface{}, error) {
 		return body.Map(), nil
 
 	default:
-		return nil, util.FmtNewtError(
-			"Unknown meta TLV type: %d", t.Header.Type)
+		return nil, errors.Errorf("unknown meta TLV type: %d", t.Header.Type)
 	}
 }
 
@@ -88,7 +86,7 @@ func (b *MetaTlvBodyMmrRef) Map() map[string]interface{} {
 	}
 }
 
-func (t *MetaTlv) Map(offset int) map[string]interface{} {
+func (t *MetaTlv) Map(index int, offset int) map[string]interface{} {
 	hmap := map[string]interface{}{
 		"_type_name": MetaTlvTypeName(t.Header.Type),
 		"type":       t.Header.Type,
@@ -105,6 +103,7 @@ func (t *MetaTlv) Map(offset int) map[string]interface{} {
 	}
 
 	return map[string]interface{}{
+		"_index":  index,
 		"_offset": offset,
 		"header":  hmap,
 		"data":    body,
@@ -126,7 +125,7 @@ func (m *Meta) Map(endOffset int) map[string]interface{} {
 
 	tlvs := []map[string]interface{}{}
 	for i, t := range m.Tlvs {
-		tlv := t.Map(startOffset + offsets.Tlvs[i])
+		tlv := t.Map(i, startOffset+offsets.Tlvs[i])
 		tlvs = append(tlvs, tlv)
 	}
 
@@ -146,7 +145,7 @@ func (m *Meta) Json(offset int) (string, error) {
 
 	bin, err := json.MarshalIndent(mmap, "", "    ")
 	if err != nil {
-		return "", util.ChildNewtError(err)
+		return "", errors.Wrapf(err, "failed to marshal MMR")
 	}
 
 	return string(bin), nil
diff --git a/mfg/meta.go b/mfg/meta.go
index 4521a89..b4786bc 100644
--- a/mfg/meta.go
+++ b/mfg/meta.go
@@ -25,7 +25,7 @@ import (
 	"io"
 	"io/ioutil"
 
-	"mynewt.apache.org/newt/util"
+	"github.com/apache/mynewt-artifact/errors"
 )
 
 // The "manufacturing meta region" is located at the end of the boot loader
@@ -139,7 +139,7 @@ func MetaTlvTypeName(typ uint8) string {
 func writeElem(elem interface{}, w io.Writer) error {
 	/* XXX: Assume target platform uses little endian. */
 	if err := binary.Write(w, binary.LittleEndian, elem); err != nil {
-		return util.ChildNewtError(err)
+		return errors.Wrapf(err, "failed to write MMR element")
 	}
 	return nil
 }
@@ -160,6 +160,43 @@ func (tlv *MetaTlv) Write(w io.Writer) (int, error) {
 	return sz, nil
 }
 
+func (tlv *MetaTlv) StructuredBody() (interface{}, error) {
+	r := bytes.NewReader(tlv.Data)
+
+	readBody := func(dst interface{}) error {
+		if err := binary.Read(r, binary.LittleEndian, dst); err != nil {
+			return errors.Wrapf(err, "error parsing TLV data")
+		}
+		return nil
+	}
+
+	switch tlv.Header.Type {
+	case META_TLV_TYPE_HASH:
+		var body MetaTlvBodyHash
+		if err := readBody(&body); err != nil {
+			return nil, err
+		}
+		return &body, nil
+
+	case META_TLV_TYPE_FLASH_AREA:
+		var body MetaTlvBodyFlashArea
+		if err := readBody(&body); err != nil {
+			return nil, err
+		}
+		return &body, nil
+
+	case META_TLV_TYPE_MMR_REF:
+		var body MetaTlvBodyMmrRef
+		if err := readBody(&body); err != nil {
+			return nil, err
+		}
+		return &body, nil
+
+	default:
+		return nil, errors.Errorf("unknown meta TLV type: %d", tlv.Header.Type)
+	}
+}
+
 func (meta *Meta) WritePlusOffsets(w io.Writer) (MetaOffsets, error) {
 	mo := MetaOffsets{}
 	sz := 0
@@ -270,25 +307,41 @@ func (meta *Meta) Hash() []byte {
 	return tlv.Data
 }
 
+func (meta *Meta) Clone() Meta {
+	var tlvs []MetaTlv
+	for _, src := range meta.Tlvs {
+		dst := MetaTlv{
+			Header: src.Header,
+			Data:   make([]byte, len(src.Data)),
+		}
+		copy(dst.Data, src.Data)
+		tlvs = append(tlvs, dst)
+	}
+
+	return Meta{
+		Tlvs:   tlvs,
+		Footer: meta.Footer,
+	}
+}
+
 func parseMetaTlv(bin []byte) (MetaTlv, int, error) {
 	r := bytes.NewReader(bin)
 
 	tlv := MetaTlv{}
 	if err := binary.Read(r, binary.LittleEndian, &tlv.Header); err != nil {
-		return tlv, 0, util.FmtNewtError(
-			"Error reading TLV header: %s", err.Error())
+		return tlv, 0, errors.Wrapf(err, "error reading TLV header")
 	}
 
 	data := make([]byte, tlv.Header.Size)
 	sz, err := r.Read(data)
 	if err != nil {
-		return tlv, 0, util.FmtNewtError(
-			"Error reading %d bytes of TLV data: %s",
-			tlv.Header.Size, err.Error())
+		return tlv, 0, errors.Wrapf(err,
+			"error reading %d bytes of TLV data",
+			tlv.Header.Size)
 	}
 	if sz != len(data) {
-		return tlv, 0, util.FmtNewtError(
-			"Error reading %d bytes of TLV data: incomplete read",
+		return tlv, 0, errors.Errorf(
+			"error reading %d bytes of TLV data: incomplete read",
 			tlv.Header.Size)
 	}
 	tlv.Data = data
@@ -301,13 +354,13 @@ func parseMetaFooter(bin []byte) (MetaFooter, int, error) {
 
 	var ftr MetaFooter
 	if err := binary.Read(r, binary.LittleEndian, &ftr); err != nil {
-		return ftr, 0, util.FmtNewtError(
-			"Error reading meta footer: %s", err.Error())
+		return ftr, 0, errors.Wrapf(err,
+			"error reading meta footer")
 	}
 
 	if ftr.Magic != META_MAGIC {
-		return ftr, 0, util.FmtNewtError(
-			"Meta footer contains invalid magic; exp:0x%08x, got:0x%08x",
+		return ftr, 0, errors.Errorf(
+			"meta footer contains invalid magic; exp:0x%08x, got:0x%08x",
 			META_MAGIC, ftr.Magic)
 	}
 
@@ -316,8 +369,8 @@ func parseMetaFooter(bin []byte) (MetaFooter, int, error) {
 
 func ParseMeta(bin []byte) (Meta, int, error) {
 	if len(bin) < META_FOOTER_SZ {
-		return Meta{}, 0, util.FmtNewtError(
-			"Binary too small to accommodate meta footer; "+
+		return Meta{}, 0, errors.Errorf(
+			"binary too small to accommodate meta footer; "+
 				"bin-size=%d ftr-size=%d", len(bin), META_FOOTER_SZ)
 	}
 
@@ -327,8 +380,8 @@ func ParseMeta(bin []byte) (Meta, int, error) {
 	}
 
 	if int(ftr.Size) > len(bin) {
-		return Meta{}, 0, util.FmtNewtError(
-			"Binary too small to accommodate meta region; "+
+		return Meta{}, 0, errors.Errorf(
+			"binary too small to accommodate meta region; "+
 				"bin-size=%d meta-size=%d", len(bin), ftr.Size)
 	}
 
diff --git a/mfg/mfg.go b/mfg/mfg.go
index 2d5df83..eea6abf 100644
--- a/mfg/mfg.go
+++ b/mfg/mfg.go
@@ -20,9 +20,11 @@
 package mfg
 
 import (
+	"bytes"
 	"crypto/sha256"
 
-	"mynewt.apache.org/newt/util"
+	"github.com/apache/mynewt-artifact/errors"
+	"github.com/apache/mynewt-artifact/flash"
 )
 
 const MFG_BIN_IMG_FILENAME = "mfgimg.bin"
@@ -43,12 +45,22 @@ func Parse(data []byte, metaEndOff int, eraseVal byte) (Mfg, error) {
 	}
 
 	if metaEndOff >= 0 {
+		if metaEndOff > len(data) {
+			return m, errors.Errorf(
+				"MMR offset (%d) beyond end of mfgimage (%d)",
+				metaEndOff, len(data))
+		}
+
 		meta, _, err := ParseMeta(data[:metaEndOff])
 		if err != nil {
 			return m, err
 		}
 		m.Meta = &meta
 		m.MetaOff = metaEndOff - int(meta.Footer.Size)
+
+		for i := 0; i < int(meta.Footer.Size); i++ {
+			m.Bin[m.MetaOff+i] = eraseVal
+		}
 	}
 
 	return m, nil
@@ -85,52 +97,78 @@ func CalcHash(bin []byte) []byte {
 	return hash[:]
 }
 
-func (m *Mfg) RecalcHash(eraseVal byte) error {
+func (m *Mfg) RecalcHash(eraseVal byte) ([]byte, error) {
+	// The hash TLV needs to be zeroed out in order to calculate the mfg
+	// hash.  Duplicate the mfg object so that we don't modify the
+	// original.
+	dup := m.Clone()
+	if dup.Meta != nil {
+		dup.Meta.ClearHash()
+	}
+
+	bin, err := dup.Bytes(eraseVal)
+	if err != nil {
+		return nil, err
+	}
+
+	return CalcHash(bin), nil
+}
+
+func (m *Mfg) RefillHash(eraseVal byte) error {
 	if m.Meta == nil || m.Meta.Hash() == nil {
 		return nil
 	}
+	tlv := m.Meta.FindFirstTlv(META_TLV_TYPE_HASH)
+	if tlv == nil {
+		return nil
+	}
 
-	// First, write with zeroed hash.
-	m.Meta.ClearHash()
-	bin, err := m.Bytes(eraseVal)
+	// Calculate hash.
+	hash, err := m.RecalcHash(eraseVal)
 	if err != nil {
 		return err
 	}
 
-	// Calculate hash and fill TLV.
-	tlv := m.Meta.FindFirstTlv(META_TLV_TYPE_HASH)
-	if tlv != nil {
-		hashData := CalcHash(bin)
-		copy(tlv.Data, hashData)
-
-		hashOff := m.MetaOff + m.Meta.HashOffset()
-		if hashOff+META_HASH_SZ > len(bin) {
-			return util.FmtNewtError(
-				"unexpected error: hash extends beyond end " +
-					"of manufacturing image")
-		}
-	}
+	// Fill hash TLV.
+	copy(tlv.Data, hash)
 
 	return nil
 }
 
-func (m *Mfg) Hash() ([]byte, error) {
+func (m *Mfg) Hash(eraseVal byte) ([]byte, error) {
 	var hashBytes []byte
+
 	if m.Meta != nil {
 		hashBytes = m.Meta.Hash()
 	}
+
 	if hashBytes == nil {
 		// No hash TLV; calculate hash manually.
-		bin, err := m.Bytes(0xff)
+		b, err := m.RecalcHash(eraseVal)
 		if err != nil {
 			return nil, err
 		}
-		hashBytes = CalcHash(bin)
+		hashBytes = b
 	}
 
 	return hashBytes, nil
 }
 
+func (m *Mfg) HashIsValid(eraseVal byte) (bool, error) {
+	// If the mfg doesn't contain a hash TLV, then there is nothing to verify.
+	tlv := m.Meta.FindFirstTlv(META_TLV_TYPE_HASH)
+	if tlv == nil {
+		return true, nil
+	}
+
+	hash, err := m.RecalcHash(eraseVal)
+	if err != nil {
+		return false, err
+	}
+
+	return bytes.Equal(hash, tlv.Data), nil
+}
+
 func (m *Mfg) Bytes(eraseVal byte) ([]byte, error) {
 	binCopy := make([]byte, len(m.Bin))
 	copy(binCopy, m.Bin)
@@ -149,3 +187,51 @@ func (m *Mfg) Bytes(eraseVal byte) ([]byte, error) {
 
 	return binCopy, nil
 }
+
+func (m *Mfg) Clone() Mfg {
+	var meta *Meta
+	if m.Meta != nil {
+		metaDup := m.Meta.Clone()
+		meta = &metaDup
+	}
+
+	bin := make([]byte, len(m.Bin))
+	copy(bin, m.Bin)
+
+	return Mfg{
+		Bin:     bin,
+		Meta:    meta,
+		MetaOff: m.MetaOff,
+	}
+}
+
+func (m *Mfg) ExtractFlashArea(area flash.FlashArea, eraseVal byte) ([]byte, error) {
+	b, err := m.Bytes(eraseVal)
+	if err != nil {
+		return nil, err
+	}
+
+	if area.Offset >= len(b) {
+		return nil, errors.Errorf(
+			"flash area in mmr (\"%s\") is beyond end of mfgimage "+
+				"(offset=%d mfgimg_len=%d)",
+			area.Name, area.Offset, len(b))
+	}
+
+	// If the end of the target contains unwritten bytes, it gets truncated
+	// from the mfgimage.
+	end := area.Offset + area.Size
+	if end > len(b) {
+		end = len(b)
+	}
+
+	return b[area.Offset:end], nil
+}
+
+func (m *Mfg) Tlvs() []MetaTlv {
+	if m.Meta == nil {
+		return nil
+	} else {
+		return m.Meta.Tlvs
+	}
+}
diff --git a/mfg/verify.go b/mfg/verify.go
new file mode 100644
index 0000000..17a5a76
--- /dev/null
+++ b/mfg/verify.go
@@ -0,0 +1,202 @@
+package mfg
+
+import (
+	"bytes"
+	"encoding/hex"
+
+	"github.com/apache/mynewt-artifact/errors"
+	"github.com/apache/mynewt-artifact/flash"
+	"github.com/apache/mynewt-artifact/image"
+	"github.com/apache/mynewt-artifact/manifest"
+)
+
+func (m *Mfg) validateManFlashMap(man manifest.MfgManifest) error {
+	idAreaMap := map[int]flash.FlashArea{}
+	for _, area := range man.FlashAreas {
+		if _, dup := idAreaMap[area.Id]; dup {
+			return errors.Errorf(
+				"mfg manifest contains duplicate flash area: %d", area.Id)
+		}
+
+		idAreaMap[area.Id] = area
+	}
+
+	seen := map[int]struct{}{}
+
+	mmrHasFlash := man.Meta != nil && man.Meta.FlashMap
+
+	for _, t := range m.Tlvs() {
+		if t.Header.Type == META_TLV_TYPE_FLASH_AREA {
+			if !mmrHasFlash {
+				return errors.Errorf(
+					"mmr contains flash map; manifest indicates otherwise")
+			}
+
+			body, err := t.StructuredBody()
+			if err != nil {
+				return err
+			}
+			flashBody := body.(*MetaTlvBodyFlashArea)
+			if _, ok := idAreaMap[int(flashBody.Area)]; !ok {
+				return errors.Errorf(
+					"flash area %d missing from mfg manifest", flashBody.Area)
+			}
+
+			seen[int(flashBody.Area)] = struct{}{}
+		}
+	}
+
+	if mmrHasFlash {
+		for _, area := range man.FlashAreas {
+			if _, ok := seen[area.Id]; !ok {
+				return errors.Errorf("flash area %d missing from mmr", area.Id)
+			}
+		}
+	}
+
+	return nil
+}
+
+func (m *Mfg) validateManMmrs(man manifest.MfgManifest) error {
+	areaMap := map[int]struct{}{}
+	if man.Meta != nil {
+		for _, mmr := range man.Meta.Mmrs {
+			fa := man.FindFlashAreaName(mmr.Area)
+			if fa == nil {
+				return errors.Errorf(
+					"flash area %s missing from mfg manifest", mmr.Area)
+			}
+
+			if _, dup := areaMap[fa.Id]; dup {
+				return errors.Errorf(
+					"mfg manifest contains duplicate mmr ref: %s", mmr.Area)
+			}
+
+			areaMap[fa.Id] = struct{}{}
+		}
+	}
+
+	seen := map[int]struct{}{}
+	for _, t := range m.Tlvs() {
+		if t.Header.Type == META_TLV_TYPE_MMR_REF {
+			body, err := t.StructuredBody()
+			if err != nil {
+				return err
+			}
+
+			mmrBody := body.(*MetaTlvBodyMmrRef)
+			if _, ok := areaMap[int(mmrBody.Area)]; !ok {
+				return errors.Errorf(
+					"mmr ref %d missing from mfg manifest", mmrBody.Area)
+			}
+
+			seen[int(mmrBody.Area)] = struct{}{}
+		}
+	}
+
+	for area, _ := range areaMap {
+		if _, ok := seen[area]; !ok {
+			return errors.Errorf("mmr ref %d missing from mmr", area)
+		}
+	}
+
+	return nil
+}
+
+func (m *Mfg) validateManTargets(man manifest.MfgManifest) error {
+	for _, t := range man.Targets {
+		fa := man.FindFlashArea(man.Device, t.Offset)
+		if fa == nil {
+			return errors.Errorf(
+				"no flash area in mfgimage corresponding to target \"%s\"",
+				t.Name)
+		}
+
+		data, err := m.ExtractFlashArea(*fa, man.EraseVal)
+		if err != nil {
+			return err
+		}
+
+		if !t.IsBoot() {
+			img, err := image.ParseImage(data)
+			if err != nil {
+				return errors.Wrapf(err,
+					"error parsing build \"%s\" embedded in mfgimage", t.Name)
+			}
+
+			if err := img.Verify(); err != nil {
+				return errors.Wrapf(err,
+					"mfgimage contains invalid build \"%s\"", t.Name)
+			}
+		}
+	}
+
+	return nil
+}
+
+// VerifyManifest compares an mfgimage's structure to its manifest.  It
+// returns an error if the mfgimage doesn't match the manifest.
+func (m *Mfg) VerifyManifest(man manifest.MfgManifest) error {
+	if man.Format != 2 {
+		return errors.Errorf(
+			"only mfgimage format 2 supported (have=%d)", man.Format)
+	}
+
+	mfgHash, err := m.Hash(man.EraseVal)
+	if err != nil {
+		return err
+	}
+	hashStr := hex.EncodeToString(mfgHash)
+	if hashStr != man.MfgHash {
+		return errors.Errorf(
+			"manifest mfg hash different from mmr: man=%s mfg=%s",
+			man.MfgHash, hashStr)
+	}
+
+	if err := m.validateManFlashMap(man); err != nil {
+		return err
+	}
+
+	if err := m.validateManMmrs(man); err != nil {
+		return err
+	}
+
+	// Verify each embedded build.
+	if err := m.validateManTargets(man); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Verify checks an mfgimage's structure and internal consistency.  It
+// returns an error if the mfgimage is incorrect.
+func (m *Mfg) Verify(eraseVal byte) error {
+	for _, t := range m.Tlvs() {
+		// Verify that TLV has a valid `type` field.
+		body, err := t.StructuredBody()
+		if err != nil {
+			return err
+		}
+
+		// Verify contents of hash TLV.
+		switch t.Header.Type {
+		case META_TLV_TYPE_HASH:
+			hashBody := body.(*MetaTlvBodyHash)
+
+			hash, err := m.RecalcHash(eraseVal)
+			if err != nil {
+				return err
+			}
+
+			if !bytes.Equal(hash, hashBody.Hash[:]) {
+				return errors.Errorf(
+					"mmr contains incorrect hash: have=%s want=%s",
+					hex.EncodeToString(hashBody.Hash[:]),
+					hex.EncodeToString(hash))
+			}
+		}
+	}
+
+	return nil
+}
diff --git a/sec/encrypt.go b/sec/encrypt.go
index 35ecdd7..d58806c 100644
--- a/sec/encrypt.go
+++ b/sec/encrypt.go
@@ -25,13 +25,13 @@ import (
 	"crypto/cipher"
 	"io"
 
-	"mynewt.apache.org/newt/util"
+	"github.com/apache/mynewt-artifact/errors"
 )
 
 func EncryptAES(plain []byte, secret []byte) ([]byte, error) {
 	block, err := aes.NewCipher(secret)
 	if err != nil {
-		return nil, util.NewNewtError("Failed to create block cipher")
+		return nil, errors.Errorf("Failed to create block cipher")
 	}
 	nonce := make([]byte, 16)
 	stream := cipher.NewCTR(block, nonce)
@@ -43,8 +43,7 @@ func EncryptAES(plain []byte, secret []byte) ([]byte, error) {
 	for {
 		cnt, err := r.Read(dataBuf)
 		if err != nil && err != io.EOF {
-			return nil, util.FmtNewtError(
-				"Failed to read from plaintext: %s", err.Error())
+			return nil, errors.Wrapf(err, "Failed to read from plaintext")
 		}
 		if cnt == 0 {
 			break
@@ -52,8 +51,7 @@ func EncryptAES(plain []byte, secret []byte) ([]byte, error) {
 
 		stream.XORKeyStream(encBuf, dataBuf[0:cnt])
 		if _, err = w.Write(encBuf[0:cnt]); err != nil {
-			return nil, util.FmtNewtError(
-				"Failed to write ciphertext: %s", err.Error())
+			return nil, errors.Wrapf(err, "failed to write ciphertext")
 		}
 	}
 
diff --git a/sec/key.go b/sec/key.go
index 9d073bd..473ef62 100644
--- a/sec/key.go
+++ b/sec/key.go
@@ -33,8 +33,7 @@ import (
 	"io/ioutil"
 
 	keywrap "github.com/NickBall/go-aes-key-wrap"
-
-	"mynewt.apache.org/newt/util"
+	"github.com/apache/mynewt-artifact/errors"
 )
 
 type SignKey struct {
@@ -43,6 +42,11 @@ type SignKey struct {
 	Ec  *ecdsa.PrivateKey
 }
 
+type PubSignKey struct {
+	Rsa *rsa.PublicKey
+	Ec  *ecdsa.PublicKey
+}
+
 func ParsePrivateKey(keyBytes []byte) (interface{}, error) {
 	var privKey interface{}
 	var err error
@@ -63,8 +67,7 @@ func ParsePrivateKey(keyBytes []byte) (interface{}, error) {
 		 */
 		privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
 		if err != nil {
-			return nil, util.FmtNewtError(
-				"Private key parsing failed: %s", err)
+			return nil, errors.Wrapf(err, "Private key parsing failed")
 		}
 	}
 	if block != nil && block.Type == "EC PRIVATE KEY" {
@@ -73,8 +76,7 @@ func ParsePrivateKey(keyBytes []byte) (interface{}, error) {
 		 */
 		privKey, err = x509.ParseECPrivateKey(block.Bytes)
 		if err != nil {
-			return nil, util.FmtNewtError(
-				"Private key parsing failed: %s", err)
+			return nil, errors.Wrapf(err, "Private key parsing failed")
 		}
 	}
 	if block != nil && block.Type == "PRIVATE KEY" {
@@ -83,8 +85,7 @@ func ParsePrivateKey(keyBytes []byte) (interface{}, error) {
 		// the key itself.
 		privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
 		if err != nil {
-			return nil, util.FmtNewtError(
-				"Private key parsing failed: %s", err)
+			return nil, errors.Wrapf(err, "Private key parsing failed")
 		}
 	}
 	if block != nil && block.Type == "ENCRYPTED PRIVATE KEY" {
@@ -92,12 +93,12 @@ func ParsePrivateKey(keyBytes []byte) (interface{}, error) {
 		// encryption.
 		privKey, err = parseEncryptedPrivateKey(block.Bytes)
 		if err != nil {
-			return nil, util.FmtNewtError(
-				"Unable to decode encrypted private key: %s", err)
+			return nil, errors.Wrapf(
+				err, "Unable to decode encrypted private key")
 		}
 	}
 	if privKey == nil {
-		return nil, util.NewNewtError(
+		return nil, errors.Errorf(
 			"Unknown private key format, EC/RSA private " +
 				"key in PEM format only.")
 	}
@@ -119,7 +120,7 @@ func BuildPrivateKey(keyBytes []byte) (SignKey, error) {
 	case *ecdsa.PrivateKey:
 		key.Ec = priv
 	default:
-		return key, util.NewNewtError("Unknown private key format")
+		return key, errors.Errorf("Unknown private key format")
 	}
 
 	return key, nil
@@ -128,8 +129,7 @@ func BuildPrivateKey(keyBytes []byte) (SignKey, error) {
 func ReadKey(filename string) (SignKey, error) {
 	keyBytes, err := ioutil.ReadFile(filename)
 	if err != nil {
-		return SignKey{}, util.FmtNewtError(
-			"Error reading key file: %s", err)
+		return SignKey{}, errors.Wrapf(err, "Error reading key file")
 	}
 
 	return BuildPrivateKey(keyBytes)
@@ -154,31 +154,21 @@ func (key *SignKey) AssertValid() {
 	if key.Rsa == nil && key.Ec == nil {
 		panic("invalid key; neither RSA nor ECC")
 	}
-
-	if key.Rsa != nil && key.Ec != nil {
-		panic("invalid key; neither RSA nor ECC")
-	}
 }
 
-func (key *SignKey) PubBytes() ([]uint8, error) {
+func (key *SignKey) PubKey() PubSignKey {
 	key.AssertValid()
 
-	var pubkey []byte
-
 	if key.Rsa != nil {
-		pubkey, _ = asn1.Marshal(key.Rsa.PublicKey)
+		return PubSignKey{Rsa: &key.Rsa.PublicKey}
 	} else {
-		switch key.Ec.Curve.Params().Name {
-		case "P-224":
-			fallthrough
-		case "P-256":
-			pubkey, _ = x509.MarshalPKIXPublicKey(&key.Ec.PublicKey)
-		default:
-			return nil, util.NewNewtError("Unsupported ECC curve")
-		}
+		return PubSignKey{Ec: &key.Ec.PublicKey}
 	}
+}
 
-	return pubkey, nil
+func (key *SignKey) PubBytes() ([]byte, error) {
+	pk := key.PubKey()
+	return pk.PubBytes()
 }
 
 func RawKeyHash(pubKeyBytes []byte) []byte {
@@ -211,13 +201,12 @@ func ParsePubKePem(keyBytes []byte) (*rsa.PublicKey, error) {
 	}
 
 	if b.Type != "PUBLIC KEY" && b.Type != "RSA PUBLIC KEY" {
-		return nil, util.NewNewtError("Invalid PEM file")
+		return nil, errors.Errorf("Invalid PEM file")
 	}
 
 	pub, err := x509.ParsePKIXPublicKey(b.Bytes)
 	if err != nil {
-		return nil, util.FmtNewtError(
-			"Error parsing pubkey file: %s", err.Error())
+		return nil, errors.Wrapf(err, "Error parsing pubkey file")
 	}
 
 	var pubk *rsa.PublicKey
@@ -225,8 +214,7 @@ func ParsePubKePem(keyBytes []byte) (*rsa.PublicKey, error) {
 	case *rsa.PublicKey:
 		pubk = pub.(*rsa.PublicKey)
 	default:
-		return nil, util.FmtNewtError(
-			"Error parsing pubkey file: %s", err.Error())
+		return nil, errors.Wrapf(err, "Error parsing pubkey file")
 	}
 
 	return pubk, nil
@@ -235,8 +223,7 @@ func ParsePubKePem(keyBytes []byte) (*rsa.PublicKey, error) {
 func ParsePrivKeDer(keyBytes []byte) (*rsa.PrivateKey, error) {
 	privKey, err := x509.ParsePKCS1PrivateKey(keyBytes)
 	if err != nil {
-		return nil, util.FmtNewtError(
-			"Error parsing private key file: %s", err.Error())
+		return nil, errors.Wrapf(err, "Error parsing private key file")
 	}
 
 	return privKey, nil
@@ -247,8 +234,7 @@ func EncryptSecretRsa(pubk *rsa.PublicKey, plainSecret []byte) ([]byte, error) {
 	cipherSecret, err := rsa.EncryptOAEP(
 		sha256.New(), rng, pubk, plainSecret, nil)
 	if err != nil {
-		return nil, util.FmtNewtError(
-			"Error from encryption: %s\n", err.Error())
+		return nil, errors.Wrapf(err, "Error from encryption")
 	}
 
 	return cipherSecret, nil
@@ -261,8 +247,7 @@ func DecryptSecretRsa(privk *rsa.PrivateKey,
 	plainSecret, err := rsa.DecryptOAEP(
 		sha256.New(), rng, privk, cipherSecret, nil)
 	if err != nil {
-		return nil, util.FmtNewtError(
-			"Error from encryption: %s\n", err.Error())
+		return nil, errors.Wrapf(err, "Error from encryption")
 	}
 
 	return plainSecret, nil
@@ -271,18 +256,18 @@ func DecryptSecretRsa(privk *rsa.PrivateKey,
 func ParseKeBase64(keyBytes []byte) (cipher.Block, error) {
 	kek, err := base64.StdEncoding.DecodeString(string(keyBytes))
 	if err != nil {
-		return nil, util.FmtNewtError(
-			"Error decoding kek: %s", err.Error())
+		return nil, errors.Wrapf(err,
+			"Error decoding kek: %s")
 	}
 	if len(kek) != 16 {
-		return nil, util.FmtNewtError(
+		return nil, errors.Errorf(
 			"Unexpected key size: %d != 16", len(kek))
 	}
 
 	cipher, err := aes.NewCipher(kek)
 	if err != nil {
-		return nil, util.FmtNewtError(
-			"Error creating keywrap cipher: %s", err.Error())
+		return nil, errors.Wrapf(err,
+			"Error creating keywrap cipher")
 	}
 
 	return cipher, nil
@@ -291,8 +276,39 @@ func ParseKeBase64(keyBytes []byte) (cipher.Block, error) {
 func EncryptSecretAes(c cipher.Block, plainSecret []byte) ([]byte, error) {
 	cipherSecret, err := keywrap.Wrap(c, plainSecret)
 	if err != nil {
-		return nil, util.FmtNewtError("Error key-wrapping: %s", err.Error())
+		return nil, errors.Wrapf(err, "Error key-wrapping")
 	}
 
 	return cipherSecret, nil
 }
+
+func (key *PubSignKey) AssertValid() {
+	if key.Rsa == nil && key.Ec == nil {
+		panic("invalid public key; neither RSA nor ECC")
+	}
+}
+
+func (key *PubSignKey) PubBytes() ([]byte, error) {
+	key.AssertValid()
+
+	var pubBytes []byte
+
+	if key.Rsa != nil {
+		var err error
+		pubBytes, err = asn1.Marshal(*key.Rsa)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		switch key.Ec.Curve.Params().Name {
+		case "P-224":
+			fallthrough
+		case "P-256":
+			pubBytes, _ = x509.MarshalPKIXPublicKey(*key.Ec)
+		default:
+			return nil, errors.Errorf("unsupported ECC curve")
+		}
+	}
+
+	return pubBytes, nil
+}
diff --git a/sec/sig.go b/sec/sig.go
new file mode 100644
index 0000000..8aa0b32
--- /dev/null
+++ b/sec/sig.go
@@ -0,0 +1,55 @@
+package sec
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/rsa"
+
+	"github.com/apache/mynewt-artifact/errors"
+)
+
+type Sig struct {
+	KeyHash []byte
+	Data    []byte
+}
+
+func checkOneKeyOneSig(k PubSignKey, sig Sig, hash []byte) (bool, error) {
+	pubBytes, err := k.PubBytes()
+	if err != nil {
+		return false, errors.WithStack(err)
+	}
+	keyHash := RawKeyHash(pubBytes)
+
+	if !bytes.Equal(keyHash, sig.KeyHash) {
+		return false, nil
+	}
+
+	if k.Rsa != nil {
+		opts := rsa.PSSOptions{
+			SaltLength: rsa.PSSSaltLengthEqualsHash,
+		}
+		err := rsa.VerifyPSS(k.Rsa, crypto.SHA256, hash, sig.Data, &opts)
+		return err == nil, nil
+	}
+
+	if k.Ec != nil {
+		return false, errors.Errorf(
+			"ecdsa signature verification not supported")
+	}
+
+	return false, nil
+}
+
+func VerifySigs(key PubSignKey, sigs []Sig, hash []byte) (int, error) {
+	for i, s := range sigs {
+		match, err := checkOneKeyOneSig(key, s, hash)
+		if err != nil {
+			return -1, err
+		}
+		if match {
+			return i, nil
+		}
+	}
+
+	return -1, nil
+}