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/01/04 18:21:16 UTC

[mynewt-newt] 11/17: Support for the 2.0 mfgimage format

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

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

commit b7f07170bff243c994d1d6d3dfdad63488c6d7db
Author: Christopher Collins <cc...@apache.org>
AuthorDate: Fri Dec 14 14:19:57 2018 -0800

    Support for the 2.0 mfgimage format
---
 artifact/image/create.go                  | 101 ++----
 artifact/image/image.go                   |   6 +
 artifact/image/key.go                     |  19 +-
 artifact/image/v1.go                      |   2 +-
 artifact/manifest/manifest.go             |   3 +-
 artifact/manifest/mfg_manifest.go         |  73 ++++
 artifact/mfg/map_meta.go                  | 153 ++++++++
 artifact/mfg/meta.go                      | 352 +++++++++++++++++++
 artifact/mfg/mfg.go                       | 113 ++++++
 newt/mfg/read.go => artifact/mfg/paths.go |  23 +-
 larva/cli/image_cmds.go                   | 341 ++++++++++++++++--
 larva/cli/mfg_cmds.go                     | 189 +++++++---
 larva/lvimg/lvimg.go                      | 116 ++++++
 larva/{mfg/mfg.go => lvmfg/lvmfg.go}      | 111 +++---
 newt/builder/targetbuild.go               |   1 +
 newt/cli/build_cmds.go                    |  11 +
 newt/cli/mfg_cmds.go                      |  56 ++-
 newt/flashmap/flashmap.go                 |  17 -
 newt/imgprod/imgprod.go                   |  14 +-
 newt/imgprod/v1.go                        |   9 +-
 newt/manifest/manifest.go                 |  47 ++-
 newt/mfg/build.go                         | 564 ++++++++++++++++++++++++++++++
 newt/mfg/create.go                        | 535 ----------------------------
 newt/mfg/decode.go                        | 310 ++++++++++++++++
 newt/mfg/emit.go                          | 342 ++++++++++++++++++
 newt/mfg/load.go                          | 317 -----------------
 newt/mfg/meta.go                          | 248 -------------
 newt/mfg/mfg.go                           |  98 ------
 newt/mfg/misc.go                          |  92 +++++
 newt/mfg/part.go                          |  96 +++++
 newt/mfg/paths.go                         | 154 +-------
 31 files changed, 2905 insertions(+), 1608 deletions(-)

diff --git a/artifact/image/create.go b/artifact/image/create.go
index 5b28120..c3e8820 100644
--- a/artifact/image/create.go
+++ b/artifact/image/create.go
@@ -90,12 +90,12 @@ func generateEncTlv(cipherSecret []byte) (ImageTlv, error) {
 	}, nil
 }
 
-func generateSigRsa(key *rsa.PrivateKey, hash []byte) ([]byte, error) {
+func generateSigRsa(key ImageSigKey, hash []byte) ([]byte, error) {
 	opts := rsa.PSSOptions{
 		SaltLength: rsa.PSSSaltLengthEqualsHash,
 	}
 	signature, err := rsa.SignPSS(
-		rand.Reader, key, crypto.SHA256, hash, &opts)
+		rand.Reader, key.Rsa, crypto.SHA256, hash, &opts)
 	if err != nil {
 		return nil, util.FmtNewtError("Failed to compute signature: %s", err)
 	}
@@ -103,24 +103,8 @@ func generateSigRsa(key *rsa.PrivateKey, hash []byte) ([]byte, error) {
 	return signature, nil
 }
 
-func generateSigTlvRsa(key ImageSigKey, hash []byte) (ImageTlv, error) {
-	sig, err := generateSigRsa(key.Rsa, hash)
-	if err != nil {
-		return ImageTlv{}, err
-	}
-
-	return ImageTlv{
-		Header: ImageTlvHdr{
-			Type: key.sigTlvType(),
-			Pad:  0,
-			Len:  256, /* 2048 bits */
-		},
-		Data: sig,
-	}, nil
-}
-
-func generateSigEc(key *ecdsa.PrivateKey, hash []byte) ([]byte, error) {
-	r, s, err := ecdsa.Sign(rand.Reader, key, hash)
+func generateSigEc(key ImageSigKey, 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)
 	}
@@ -135,88 +119,65 @@ func generateSigEc(key *ecdsa.PrivateKey, hash []byte) ([]byte, error) {
 		return nil, util.FmtNewtError("Failed to construct signature: %s", err)
 	}
 
-	return signature, nil
-}
-
-func generateSigTlvEc(key ImageSigKey, hash []byte) (ImageTlv, error) {
-	sig, err := generateSigEc(key.Ec, hash)
-	if err != nil {
-		return ImageTlv{}, err
-	}
-
 	sigLen := key.sigLen()
-	if len(sig) > int(sigLen) {
-		return ImageTlv{}, util.FmtNewtError("Something is really wrong\n")
-	}
-
-	b := &bytes.Buffer{}
-
-	if _, err := b.Write(sig); err != nil {
-		return ImageTlv{},
-			util.FmtNewtError("Failed to append sig: %s", err.Error())
+	if len(signature) > int(sigLen) {
+		return nil, util.FmtNewtError("Something is really wrong\n")
 	}
 
-	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())
-	}
+	pad := make([]byte, int(sigLen)-len(signature))
+	signature = append(signature, pad...)
 
-	return ImageTlv{
-		Header: ImageTlvHdr{
-			Type: key.sigTlvType(),
-			Pad:  0,
-			Len:  sigLen,
-		},
-		Data: b.Bytes(),
-	}, nil
+	return signature, nil
 }
 
-func generateSigTlv(key ImageSigKey, hash []byte) (ImageTlv, error) {
+func generateSig(key ImageSigKey, hash []byte) ([]byte, error) {
 	key.assertValid()
 
 	if key.Rsa != nil {
-		return generateSigTlvRsa(key, hash)
+		return generateSigRsa(key, hash)
 	} else {
-		return generateSigTlvEc(key, hash)
+		return generateSigEc(key, hash)
 	}
 }
 
-func generateKeyHashTlv(key ImageSigKey) (ImageTlv, error) {
-	key.assertValid()
-
-	keyHash, err := key.sigKeyHash()
-	if err != nil {
-		return ImageTlv{}, util.FmtNewtError(
-			"Failed to compute hash of the public key: %s", err.Error())
-	}
-
+func BuildKeyHashTlv(keyBytes []byte) ImageTlv {
+	data := RawKeyHash(keyBytes)
 	return ImageTlv{
 		Header: ImageTlvHdr{
 			Type: IMAGE_TLV_KEYHASH,
 			Pad:  0,
-			Len:  uint16(len(keyHash)),
+			Len:  uint16(len(data)),
 		},
-		Data: keyHash,
-	}, nil
+		Data: data,
+	}
 }
 
-func GenerateSigTlvs(keys []ImageSigKey, hash []byte) ([]ImageTlv, error) {
+func BuildSigTlvs(keys []ImageSigKey, hash []byte) ([]ImageTlv, error) {
 	var tlvs []ImageTlv
 
 	for _, key := range keys {
 		key.assertValid()
 
-		tlv, err := generateKeyHashTlv(key)
+		// Key hash TLV.
+		pubKey, err := key.PubBytes()
 		if err != nil {
 			return nil, err
 		}
+		tlv := BuildKeyHashTlv(pubKey)
 		tlvs = append(tlvs, tlv)
 
-		tlv, err = generateSigTlv(key, hash)
+		// Signature TLV.
+		sig, err := generateSig(key, hash)
 		if err != nil {
 			return nil, err
 		}
+		tlv = ImageTlv{
+			Header: ImageTlvHdr{
+				Type: key.sigTlvType(),
+				Len:  uint16(len(sig)),
+			},
+			Data: sig,
+		}
 		tlvs = append(tlvs, tlv)
 	}
 
@@ -409,7 +370,7 @@ func (ic *ImageCreator) Create() (Image, error) {
 	}
 	ri.Tlvs = append(ri.Tlvs, tlv)
 
-	tlvs, err := GenerateSigTlvs(ic.SigKeys, hashBytes)
+	tlvs, err := BuildSigTlvs(ic.SigKeys, hashBytes)
 	if err != nil {
 		return ri, err
 	}
diff --git a/artifact/image/image.go b/artifact/image/image.go
index 244cfed..1ed093f 100644
--- a/artifact/image/image.go
+++ b/artifact/image/image.go
@@ -134,6 +134,12 @@ func ImageTlvTypeName(tlvType uint8) string {
 	return name
 }
 
+func ImageTlvTypeIsSig(tlvType uint8) bool {
+	return tlvType == IMAGE_TLV_RSA2048 ||
+		tlvType == IMAGE_TLV_ECDSA224 ||
+		tlvType == IMAGE_TLV_ECDSA256
+}
+
 func ParseVersion(versStr string) (ImageVersion, error) {
 	var err error
 	var major uint64
diff --git a/artifact/image/key.go b/artifact/image/key.go
index 9141f6e..8345cd9 100644
--- a/artifact/image/key.go
+++ b/artifact/image/key.go
@@ -152,25 +152,30 @@ func (key *ImageSigKey) assertValid() {
 	}
 }
 
-func (key *ImageSigKey) sigKeyHash() ([]uint8, error) {
+func (key *ImageSigKey) PubBytes() ([]uint8, error) {
 	key.assertValid()
 
+	var pubkey []byte
+
 	if key.Rsa != nil {
-		pubkey, _ := asn1.Marshal(key.Rsa.PublicKey)
-		sum := sha256.Sum256(pubkey)
-		return sum[:4], nil
+		pubkey, _ = asn1.Marshal(key.Rsa.PublicKey)
 	} else {
 		switch key.Ec.Curve.Params().Name {
 		case "P-224":
 			fallthrough
 		case "P-256":
-			pubkey, _ := x509.MarshalPKIXPublicKey(&key.Ec.PublicKey)
-			sum := sha256.Sum256(pubkey)
-			return sum[:4], nil
+			pubkey, _ = x509.MarshalPKIXPublicKey(&key.Ec.PublicKey)
 		default:
 			return nil, util.NewNewtError("Unsupported ECC curve")
 		}
 	}
+
+	return pubkey, nil
+}
+
+func RawKeyHash(pubKeyBytes []byte) []byte {
+	sum := sha256.Sum256(pubKeyBytes)
+	return sum[:4]
 }
 
 func (key *ImageSigKey) sigLen() uint16 {
diff --git a/artifact/image/v1.go b/artifact/image/v1.go
index bab86f6..5540d85 100644
--- a/artifact/image/v1.go
+++ b/artifact/image/v1.go
@@ -233,7 +233,7 @@ func generateV1SigTlvRsa(key ImageSigKey, hash []byte) (ImageTlv, error) {
 }
 
 func generateV1SigTlvEc(key ImageSigKey, hash []byte) (ImageTlv, error) {
-	sig, err := generateSigEc(key.Ec, hash)
+	sig, err := generateSigEc(key, hash)
 	if err != nil {
 		return ImageTlv{}, err
 	}
diff --git a/artifact/manifest/manifest.go b/artifact/manifest/manifest.go
index 62d8c06..3a3376a 100644
--- a/artifact/manifest/manifest.go
+++ b/artifact/manifest/manifest.go
@@ -5,7 +5,6 @@ import (
 	"io"
 	"io/ioutil"
 
-	"mynewt.apache.org/newt/artifact/flash"
 	"mynewt.apache.org/newt/util"
 )
 
@@ -57,7 +56,7 @@ type Manifest struct {
 	LoaderPkgs []*ManifestPkg    `json:"loader_pkgs,omitempty"`
 	TgtVars    []string          `json:"target"`
 	Repos      []*ManifestRepo   `json:"repos"`
-	FlashAreas []flash.FlashArea `json:"flash_map"`
+	Syscfg     map[string]string `json:"syscfg"`
 
 	PkgSizes       []*ManifestSizePkg `json:"pkgsz"`
 	LoaderPkgSizes []*ManifestSizePkg `json:"loader_pkgsz,omitempty"`
diff --git a/artifact/manifest/mfg_manifest.go b/artifact/manifest/mfg_manifest.go
new file mode 100644
index 0000000..25cf9b7
--- /dev/null
+++ b/artifact/manifest/mfg_manifest.go
@@ -0,0 +1,73 @@
+package manifest
+
+import (
+	"encoding/json"
+	"io/ioutil"
+
+	"mynewt.apache.org/newt/artifact/flash"
+	"mynewt.apache.org/newt/util"
+)
+
+type MfgManifestTarget struct {
+	Name         string `json:"name"`
+	Offset       int    `json:"offset"`
+	BinPath      string `json:"bin_path,omitempty"`
+	ImagePath    string `json:"image_path,omitempty"`
+	ManifestPath string `json:"manifest_path"`
+}
+
+type MfgManifestMetaMmr struct {
+	Area      string `json:"area"`
+	Device    int    `json:"_device"`
+	EndOffset int    `json:"_end_offset"`
+}
+
+type MfgManifestMeta struct {
+	EndOffset int                  `json:"end_offset"`
+	Size      int                  `json:"size"`
+	Hash      bool                 `json:"hash_present"`
+	FlashMap  bool                 `json:"flash_map_present"`
+	Mmrs      []MfgManifestMetaMmr `json:"mmrs,omitempty"`
+	// XXX: refhash
+}
+
+type MfgManifest struct {
+	Name       string            `json:"name"`
+	BuildTime  string            `json:"build_time"`
+	Format     int               `json:"format"`
+	MfgHash    string            `json:"mfg_hash"`
+	Version    string            `json:"version"`
+	Device     int               `json:"device"`
+	BinPath    string            `json:"bin_path"`
+	FlashAreas []flash.FlashArea `json:"flash_map"`
+
+	Targets []MfgManifestTarget `json:"targets"`
+	Meta    *MfgManifestMeta    `json:"meta,omitempty"`
+}
+
+func ReadMfgManifest(path string) (MfgManifest, error) {
+	m := MfgManifest{}
+
+	content, err := ioutil.ReadFile(path)
+	if err != nil {
+		return m, util.ChildNewtError(err)
+	}
+
+	if err := json.Unmarshal(content, &m); err != nil {
+		return m, util.FmtNewtError(
+			"Failure decoding mfg manifest with path \"%s\": %s",
+			path, err.Error())
+	}
+
+	return m, nil
+}
+
+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 buffer, nil
+}
diff --git a/artifact/mfg/map_meta.go b/artifact/mfg/map_meta.go
new file mode 100644
index 0000000..b16c37e
--- /dev/null
+++ b/artifact/mfg/map_meta.go
@@ -0,0 +1,153 @@
+/**
+ * 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 mfg
+
+import (
+	"bytes"
+	"encoding/binary"
+	"encoding/hex"
+	"encoding/json"
+
+	"mynewt.apache.org/newt/util"
+)
+
+func (t *MetaTlv) bodyMap() (map[string]interface{}, error) {
+	r := bytes.NewReader(t.Data)
+
+	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 nil
+	}
+
+	switch t.Header.Type {
+	case META_TLV_TYPE_HASH:
+		var body MetaTlvBodyHash
+		if err := readBody(&body); err != nil {
+			return nil, err
+		}
+		return body.Map(), nil
+
+	case META_TLV_TYPE_FLASH_AREA:
+		var body MetaTlvBodyFlashArea
+		if err := readBody(&body); err != nil {
+			return nil, err
+		}
+		return body.Map(), nil
+
+	case META_TLV_TYPE_MMR_REF:
+		var body MetaTlvBodyMmrRef
+		if err := readBody(&body); err != nil {
+			return nil, err
+		}
+		return body.Map(), nil
+
+	default:
+		return nil, util.FmtNewtError(
+			"Unknown meta TLV type: %d", t.Header.Type)
+	}
+}
+
+func (b *MetaTlvBodyFlashArea) Map() map[string]interface{} {
+	return map[string]interface{}{
+		"area":   b.Area,
+		"device": b.Device,
+		"offset": b.Offset,
+		"size":   b.Size,
+	}
+}
+
+func (b *MetaTlvBodyHash) Map() map[string]interface{} {
+	return map[string]interface{}{
+		"hash": hex.EncodeToString(b.Hash[:]),
+	}
+}
+
+func (b *MetaTlvBodyMmrRef) Map() map[string]interface{} {
+	return map[string]interface{}{
+		"area": b.Area,
+	}
+}
+
+func (t *MetaTlv) Map(offset int) map[string]interface{} {
+	hmap := map[string]interface{}{
+		"_type_name": MetaTlvTypeName(t.Header.Type),
+		"type":       t.Header.Type,
+		"size":       t.Header.Size,
+	}
+
+	var body interface{}
+
+	bmap, err := t.bodyMap()
+	if err != nil {
+		body = hex.EncodeToString(t.Data)
+	} else {
+		body = bmap
+	}
+
+	return map[string]interface{}{
+		"_offset": offset,
+		"header":  hmap,
+		"data":    body,
+	}
+}
+
+func (f *MetaFooter) Map(offset int) map[string]interface{} {
+	return map[string]interface{}{
+		"_offset": offset,
+		"size":    f.Size,
+		"magic":   f.Magic,
+		"version": f.Version,
+	}
+}
+
+func (m *Meta) Map(endOffset int) map[string]interface{} {
+	offsets := m.Offsets()
+	startOffset := endOffset - int(m.Footer.Size)
+
+	tlvs := []map[string]interface{}{}
+	for i, t := range m.Tlvs {
+		tlv := t.Map(startOffset + offsets.Tlvs[i])
+		tlvs = append(tlvs, tlv)
+	}
+
+	ftr := m.Footer.Map(startOffset + offsets.Footer)
+
+	return map[string]interface{}{
+		"_offset":     startOffset,
+		"_end_offset": endOffset,
+		"_size":       m.Footer.Size,
+		"tlvs":        tlvs,
+		"footer":      ftr,
+	}
+}
+
+func (m *Meta) Json(offset int) (string, error) {
+	mmap := m.Map(offset)
+
+	bin, err := json.MarshalIndent(mmap, "", "    ")
+	if err != nil {
+		return "", util.ChildNewtError(err)
+	}
+
+	return string(bin), nil
+}
diff --git a/artifact/mfg/meta.go b/artifact/mfg/meta.go
new file mode 100644
index 0000000..4521a89
--- /dev/null
+++ b/artifact/mfg/meta.go
@@ -0,0 +1,352 @@
+/**
+ * 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 mfg
+
+import (
+	"bytes"
+	"encoding/binary"
+	"io"
+	"io/ioutil"
+
+	"mynewt.apache.org/newt/util"
+)
+
+// The "manufacturing meta region" is located at the end of the boot loader
+// flash area.  This region has the following structure.
+//
+//  0                   1                   2                   3
+//  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// |Version (0x01) |                  0xff padding                 |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// |   TLV type    |   TLV size    | TLV data ("TLV size" bytes)   ~
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               ~
+// ~                                                               ~
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// |   TLV type    |   TLV size    | TLV data ("TLV size" bytes)   ~
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               ~
+// ~                                                               ~
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// |   Region size                 |         0xff padding          |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// |                       Magic (0x3bb2a269)                      |
+// +-+-+-+-+-+--+-+-+-+-end of boot loader area+-+-+-+-+-+-+-+-+-+-+
+//
+// The number of TLVs is variable; two are shown above for illustrative
+// purposes.
+//
+// Fields:
+// <Header>
+// 1. Version: Manufacturing meta version number; always 0x01.
+//
+// <TLVs>
+// 2. TLV type: Indicates the type of data to follow.
+// 3. TLV size: The number of bytes of data to follow.
+// 4. TLV data: TLV-size bytes of data.
+//
+// <Footer>
+// 5. Region size: The size, in bytes, of the entire manufacturing meta region;
+//    includes header, TLVs, and footer.
+// 6. Magic: indicates the presence of the manufacturing meta region.
+
+const META_MAGIC = 0x3bb2a269
+const META_VERSION = 2
+const META_TLV_TYPE_HASH = 0x01
+const META_TLV_TYPE_FLASH_AREA = 0x02
+const META_TLV_TYPE_MMR_REF = 0x04
+
+const META_HASH_SZ = 32
+const META_FOOTER_SZ = 8
+const META_TLV_HEADER_SZ = 2
+const META_TLV_HASH_SZ = META_HASH_SZ
+const META_TLV_FLASH_AREA_SZ = 10
+const META_TLV_MMR_REF_SZ = 1
+
+type MetaFooter struct {
+	Size    uint16 // Includes header, TLVs, and footer.
+	Version uint8
+	Pad8    uint8  // 0xff
+	Magic   uint32 // META_MAGIC
+}
+
+type MetaTlvHeader struct {
+	Type uint8 // Indicates the type of data to follow.
+	Size uint8 // The number of bytes of data to follow.
+}
+
+type MetaTlvBodyFlashArea struct {
+	Area   uint8  // Unique value identifying this flash area.
+	Device uint8  // Indicates host flash device (aka section number).
+	Offset uint32 // The byte offset within the flash device.
+	Size   uint32 // Size, in bytes, of entire flash area.
+}
+
+type MetaTlvBodyHash struct {
+	Hash [META_HASH_SZ]byte
+}
+
+type MetaTlvBodyMmrRef struct {
+	Area uint8
+}
+
+type MetaTlv struct {
+	Header MetaTlvHeader
+	Data   []byte
+}
+
+type Meta struct {
+	Tlvs   []MetaTlv
+	Footer MetaFooter
+}
+
+type MetaOffsets struct {
+	Tlvs      []int
+	Footer    int
+	TotalSize int
+}
+
+var metaTlvTypeNameMap = map[uint8]string{
+	META_TLV_TYPE_HASH:       "hash",
+	META_TLV_TYPE_FLASH_AREA: "flash_area",
+	META_TLV_TYPE_MMR_REF:    "mmr_ref",
+}
+
+func MetaTlvTypeName(typ uint8) string {
+	name := metaTlvTypeNameMap[typ]
+	if name == "" {
+		name = "???"
+	}
+	return name
+}
+
+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 nil
+}
+
+func (tlv *MetaTlv) Write(w io.Writer) (int, error) {
+	sz := 0
+
+	if err := writeElem(tlv.Header, w); err != nil {
+		return sz, err
+	}
+	sz += META_TLV_HEADER_SZ
+
+	if err := writeElem(tlv.Data, w); err != nil {
+		return sz, err
+	}
+	sz += len(tlv.Data)
+
+	return sz, nil
+}
+
+func (meta *Meta) WritePlusOffsets(w io.Writer) (MetaOffsets, error) {
+	mo := MetaOffsets{}
+	sz := 0
+
+	for _, tlv := range meta.Tlvs {
+		tlvSz, err := tlv.Write(w)
+		if err != nil {
+			return mo, err
+		}
+		mo.Tlvs = append(mo.Tlvs, sz)
+		sz += tlvSz
+	}
+
+	if err := writeElem(meta.Footer, w); err != nil {
+		return mo, err
+	}
+	mo.Footer = sz
+	sz += META_FOOTER_SZ
+
+	mo.TotalSize = sz
+
+	return mo, nil
+}
+
+func (meta *Meta) Offsets() MetaOffsets {
+	mo, _ := meta.WritePlusOffsets(ioutil.Discard)
+	return mo
+}
+
+func (meta *Meta) Write(w io.Writer) (int, error) {
+	mo, err := meta.WritePlusOffsets(w)
+	if err != nil {
+		return 0, err
+	}
+
+	return mo.TotalSize, nil
+}
+
+func (meta *Meta) Size() int {
+	return meta.Offsets().TotalSize
+}
+
+func (meta *Meta) Bytes() ([]byte, error) {
+	b := &bytes.Buffer{}
+
+	_, err := meta.Write(b)
+	if err != nil {
+		return nil, err
+	}
+
+	return b.Bytes(), nil
+}
+
+func (meta *Meta) FindTlvIndices(typ uint8) []int {
+	indices := []int{}
+
+	for i, tlv := range meta.Tlvs {
+		if tlv.Header.Type == typ {
+			indices = append(indices, i)
+		}
+	}
+
+	return indices
+}
+
+func (meta *Meta) FindTlvs(typ uint8) []*MetaTlv {
+	indices := meta.FindTlvIndices(typ)
+
+	tlvs := []*MetaTlv{}
+	for _, index := range indices {
+		tlvs = append(tlvs, &meta.Tlvs[index])
+	}
+
+	return tlvs
+}
+
+func (meta *Meta) FindFirstTlv(typ uint8) *MetaTlv {
+	indices := meta.FindTlvIndices(typ)
+	if len(indices) == 0 {
+		return nil
+	}
+
+	return &meta.Tlvs[indices[0]]
+}
+
+func (meta *Meta) HashOffset() int {
+	mo := meta.Offsets()
+	indices := meta.FindTlvIndices(META_TLV_TYPE_HASH)
+	if len(indices) == 0 {
+		return -1
+	}
+
+	return META_TLV_HEADER_SZ + mo.Tlvs[indices[0]]
+}
+
+func (meta *Meta) ClearHash() {
+	tlv := meta.FindFirstTlv(META_TLV_TYPE_HASH)
+	if tlv != nil {
+		tlv.Data = make([]byte, META_HASH_SZ)
+	}
+}
+
+func (meta *Meta) Hash() []byte {
+	tlv := meta.FindFirstTlv(META_TLV_TYPE_HASH)
+	if tlv == nil {
+		return nil
+	}
+	return tlv.Data
+}
+
+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())
+	}
+
+	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())
+	}
+	if sz != len(data) {
+		return tlv, 0, util.FmtNewtError(
+			"Error reading %d bytes of TLV data: incomplete read",
+			tlv.Header.Size)
+	}
+	tlv.Data = data
+
+	return tlv, META_TLV_HEADER_SZ + int(tlv.Header.Size), nil
+}
+
+func parseMetaFooter(bin []byte) (MetaFooter, int, error) {
+	r := bytes.NewReader(bin)
+
+	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())
+	}
+
+	if ftr.Magic != META_MAGIC {
+		return ftr, 0, util.FmtNewtError(
+			"Meta footer contains invalid magic; exp:0x%08x, got:0x%08x",
+			META_MAGIC, ftr.Magic)
+	}
+
+	return ftr, META_FOOTER_SZ, nil
+}
+
+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; "+
+				"bin-size=%d ftr-size=%d", len(bin), META_FOOTER_SZ)
+	}
+
+	ftr, _, err := parseMetaFooter(bin[len(bin)-META_FOOTER_SZ:])
+	if err != nil {
+		return Meta{}, 0, err
+	}
+
+	if int(ftr.Size) > len(bin) {
+		return Meta{}, 0, util.FmtNewtError(
+			"Binary too small to accommodate meta region; "+
+				"bin-size=%d meta-size=%d", len(bin), ftr.Size)
+	}
+
+	ftrOff := len(bin) - META_FOOTER_SZ
+	off := len(bin) - int(ftr.Size)
+
+	tlvs := []MetaTlv{}
+	for off < ftrOff {
+		tlv, sz, err := parseMetaTlv(bin[off:])
+		if err != nil {
+			return Meta{}, 0, err
+		}
+		tlvs = append(tlvs, tlv)
+		off += sz
+	}
+
+	return Meta{
+		Tlvs:   tlvs,
+		Footer: ftr,
+	}, off, nil
+}
diff --git a/artifact/mfg/mfg.go b/artifact/mfg/mfg.go
new file mode 100644
index 0000000..8e999ad
--- /dev/null
+++ b/artifact/mfg/mfg.go
@@ -0,0 +1,113 @@
+package mfg
+
+import (
+	"crypto/sha256"
+
+	"mynewt.apache.org/newt/util"
+)
+
+const MFG_IMG_FILENAME = "mfgimg.bin"
+const MFG_MANIFEST_FILENAME = "manifest.json"
+
+type Mfg struct {
+	Bin  []byte
+	Meta *Meta
+
+	// Unused if Meta==nil.
+	MetaOff int
+}
+
+func Parse(data []byte, metaEndOff int, eraseVal byte) (Mfg, error) {
+	m := Mfg{
+		Bin: data,
+	}
+
+	if metaEndOff >= 0 {
+		meta, _, err := ParseMeta(data[:metaEndOff])
+		if err != nil {
+			return m, err
+		}
+		m.Meta = &meta
+		m.MetaOff = metaEndOff - int(meta.Footer.Size)
+	}
+
+	return m, 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]
+}
+
+func AddPadding(b []byte, eraseVal byte, padLen int) []byte {
+	for i := 0; i < padLen; i++ {
+		b = append(b, eraseVal)
+	}
+	return b
+}
+
+func (m *Mfg) bytesZeroedHash(eraseVal byte) ([]byte, error) {
+	binCopy := make([]byte, len(m.Bin))
+	copy(binCopy, m.Bin)
+
+	m.Meta.ClearHash()
+
+	metaBytes, err := m.Meta.Bytes()
+	if err != nil {
+		return nil, err
+	}
+
+	padLen := m.MetaOff + len(metaBytes) - len(binCopy)
+	if padLen > 0 {
+		binCopy = AddPadding(binCopy, eraseVal, padLen)
+	}
+
+	copy(binCopy[m.MetaOff:m.MetaOff+len(metaBytes)], metaBytes)
+
+	return binCopy, nil
+}
+
+// Calculates the SHA256 hash, using the full manufacturing image as input.
+// Hash-calculation algorithm is as follows:
+// 1. Zero out the 32 bytes that will contain the hash.
+// 2. Apply SHA256 to the result.
+//
+// This function assumes that the 32 bytes of hash data have already been
+// zeroed.
+func CalcHash(bin []byte) []byte {
+	hash := sha256.Sum256(bin)
+	return hash[:]
+}
+
+func (m *Mfg) Bytes(eraseVal byte) ([]byte, error) {
+	// First, write with zeroed hash.
+	bin, err := m.bytesZeroedHash(eraseVal)
+	if err != nil {
+		return nil, 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 nil, util.FmtNewtError(
+				"unexpected error: hash extends beyond end " +
+					"of manufacturing image")
+		}
+
+		copy(bin[hashOff:hashOff+META_HASH_SZ], tlv.Data)
+	}
+
+	return bin, nil
+}
diff --git a/newt/mfg/read.go b/artifact/mfg/paths.go
similarity index 64%
rename from newt/mfg/read.go
rename to artifact/mfg/paths.go
index 04520db..483aca2 100644
--- a/newt/mfg/read.go
+++ b/artifact/mfg/paths.go
@@ -20,21 +20,16 @@
 package mfg
 
 import (
-	"strings"
-
-	"mynewt.apache.org/newt/newt/builder"
+	"fmt"
+	"path/filepath"
 )
 
-// @return						mfg-image-path, error
-func (mi *MfgImage) Upload() (string, error) {
-	// For now, we always upload section 0 only.
-	section0Path := MfgSectionBinPath(mi.basePkg.Name(), 0)
-	baseName := strings.TrimSuffix(section0Path, ".bin")
-
-	envSettings := map[string]string{"MFG_IMAGE": "1"}
-	if err := builder.Load(baseName, mi.bsp, envSettings); err != nil {
-		return "", err
-	}
+const MANIFEST_FILENAME = "manifest.json"
+const BOOT_DIR = "bootloader"
+const BOOT_MANIFEST_PATH = BOOT_DIR + "/manifest.json"
+const SECTION_BIN_DIR = "sections"
 
-	return section0Path, nil
+func SectionBinPath(mfgPkgName string, sectionNum int) string {
+	return fmt.Sprintf("%s/%s-s%d.bin", SECTION_BIN_DIR,
+		filepath.Base(mfgPkgName), sectionNum)
 }
diff --git a/larva/cli/image_cmds.go b/larva/cli/image_cmds.go
index ce1093e..cdcac33 100644
--- a/larva/cli/image_cmds.go
+++ b/larva/cli/image_cmds.go
@@ -20,16 +20,26 @@
 package cli
 
 import (
-	"encoding/hex"
+	"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/larva/lvimg"
 	"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 {
@@ -41,35 +51,39 @@ func readImage(filename string) (image.Image, error) {
 }
 
 func writeImage(img image.Image, filename string) error {
+	if err := lvimg.VerifyImage(img); err != nil {
+		return err
+	}
+
 	if err := img.WriteToFile(filename); err != nil {
 		return err
 	}
 
-	log.Debugf("Wrote image %s", filename)
+	util.StatusMessage(util.VERBOSITY_DEFAULT, "Wrote image %s\n", filename)
 	return nil
 }
 
-func reportDupSigs(img image.Image) {
-	m := map[string]struct{}{}
-	dups := map[string]struct{}{}
-
-	for _, tlv := range img.Tlvs {
-		if tlv.Header.Type == image.IMAGE_TLV_KEYHASH {
-			h := hex.EncodeToString(tlv.Data)
-			if _, ok := m[h]; ok {
-				dups[h] = struct{}{}
-			} else {
-				m[h] = struct{}{}
-			}
-		}
+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)
 	}
 
-	if len(dups) > 0 {
-		fmt.Printf("Warning: duplicate signatures detected:\n")
-		for d, _ := range dups {
-			fmt.Printf("    %s\n", d)
-		}
+	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) {
@@ -89,6 +103,32 @@ func runShowCmd(cmd *cobra.Command, args []string) {
 	fmt.Printf("%s\n", s)
 }
 
+func runBriefCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 1 {
+		LarvaUsage(cmd, nil)
+	}
+
+	img, err := readImage(args[0])
+	if err != nil {
+		LarvaUsage(cmd, err)
+	}
+
+	offsets, err := img.Offsets()
+	if err != nil {
+		LarvaUsage(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 {
 		LarvaUsage(cmd, nil)
@@ -116,14 +156,106 @@ func runSignCmd(cmd *cobra.Command, args []string) {
 			"Failed to read hash from specified image: %s", err.Error()))
 	}
 
-	tlvs, err := image.GenerateSigTlvs(keys, hash)
+	tlvs, err := image.BuildSigTlvs(keys, hash)
 	if err != nil {
 		LarvaUsage(nil, err)
 	}
 
 	img.Tlvs = append(img.Tlvs, tlvs...)
 
-	reportDupSigs(img)
+	if err := writeImage(img, outFilename); err != nil {
+		LarvaUsage(nil, err)
+	}
+}
+
+func runAddTlvsCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 3 {
+		LarvaUsage(cmd, nil)
+	}
+
+	inFilename := args[0]
+	outFilename, err := CalcOutFilename(inFilename)
+	if err != nil {
+		LarvaUsage(cmd, err)
+	}
+
+	img, err := readImage(inFilename)
+	if err != nil {
+		LarvaUsage(cmd, err)
+	}
+
+	tlvArgs := args[1:]
+	if len(tlvArgs)%2 != 0 {
+		LarvaUsage(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 {
+			LarvaUsage(cmd, err)
+		}
+
+		tlvs = append(tlvs, tlv)
+	}
+
+	img.Tlvs = append(img.Tlvs, tlvs...)
+
+	if err := writeImage(img, outFilename); err != nil {
+		LarvaUsage(nil, err)
+	}
+}
+
+func runRmtlvsCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 2 {
+		LarvaUsage(cmd, nil)
+	}
+
+	inFilename := args[0]
+	outFilename, err := CalcOutFilename(inFilename)
+	if err != nil {
+		LarvaUsage(cmd, err)
+	}
+
+	img, err := readImage(inFilename)
+	if err != nil {
+		LarvaUsage(cmd, err)
+	}
+
+	tlvIndices := []int{}
+	idxMap := map[int]struct{}{}
+	for _, arg := range args[1:] {
+		idx, err := util.AtoiNoOct(arg)
+		if err != nil {
+			LarvaUsage(cmd, util.FmtNewtError("Invalid TLV index: %s", arg))
+		}
+
+		if idx < 0 || idx >= len(img.Tlvs) {
+			LarvaUsage(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 {
+			LarvaUsage(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 {
 		LarvaUsage(nil, err)
@@ -160,6 +292,113 @@ func runRmsigsCmd(cmd *cobra.Command, args []string) {
 	}
 }
 
+func runHashableCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 1 {
+		LarvaUsage(cmd, nil)
+	}
+
+	if OptOutFilename == "" {
+		LarvaUsage(cmd, util.FmtNewtError("--outfile (-o) option required"))
+	}
+
+	inFilename := args[0]
+	outFilename := OptOutFilename
+
+	img, err := readImage(inFilename)
+	if err != nil {
+		LarvaUsage(cmd, err)
+	}
+
+	f, err := os.Create(outFilename)
+	if err != nil {
+		LarvaUsage(nil, util.ChildNewtError(err))
+	}
+	defer f.Close()
+
+	if err := binary.Write(f, binary.LittleEndian, &img.Header); err != nil {
+		LarvaUsage(nil, util.FmtNewtError(
+			"Error writing image header: %s", err.Error()))
+	}
+	_, err = f.Write(img.Body)
+	if err != nil {
+		LarvaUsage(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 {
+		LarvaUsage(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)) {
+
+		LarvaUsage(cmd, util.FmtNewtError(
+			"Invalid signature type: %s", args[3]))
+	}
+
+	outFilename, err := CalcOutFilename(imgFilename)
+	if err != nil {
+		LarvaUsage(cmd, err)
+	}
+
+	img, err := readImage(imgFilename)
+	if err != nil {
+		LarvaUsage(cmd, err)
+	}
+
+	keyData, err := ioutil.ReadFile(keyFilename)
+	if err != nil {
+		LarvaUsage(cmd, util.FmtNewtError(
+			"Error reading key file: %s", err.Error()))
+	}
+
+	sigData, err := ioutil.ReadFile(sigFilename)
+	if err != nil {
+		LarvaUsage(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 = lvimg.PadEcdsa256Sig(sigData)
+		if err != nil {
+			LarvaUsage(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 {
+		LarvaUsage(nil, err)
+	}
+}
+
 func AddImageCommands(cmd *cobra.Command) {
 	imageCmd := &cobra.Command{
 		Use:   "image",
@@ -177,6 +416,13 @@ func AddImageCommands(cmd *cobra.Command) {
 	}
 	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",
@@ -190,6 +436,33 @@ func AddImageCommands(cmd *cobra.Command) {
 
 	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",
@@ -202,4 +475,28 @@ func AddImageCommands(cmd *cobra.Command) {
 		"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)
 }
diff --git a/larva/cli/mfg_cmds.go b/larva/cli/mfg_cmds.go
index ad201cc..1a90b12 100644
--- a/larva/cli/mfg_cmds.go
+++ b/larva/cli/mfg_cmds.go
@@ -29,36 +29,39 @@ import (
 
 	"mynewt.apache.org/newt/artifact/flash"
 	"mynewt.apache.org/newt/artifact/manifest"
-	"mynewt.apache.org/newt/larva/mfg"
+	"mynewt.apache.org/newt/artifact/mfg"
+	"mynewt.apache.org/newt/larva/lvmfg"
 	"mynewt.apache.org/newt/util"
 )
 
-var optDeviceNum int
-
-func readManifest(filename string) (manifest.Manifest, error) {
-	man, err := manifest.ReadManifest(filename)
+func readMfgBin(filename string) ([]byte, error) {
+	bin, err := ioutil.ReadFile(filename)
 	if err != nil {
-		return man, err
+		return nil, util.FmtNewtError(
+			"Failed to read manufacturing image: %s", err.Error())
 	}
 
-	log.Debugf("Successfully read manifest %s", filename)
-	return man, nil
+	return bin, nil
 }
 
-func readFlashAreas(manifestFilename string) ([]flash.FlashArea, error) {
-	man, err := readManifest(manifestFilename)
-	if err != nil {
-		return nil, err
-	}
+func readManifest(mfgDir string) (manifest.MfgManifest, error) {
+	return manifest.ReadMfgManifest(mfgDir + "/" + mfg.MANIFEST_FILENAME)
+}
 
-	areas := flash.SortFlashAreasByDevOff(man.FlashAreas)
+func extractFlashAreas(mman manifest.MfgManifest) ([]flash.FlashArea, error) {
+	areas := flash.SortFlashAreasByDevOff(mman.FlashAreas)
+
+	if len(areas) == 0 {
+		LarvaUsage(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 := mfg.VerifyAreas(areas, optDeviceNum); err != nil {
+	if err := lvmfg.VerifyAreas(areas); err != nil {
 		return nil, err
 	}
 
@@ -66,8 +69,10 @@ func readFlashAreas(manifestFilename string) ([]flash.FlashArea, error) {
 	return areas, nil
 }
 
-func createMfgMap(binDir string, areas []flash.FlashArea) (mfg.MfgMap, error) {
-	mm := mfg.MfgMap{}
+func createNameBlobMap(binDir string,
+	areas []flash.FlashArea) (lvmfg.NameBlobMap, error) {
+
+	mm := lvmfg.NameBlobMap{}
 
 	for _, area := range areas {
 		filename := fmt.Sprintf("%s/%s.bin", binDir, area.Name)
@@ -84,27 +89,68 @@ func createMfgMap(binDir string, areas []flash.FlashArea) (mfg.MfgMap, error) {
 	return mm, nil
 }
 
-func runSplitCmd(cmd *cobra.Command, args []string) {
-	if len(args) < 3 {
+func runMfgShowCmd(cmd *cobra.Command, args []string) {
+	if len(args) < 2 {
 		LarvaUsage(cmd, nil)
 	}
+	inFilename := args[0]
 
-	imgFilename := args[0]
-	manFilename := args[1]
-	outDir := args[2]
-
-	mfgBin, err := ioutil.ReadFile(imgFilename)
+	metaEndOff, err := util.AtoiNoOct(args[1])
 	if err != nil {
 		LarvaUsage(cmd, util.FmtNewtError(
-			"Failed to read manufacturing image: %s", err.Error()))
+			"invalid meta offset \"%s\"", args[1]))
+	}
+
+	bin, err := readMfgBin(inFilename)
+	if err != nil {
+		LarvaUsage(cmd, err)
+	}
+
+	m, err := mfg.Parse(bin, metaEndOff, 0xff)
+	if err != nil {
+		LarvaUsage(nil, err)
 	}
 
-	areas, err := readFlashAreas(manFilename)
+	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 {
+			LarvaUsage(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 {
+		LarvaUsage(cmd, nil)
+	}
+
+	mfgDir := args[0]
+	outDir := args[1]
+
+	mm, err := readManifest(mfgDir)
 	if err != nil {
 		LarvaUsage(cmd, err)
 	}
 
-	mm, err := mfg.Split(mfgBin, optDeviceNum, areas)
+	areas, err := extractFlashAreas(mm)
+	if err != nil {
+		LarvaUsage(nil, err)
+	}
+
+	binPath := fmt.Sprintf("%s/%s", mfgDir, mm.BinPath)
+	bin, err := ioutil.ReadFile(binPath)
+	if err != nil {
+		LarvaUsage(cmd, util.FmtNewtError(
+			"Failed to read \"%s\": %s", binPath, err.Error()))
+	}
+
+	nbmap, err := lvmfg.Split(bin, mm.Device, areas, 0xff)
 	if err != nil {
 		LarvaUsage(nil, err)
 	}
@@ -113,39 +159,88 @@ func runSplitCmd(cmd *cobra.Command, args []string) {
 		LarvaUsage(nil, util.ChildNewtError(err))
 	}
 
-	for name, data := range mm {
+	for name, data := range nbmap {
 		filename := fmt.Sprintf("%s/%s.bin", outDir, name)
-		if err := ioutil.WriteFile(filename, data, os.ModePerm); err != nil {
+		if err := ioutil.WriteFile(filename, data,
+			os.ModePerm); err != nil {
+
 			LarvaUsage(nil, util.ChildNewtError(err))
 		}
 	}
+
+	mfgDstDir := fmt.Sprintf("%s/mfg", outDir)
+	util.StatusMessage(util.VERBOSITY_DEFAULT,
+		"Copying source mfg directory to %s\n", mfgDstDir)
+	if err := util.CopyDir(mfgDir, mfgDstDir); err != nil {
+		LarvaUsage(nil, err)
+	}
 }
 
 func runJoinCmd(cmd *cobra.Command, args []string) {
-	if len(args) < 3 {
+	if len(args) < 2 {
 		LarvaUsage(cmd, nil)
 	}
 
-	binDir := args[0]
-	manFilename := args[1]
-	outFilename := args[2]
+	splitDir := args[0]
+	outDir := args[1]
 
-	areas, err := readFlashAreas(manFilename)
+	if util.NodeExist(outDir) {
+		LarvaUsage(nil, util.FmtNewtError(
+			"Destination \"%s\" already exists", outDir))
+	}
+
+	mm, err := readManifest(splitDir + "/mfg")
+	if err != nil {
+		LarvaUsage(cmd, err)
+	}
+	areas, err := extractFlashAreas(mm)
 	if err != nil {
 		LarvaUsage(cmd, err)
 	}
 
-	mm, err := createMfgMap(binDir, areas)
+	nbmap, err := createNameBlobMap(splitDir, areas)
+	if err != nil {
+		LarvaUsage(nil, err)
+	}
+
+	bin, err := lvmfg.Join(nbmap, 0xff, areas)
 	if err != nil {
 		LarvaUsage(nil, err)
 	}
 
-	mfgBin, err := mfg.Join(mm, 0xff, areas)
+	m, err := mfg.Parse(bin, mm.Meta.EndOffset, 0xff)
 	if err != nil {
 		LarvaUsage(nil, err)
 	}
 
-	if err := ioutil.WriteFile(outFilename, mfgBin, os.ModePerm); err != nil {
+	infos, err := ioutil.ReadDir(splitDir + "/mfg")
+	if err != nil {
+		LarvaUsage(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 = util.CopyDir(src, dst)
+			} else {
+				err = util.CopyFile(src, dst)
+			}
+			if err != nil {
+				LarvaUsage(nil, err)
+			}
+		}
+	}
+
+	finalBin, err := m.Bytes(0xff)
+	if err != nil {
+		LarvaUsage(nil, err)
+	}
+
+	if err := ioutil.WriteFile(outDir+"/"+mfg.MFG_IMG_FILENAME, finalBin,
+		os.ModePerm); err != nil {
+
 		LarvaUsage(nil, util.ChildNewtError(err))
 	}
 }
@@ -182,7 +277,7 @@ func runBootKeyCmd(cmd *cobra.Command, args []string) {
 			"Failed to read new key der: %s", err.Error()))
 	}
 
-	if err := mfg.ReplaceBootKey(sec0, okey, nkey); err != nil {
+	if err := lvmfg.ReplaceBootKey(sec0, okey, nkey); err != nil {
 		LarvaUsage(nil, err)
 	}
 
@@ -201,26 +296,28 @@ func AddMfgCommands(cmd *cobra.Command) {
 	}
 	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 <mfg-image> <manifest> <out-dir>",
+		Use:   "split <mfg-image-dir> <out-dir>",
 		Short: "Splits a Mynewt mfg section into several files",
 		Run:   runSplitCmd,
 	}
 
-	splitCmd.PersistentFlags().IntVarP(&optDeviceNum, "device", "d", 0,
-		"Flash device number")
-
 	mfgCmd.AddCommand(splitCmd)
 
 	joinCmd := &cobra.Command{
-		Use:   "join <bin-dir> <manifest> <out-mfg-image>",
+		Use:   "join <split-dir> <out-dir>",
 		Short: "Joins a split mfg section into a single file",
 		Run:   runJoinCmd,
 	}
 
-	joinCmd.PersistentFlags().IntVarP(&optDeviceNum, "device", "d", 0,
-		"Flash device number")
-
 	mfgCmd.AddCommand(joinCmd)
 
 	bootKeyCmd := &cobra.Command{
diff --git a/larva/lvimg/lvimg.go b/larva/lvimg/lvimg.go
new file mode 100644
index 0000000..60d5ef4
--- /dev/null
+++ b/larva/lvimg/lvimg.go
@@ -0,0 +1,116 @@
+/**
+ * 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 lvimg
+
+import (
+	"encoding/hex"
+	"fmt"
+	"strings"
+
+	"mynewt.apache.org/newt/artifact/image"
+	"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
+}
diff --git a/larva/mfg/mfg.go b/larva/lvmfg/lvmfg.go
similarity index 64%
rename from larva/mfg/mfg.go
rename to larva/lvmfg/lvmfg.go
index f98ab06..dbc0168 100644
--- a/larva/mfg/mfg.go
+++ b/larva/lvmfg/lvmfg.go
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-package mfg
+package lvmfg
 
 import (
 	"bytes"
@@ -26,10 +26,17 @@ import (
 	"strings"
 
 	"mynewt.apache.org/newt/artifact/flash"
+	"mynewt.apache.org/newt/artifact/mfg"
 	"mynewt.apache.org/newt/util"
 )
 
-type MfgMap map[string][]byte
+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 {
@@ -52,24 +59,27 @@ func verifyArea(area flash.FlashArea, minOffset int) error {
 }
 
 // `areas` must be sorted by device ID, then by offset.
-func VerifyAreas(areas []flash.FlashArea, deviceNum int) error {
+func VerifyAreas(areas []flash.FlashArea) error {
+	prevDevice := -1
 	off := 0
 	for _, area := range areas {
-		if area.Device == deviceNum {
-			if err := verifyArea(area, off); err != nil {
-				return err
-			}
-			off += area.Size
+		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) (MfgMap, error) {
+	areas []flash.FlashArea, eraseVal byte) (NameBlobMap, error) {
 
-	mm := MfgMap{}
+	mm := NameBlobMap{}
 
 	for _, area := range areas {
 		if _, ok := mm[area.Name]; ok {
@@ -77,16 +87,18 @@ func Split(mfgBin []byte, deviceNum int,
 				"two or more flash areas with same name: \"%s\"", area.Name)
 		}
 
-		if area.Device == deviceNum && area.Offset < len(mfgBin) {
-			end := area.Offset + area.Size
-			if end > len(mfgBin) {
-				return nil, util.FmtNewtError(
-					"area \"%s\" (offset=%d size=%d) "+
-						"extends beyond end of manufacturing image",
-					area.Name, area.Offset, area.Size)
+		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] = mfgBin[area.Offset:end]
+			mm[area.Name] = StripPadding(areaBin, eraseVal)
 		}
 	}
 
@@ -94,19 +106,8 @@ func Split(mfgBin []byte, deviceNum int,
 }
 
 // `areas` must be sorted by device ID, then by offset.
-func Join(mm MfgMap, eraseVal byte, areas []flash.FlashArea) ([]byte, error) {
-	// Ensure all areas in the mfg map belong to the same flash device.
-	device := -1
-	for _, area := range areas {
-		if _, ok := mm[area.Name]; ok {
-			if device == -1 {
-				device = area.Device
-			} else if device != area.Device {
-				return nil, util.FmtNewtError(
-					"multiple flash devices: %d != %d", device, area.Device)
-			}
-		}
-	}
+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{}{}
@@ -115,24 +116,33 @@ func Join(mm MfgMap, eraseVal byte, areas []flash.FlashArea) ([]byte, error) {
 	}
 
 	joined := []byte{}
-
-	off := 0
 	for _, area := range areas {
 		bin := mm[area.Name]
-		if bin == nil {
-			break
-		}
-		delete(unseen, area.Name)
 
-		padSize := area.Offset - off
-		for i := 0; i < padSize; i++ {
-			joined = append(joined, 0xff)
-		}
+		// Only include this area if it belongs to the mfg image we are
+		// joining.
+		if bin != nil {
+			delete(unseen, area.Name)
 
-		joined = append(joined, bin...)
+			// 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 (%x) at offset %d (0x%x)\n",
+				area.Name, binstr, len(joined), len(joined))
+			joined = append(joined, bin...)
+		}
 	}
 
-	// Ensure we processed every area in the mfg map.
+	// Ensure we processed every area in the map.
 	if len(unseen) > 0 {
 		names := []string{}
 		for name, _ := range unseen {
@@ -144,6 +154,9 @@ func Join(mm MfgMap, eraseVal byte, areas []flash.FlashArea) ([]byte, error) {
 			"unprocessed flash areas: %s", strings.Join(names, ", "))
 	}
 
+	// Strip padding from the end of the joined bianry.
+	joined = StripPadding(joined, eraseVal)
+
 	return joined, nil
 }
 
@@ -176,3 +189,15 @@ func ReplaceBootKey(sec0 []byte, okey []byte, nkey []byte) error {
 
 	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/newt/builder/targetbuild.go b/newt/builder/targetbuild.go
index 23c361a..94fe8f9 100644
--- a/newt/builder/targetbuild.go
+++ b/newt/builder/targetbuild.go
@@ -28,6 +28,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+	"path/filepath"
 	"strings"
 
 	log "github.com/Sirupsen/logrus"
diff --git a/newt/cli/build_cmds.go b/newt/cli/build_cmds.go
index d986510..d7cb8fc 100644
--- a/newt/cli/build_cmds.go
+++ b/newt/cli/build_cmds.go
@@ -27,6 +27,8 @@ import (
 
 	"github.com/spf13/cobra"
 	"mynewt.apache.org/newt/newt/builder"
+	"mynewt.apache.org/newt/newt/imgprod"
+	"mynewt.apache.org/newt/newt/manifest"
 	"mynewt.apache.org/newt/newt/pkg"
 	"mynewt.apache.org/newt/newt/project"
 	"mynewt.apache.org/newt/newt/target"
@@ -160,6 +162,15 @@ func buildRunCmd(cmd *cobra.Command, args []string, printShellCmds bool, execute
 			NewtUsage(nil, err)
 		}
 
+		// Produce bare "imageless" manifest.
+		mopts, err := manifest.OptsForNonImage(b)
+		if err != nil {
+			NewtUsage(nil, err)
+		}
+		if err := imgprod.ProduceManifest(mopts); err != nil {
+			NewtUsage(nil, err)
+		}
+
 		util.StatusMessage(util.VERBOSITY_DEFAULT,
 			"Target successfully built: %s\n", t.Name())
 	}
diff --git a/newt/cli/mfg_cmds.go b/newt/cli/mfg_cmds.go
index 513e2c4..78d5eb9 100644
--- a/newt/cli/mfg_cmds.go
+++ b/newt/cli/mfg_cmds.go
@@ -20,6 +20,8 @@
 package cli
 
 import (
+	"fmt"
+
 	"github.com/spf13/cobra"
 
 	"mynewt.apache.org/newt/artifact/image"
@@ -50,31 +52,34 @@ func ResolveMfgPkg(pkgName string) (*pkg.LocalPackage, error) {
 	return lpkg, nil
 }
 
-func mfgCreate(mi *mfg.MfgImage) {
-	pathStr := ""
-	for _, path := range mi.FromPaths() {
-		pathStr += "    * " + path + "\n"
+func mfgCreate(me mfg.MfgEmitter) {
+	srcPaths, dstPaths, err := me.Emit()
+	if err != nil {
+		NewtUsage(nil, err)
 	}
 
-	util.StatusMessage(util.VERBOSITY_DEFAULT,
-		"Creating a manufacturing image from the following files:\n%s\n",
-		pathStr)
+	srcStr := ""
+	dstStr := ""
 
-	outputPaths, err := mi.CreateMfgImage()
-	if err != nil {
-		NewtUsage(nil, err)
+	for _, p := range srcPaths {
+		srcStr += fmt.Sprintf("    %s\n", p)
 	}
 
-	pathStr = ""
-	for _, path := range outputPaths {
-		pathStr += "    * " + path + "\n"
+	for _, p := range dstPaths {
+		dstStr += fmt.Sprintf("    %s\n", p)
 	}
+
 	util.StatusMessage(util.VERBOSITY_DEFAULT,
-		"Generated the following files:\n%s", pathStr)
+		"Creating a manufacturing image from the following files:\n%s\n",
+		srcStr)
+
+	util.StatusMessage(util.VERBOSITY_DEFAULT,
+		"Generated the following files:\n%s\n",
+		dstStr)
 }
 
-func mfgLoad(mi *mfg.MfgImage) {
-	binPath, err := mi.Upload()
+func mfgLoad(basePkg *pkg.LocalPackage) {
+	binPath, err := mfg.Upload(basePkg)
 	if err != nil {
 		NewtUsage(nil, err)
 	}
@@ -101,13 +106,12 @@ func mfgCreateRunCmd(cmd *cobra.Command, args []string) {
 		NewtUsage(cmd, err)
 	}
 
-	mi, err := mfg.Load(lpkg)
+	me, err := mfg.LoadMfgEmitter(lpkg, ver)
 	if err != nil {
 		NewtUsage(nil, err)
 	}
 
-	mi.SetVersion(ver)
-	mfgCreate(mi)
+	mfgCreate(me)
 }
 
 func mfgLoadRunCmd(cmd *cobra.Command, args []string) {
@@ -121,12 +125,7 @@ func mfgLoadRunCmd(cmd *cobra.Command, args []string) {
 		NewtUsage(cmd, err)
 	}
 
-	mi, err := mfg.Load(lpkg)
-	if err != nil {
-		NewtUsage(nil, err)
-	}
-
-	mfgLoad(mi)
+	mfgLoad(lpkg)
 }
 
 func mfgDeployRunCmd(cmd *cobra.Command, args []string) {
@@ -149,15 +148,14 @@ func mfgDeployRunCmd(cmd *cobra.Command, args []string) {
 		}
 	}
 
-	mi, err := mfg.Load(lpkg)
+	me, err := mfg.LoadMfgEmitter(lpkg, ver)
 	if err != nil {
 		NewtUsage(nil, err)
 	}
 
-	mi.SetVersion(ver)
-	mfgCreate(mi)
+	mfgCreate(me)
 
-	mfgLoad(mi)
+	mfgLoad(lpkg)
 }
 
 func AddMfgCommands(cmd *cobra.Command) {
diff --git a/newt/flashmap/flashmap.go b/newt/flashmap/flashmap.go
index 68fa699..d044f42 100644
--- a/newt/flashmap/flashmap.go
+++ b/newt/flashmap/flashmap.go
@@ -26,7 +26,6 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
-	"sort"
 	"strings"
 
 	log "github.com/Sirupsen/logrus"
@@ -172,22 +171,6 @@ func (flashMap FlashMap) SortedAreas() []flash.FlashArea {
 	return flash.SortFlashAreasById(areas)
 }
 
-func (flashMap FlashMap) DeviceIds() []int {
-	deviceMap := map[int]struct{}{}
-
-	for _, area := range flashMap.Areas {
-		deviceMap[area.Device] = struct{}{}
-	}
-
-	devices := make([]int, 0, len(deviceMap))
-	for device, _ := range deviceMap {
-		devices = append(devices, device)
-	}
-	sort.Ints(devices)
-
-	return devices
-}
-
 func areasDistinct(a flash.FlashArea, b flash.FlashArea) bool {
 	var lo flash.FlashArea
 	var hi flash.FlashArea
diff --git a/newt/imgprod/imgprod.go b/newt/imgprod/imgprod.go
index a858c49..dc72eca 100644
--- a/newt/imgprod/imgprod.go
+++ b/newt/imgprod/imgprod.go
@@ -262,16 +262,14 @@ func ProduceAll(t *builder.TargetBuilder, ver image.ImageVersion,
 		return err
 	}
 
-	mopts := manifest.ManifestCreateOpts{
-		TgtBldr:    t,
-		AppHash:    pset.App.Hash,
-		Version:    ver,
-		BuildID:    fmt.Sprintf("%x", pset.App.Hash),
-		FlashAreas: t.BspPkg().FlashMap.SortedAreas(),
+	var loaderHash []byte
+	if pset.Loader != nil {
+		loaderHash = pset.Loader.Hash
 	}
 
-	if pset.Loader != nil {
-		mopts.LoaderHash = pset.Loader.Hash
+	mopts, err := manifest.OptsForImage(t, ver, pset.App.Hash, loaderHash)
+	if err != nil {
+		return err
 	}
 
 	if err := ProduceManifest(mopts); err != nil {
diff --git a/newt/imgprod/v1.go b/newt/imgprod/v1.go
index 37067d4..05f0a33 100644
--- a/newt/imgprod/v1.go
+++ b/newt/imgprod/v1.go
@@ -195,11 +195,10 @@ func ProduceAllV1(t *builder.TargetBuilder, ver image.ImageVersion,
 	}
 
 	mopts := manifest.ManifestCreateOpts{
-		TgtBldr:    t,
-		AppHash:    pset.App.Hash,
-		Version:    ver,
-		BuildID:    fmt.Sprintf("%x", pset.App.Hash),
-		FlashAreas: t.BspPkg().FlashMap.SortedAreas(),
+		TgtBldr: t,
+		AppHash: pset.App.Hash,
+		Version: ver,
+		BuildID: fmt.Sprintf("%x", pset.App.Hash),
 	}
 
 	if pset.Loader != nil {
diff --git a/newt/manifest/manifest.go b/newt/manifest/manifest.go
index 7c9eaff..c2b5243 100644
--- a/newt/manifest/manifest.go
+++ b/newt/manifest/manifest.go
@@ -30,7 +30,6 @@ import (
 
 	log "github.com/Sirupsen/logrus"
 
-	"mynewt.apache.org/newt/artifact/flash"
 	"mynewt.apache.org/newt/artifact/image"
 	"mynewt.apache.org/newt/artifact/manifest"
 	"mynewt.apache.org/newt/newt/builder"
@@ -49,7 +48,7 @@ type ManifestCreateOpts struct {
 	AppHash    []byte
 	Version    image.ImageVersion
 	BuildID    string
-	FlashAreas []flash.FlashArea
+	Syscfg     map[string]string
 }
 
 type RepoManager struct {
@@ -245,17 +244,47 @@ func ManifestPkgSizes(b *builder.Builder) (ManifestSizeCollector, error) {
 	return msc, nil
 }
 
+func OptsForNonImage(t *builder.TargetBuilder) (ManifestCreateOpts, error) {
+	res, err := t.Resolve()
+	if err != nil {
+		return ManifestCreateOpts{}, err
+	}
+
+	return ManifestCreateOpts{
+		TgtBldr: t,
+		Syscfg:  res.Cfg.SettingValues(),
+	}, nil
+}
+
+func OptsForImage(t *builder.TargetBuilder, ver image.ImageVersion,
+	appHash []byte, loaderHash []byte) (ManifestCreateOpts, error) {
+
+	res, err := t.Resolve()
+	if err != nil {
+		return ManifestCreateOpts{}, err
+	}
+
+	return ManifestCreateOpts{
+		TgtBldr:    t,
+		AppHash:    appHash,
+		LoaderHash: loaderHash,
+		Version:    ver,
+		BuildID:    fmt.Sprintf("%x", appHash),
+		Syscfg:     res.Cfg.SettingValues(),
+	}, nil
+}
+
 func CreateManifest(opts ManifestCreateOpts) (manifest.Manifest, error) {
 	t := opts.TgtBldr
 
 	m := manifest.Manifest{
-		Name:       t.GetTarget().FullName(),
-		Date:       time.Now().Format(time.RFC3339),
-		Version:    opts.Version.String(),
-		BuildID:    opts.BuildID,
-		Image:      t.AppBuilder.AppImgPath(),
-		ImageHash:  fmt.Sprintf("%x", opts.AppHash),
-		FlashAreas: opts.FlashAreas,
+		Name:      t.GetTarget().FullName(),
+		Date:      time.Now().Format(time.RFC3339),
+		Version:   opts.Version.String(),
+		BuildID:   opts.BuildID,
+		Image:     t.AppBuilder.AppImgPath(),
+		ImageHash: fmt.Sprintf("%x", opts.AppHash),
+		Syscfg:    opts.Syscfg,
 	}
 
 	rm := NewRepoManager()
diff --git a/newt/mfg/build.go b/newt/mfg/build.go
new file mode 100644
index 0000000..a3f7291
--- /dev/null
+++ b/newt/mfg/build.go
@@ -0,0 +1,564 @@
+/**
+ * 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 mfg
+
+import (
+	"bytes"
+	"encoding/binary"
+	"fmt"
+	"io/ioutil"
+	"path/filepath"
+	"sort"
+	"strings"
+
+	"mynewt.apache.org/newt/artifact/flash"
+	"mynewt.apache.org/newt/artifact/image"
+	"mynewt.apache.org/newt/artifact/manifest"
+	"mynewt.apache.org/newt/artifact/mfg"
+	"mynewt.apache.org/newt/newt/builder"
+	"mynewt.apache.org/newt/newt/flashmap"
+	"mynewt.apache.org/newt/newt/parse"
+	"mynewt.apache.org/newt/newt/pkg"
+	"mynewt.apache.org/newt/newt/project"
+	"mynewt.apache.org/newt/newt/target"
+	"mynewt.apache.org/newt/util"
+)
+
+type MfgBuildTarget struct {
+	Target  *target.Target
+	Area    flash.FlashArea
+	Offset  int
+	IsBoot  bool
+	BinPath string
+}
+
+type MfgBuildRaw struct {
+	Filename string
+	Offset   int
+	Area     flash.FlashArea
+}
+
+type MfgBuildMetaMmr struct {
+	Area flash.FlashArea
+}
+
+type MfgBuildMeta struct {
+	Area     flash.FlashArea
+	Hash     bool
+	FlashMap bool
+	Mmrs     []MfgBuildMetaMmr
+}
+
+// Can be used to construct an Mfg object.
+type MfgBuilder struct {
+	BasePkg *pkg.LocalPackage
+	Bsp     *pkg.BspPackage
+	Targets []MfgBuildTarget
+	Raws    []MfgBuildRaw
+	Meta    *MfgBuildMeta
+}
+
+// Searches the provided flash map for the named area.
+func lookUpArea(fm flashmap.FlashMap, name string) (flash.FlashArea, error) {
+	area, ok := fm.Areas[name]
+	if !ok {
+		return flash.FlashArea{}, util.FmtNewtError(
+			"reference to undefined flash area \"%s\"", name)
+	}
+
+	return area, nil
+}
+
+// Searches the project for the target corresponding to the specified decoded
+// entry (read from `mfg.yml`).
+func lookUpTarget(dt DecodedTarget) (*target.Target, error) {
+	t := target.GetTargets()[dt.Name]
+	if t == nil {
+		return nil, util.FmtNewtError(
+			"target entry references undefined target \"%s\"", dt.Name)
+	}
+
+	return t, nil
+}
+
+func normalizeOffset(offset int, length int,
+	area flash.FlashArea) (int, error) {
+
+	areaEnd := area.Offset + area.Size
+	if offset == OFFSET_END {
+		if length > area.Size {
+			return 0, util.FmtNewtError(
+				"segment is too large to fit in flash area \"%s\"; "+
+					"segment=%d, area=%d", area.Name, length, area.Size)
+		}
+		return areaEnd - length, nil
+	}
+
+	if offset+length > area.Size {
+		return 0, util.FmtNewtError(
+			"segment extends beyond end of flash area \"%s\"; "+
+				"offset=%d len=%d area_len=%d",
+			area.Name, offset, length, area.Size)
+	}
+
+	return area.Offset + offset, nil
+}
+
+func calcBsp(dm DecodedMfg,
+	basePkg *pkg.LocalPackage) (*pkg.BspPackage, error) {
+
+	var bspLpkg *pkg.LocalPackage
+	bspMap := map[*pkg.LocalPackage]struct{}{}
+	for _, dt := range dm.Targets {
+		t, err := lookUpTarget(dt)
+		if err != nil {
+			return nil, err
+		}
+
+		bspLpkg = t.Bsp()
+		bspMap[bspLpkg] = struct{}{}
+	}
+
+	if dm.Bsp != "" {
+		var err error
+		bspLpkg, err = project.GetProject().ResolvePackage(
+			basePkg.Repo(), dm.Bsp)
+		if err != nil {
+			return nil, util.FmtNewtError(
+				"failed to resolve BSP package: %s", err.Error())
+		}
+		bspMap[bspLpkg] = struct{}{}
+	}
+
+	if len(bspMap) == 0 {
+		return nil, util.FmtNewtError("at least one target required")
+	}
+
+	if len(bspMap) > 1 {
+		return nil, util.FmtNewtError("multiple BSPs detected")
+	}
+
+	bsp, err := pkg.NewBspPackage(bspLpkg)
+	if err != nil {
+		return nil, util.FmtNewtError(err.Error())
+	}
+
+	return bsp, nil
+}
+
+func (raw *MfgBuildRaw) ToPart(entryIdx int) (Part, error) {
+	data, err := ioutil.ReadFile(raw.Filename)
+	if err != nil {
+		return Part{}, util.ChildNewtError(err)
+	}
+
+	off, err := normalizeOffset(raw.Offset, len(data), raw.Area)
+	if err != nil {
+		return Part{}, err
+	}
+
+	return Part{
+		Name:   fmt.Sprintf("raw-%d (%s)", entryIdx, raw.Filename),
+		Offset: off,
+		Data:   data,
+	}, nil
+}
+
+func (mt *MfgBuildTarget) ToPart() (Part, error) {
+	data, err := ioutil.ReadFile(mt.BinPath)
+	if err != nil {
+		return Part{}, util.ChildNewtError(err)
+	}
+
+	off, err := normalizeOffset(mt.Offset, len(data), mt.Area)
+	if err != nil {
+		return Part{}, err
+	}
+
+	return Part{
+		Name:   fmt.Sprintf("%s (%s)", mt.Area.Name, filepath.Base(mt.BinPath)),
+		Offset: off,
+		Data:   data,
+	}, nil
+}
+
+func newMfgBuildTarget(dt DecodedTarget,
+	fm flashmap.FlashMap) (MfgBuildTarget, error) {
+
+	t, err := lookUpTarget(dt)
+	if err != nil {
+		return MfgBuildTarget{}, err
+	}
+
+	area, err := lookUpArea(fm, dt.Area)
+	if err != nil {
+		return MfgBuildTarget{}, err
+	}
+
+	mpath := builder.ManifestPath(dt.Name, builder.BUILD_NAME_APP,
+		t.App().Name())
+	man, err := manifest.ReadManifest(mpath)
+	if err != nil {
+		return MfgBuildTarget{}, util.FmtNewtError("%s", err.Error())
+	}
+
+	isBoot := parse.ValueIsTrue(man.Syscfg["BOOT_LOADER"])
+
+	return MfgBuildTarget{
+		Target:  t,
+		Area:    area,
+		Offset:  dt.Offset,
+		IsBoot:  isBoot,
+		BinPath: targetSrcBinPath(t, isBoot),
+	}, nil
+}
+
+func newMfgBuildRaw(dr DecodedRaw,
+	fm flashmap.FlashMap, basePath string) (MfgBuildRaw, error) {
+
+	filename := dr.Filename
+	if !strings.HasPrefix(filename, "/") {
+		filename = basePath + "/" + filename
+	}
+
+	area, err := lookUpArea(fm, dr.Area)
+	if err != nil {
+		return MfgBuildRaw{}, err
+	}
+
+	return MfgBuildRaw{
+		Filename: filename,
+		Offset:   dr.Offset,
+		Area:     area,
+	}, nil
+}
+
+func newMfgBuildMeta(dm DecodedMeta,
+	fm flashmap.FlashMap) (MfgBuildMeta, error) {
+
+	area, ok := fm.Areas[dm.Area]
+	if !ok {
+		return MfgBuildMeta{}, util.FmtNewtError(
+			"meta region specifies unrecognized flash area: \"%s\"", dm.Area)
+	}
+
+	var mmrs []MfgBuildMetaMmr
+	for _, dmmr := range dm.Mmrs {
+		area, err := lookUpArea(fm, dmmr.Area)
+		if err != nil {
+			return MfgBuildMeta{}, err
+		}
+		mmr := MfgBuildMetaMmr{
+			Area: area,
+		}
+		mmrs = append(mmrs, mmr)
+	}
+
+	return MfgBuildMeta{
+		Area:     area,
+		Hash:     dm.Hash,
+		FlashMap: dm.FlashMap,
+		Mmrs:     mmrs,
+	}, nil
+}
+
+func (mb *MfgBuilder) parts() ([]Part, error) {
+	parts := []Part{}
+
+	// Create parts from the raw entries.
+	for i, raw := range mb.Raws {
+		part, err := raw.ToPart(i)
+		if err != nil {
+			return nil, err
+		}
+		parts = append(parts, part)
+	}
+
+	// Create parts from the target entries.
+	for _, t := range mb.Targets {
+		part, err := t.ToPart()
+		if err != nil {
+			return nil, err
+		}
+		parts = append(parts, part)
+	}
+
+	// Sort by offset.
+	return SortParts(parts), nil
+}
+
+func (mb *MfgBuilder) detectOverlaps() error {
+	type overlap struct {
+		p1 Part
+		p2 Part
+	}
+
+	overlaps := []overlap{}
+
+	parts, err := mb.parts()
+	if err != nil {
+		return err
+	}
+
+	for i, p1 := range parts[:len(parts)-1] {
+		p1end := p1.Offset + len(p1.Data)
+		for _, p2 := range parts[i+1:] {
+			// Parts are sorted by offset, so only one comparison is
+			// necessary to detect overlap.
+			if p2.Offset < p1end {
+				overlaps = append(overlaps, overlap{
+					p1: p1,
+					p2: p2,
+				})
+			}
+		}
+	}
+
+	if len(overlaps) > 0 {
+		str := "flash overlaps detected:"
+		for _, overlap := range overlaps {
+
+			p1end := overlap.p1.Offset + len(overlap.p1.Data)
+			p2end := overlap.p2.Offset + len(overlap.p2.Data)
+			str += fmt.Sprintf("\n    * [%s] (%d - %d) <=> [%s] (%d - %d)",
+				overlap.p1.Name, overlap.p1.Offset, p1end,
+				overlap.p2.Name, overlap.p2.Offset, p2end)
+		}
+
+		return util.NewNewtError(str)
+	}
+
+	return nil
+}
+
+// Determines which flash device the manufacturing image is intended for.  It
+// is an error if the mfg definition specifies 0 or >1 devices.
+func (mb *MfgBuilder) calcDevice() (int, error) {
+	deviceMap := map[int]struct{}{}
+	for _, t := range mb.Targets {
+		deviceMap[t.Area.Device] = struct{}{}
+	}
+	for _, r := range mb.Raws {
+		deviceMap[r.Area.Device] = struct{}{}
+	}
+
+	devices := make([]int, 0, len(deviceMap))
+	for d, _ := range deviceMap {
+		devices = append(devices, d)
+	}
+	sort.Ints(devices)
+
+	if len(devices) == 0 {
+		return 0, util.FmtNewtError(
+			"manufacturing image definition does not indicate flash device")
+	}
+
+	if len(devices) > 1 {
+		return 0, util.FmtNewtError(
+			"multiple flash devices in use by single manufacturing image: %v",
+			devices)
+	}
+
+	return devices[0], nil
+}
+
+func newMfgBuilder(basePkg *pkg.LocalPackage, dm DecodedMfg,
+	ver image.ImageVersion) (MfgBuilder, error) {
+
+	mb := MfgBuilder{
+		BasePkg: basePkg,
+	}
+
+	bsp, err := calcBsp(dm, basePkg)
+	if err != nil {
+		return mb, err
+	}
+	mb.Bsp = bsp
+
+	for _, dt := range dm.Targets {
+		mbt, err := newMfgBuildTarget(dt, bsp.FlashMap)
+		if err != nil {
+			return mb, err
+		}
+		mb.Targets = append(mb.Targets, mbt)
+	}
+
+	for _, dr := range dm.Raws {
+		mbr, err := newMfgBuildRaw(dr, bsp.FlashMap, basePkg.BasePath())
+		if err != nil {
+			return mb, err
+		}
+		mb.Raws = append(mb.Raws, mbr)
+	}
+
+	if dm.Meta != nil {
+		meta, err := newMfgBuildMeta(*dm.Meta, mb.Bsp.FlashMap)
+		if err != nil {
+			return mb, err
+		}
+		mb.Meta = &meta
+	}
+
+	if _, err := mb.calcDevice(); err != nil {
+		return mb, err
+	}
+
+	if err := mb.detectOverlaps(); err != nil {
+		return mb, err
+	}
+
+	return mb, nil
+}
+
+// Creates a zeroed-out hash MMR TLV.  The hash's original value must be zero
+// for the actual hash to be calculated later.  After the actual value is
+// calculated, it replaces the zeros in the TLV.
+func newZeroHashTlv() mfg.MetaTlv {
+	return mfg.MetaTlv{
+		Header: mfg.MetaTlvHeader{
+			Type: mfg.META_TLV_TYPE_HASH,
+			Size: mfg.META_TLV_HASH_SZ,
+		},
+		Data: make([]byte, mfg.META_HASH_SZ),
+	}
+}
+
+// Creates a flash area MMR TLV.
+func newFlashAreaTlv(area flash.FlashArea) (mfg.MetaTlv, error) {
+	tlv := mfg.MetaTlv{
+		Header: mfg.MetaTlvHeader{
+			Type: mfg.META_TLV_TYPE_FLASH_AREA,
+			Size: mfg.META_TLV_FLASH_AREA_SZ,
+		},
+	}
+
+	body := mfg.MetaTlvBodyFlashArea{
+		Area:   uint8(area.Id),
+		Device: uint8(area.Device),
+		Offset: uint32(area.Offset),
+		Size:   uint32(area.Size),
+	}
+
+	b := &bytes.Buffer{}
+	if err := binary.Write(b, binary.LittleEndian, body); err != nil {
+		return tlv, util.ChildNewtError(err)
+	}
+
+	tlv.Data = b.Bytes()
+
+	return tlv, nil
+}
+
+// Creates an MMR ref TLV.
+func newMmrRefTlv(area flash.FlashArea) (mfg.MetaTlv, error) {
+	tlv := mfg.MetaTlv{
+		Header: mfg.MetaTlvHeader{
+			Type: mfg.META_TLV_TYPE_MMR_REF,
+			Size: mfg.META_TLV_MMR_REF_SZ,
+		},
+	}
+
+	body := mfg.MetaTlvBodyMmrRef{
+		Area: uint8(area.Id),
+	}
+
+	b := &bytes.Buffer{}
+	if err := binary.Write(b, binary.LittleEndian, body); err != nil {
+		return tlv, util.ChildNewtError(err)
+	}
+
+	tlv.Data = b.Bytes()
+
+	return tlv, nil
+}
+
+// Builds a manufacturing meta region.
+func (mb *MfgBuilder) buildMeta() (mfg.Meta, error) {
+	meta := mfg.Meta{
+		Footer: mfg.MetaFooter{
+			Size:    0, // Filled in later.
+			Version: mfg.META_VERSION,
+			Pad8:    0xff,
+			Magic:   mfg.META_MAGIC,
+		},
+	}
+
+	// Hash TLV.
+	if mb.Meta.Hash {
+		meta.Tlvs = append(meta.Tlvs, newZeroHashTlv())
+	}
+
+	// Flash map TLVs.
+	if mb.Meta.FlashMap {
+		for _, area := range mb.Bsp.FlashMap.SortedAreas() {
+			tlv, err := newFlashAreaTlv(area)
+			if err != nil {
+				return meta, err
+			}
+
+			meta.Tlvs = append(meta.Tlvs, tlv)
+		}
+	}
+
+	// MMR ref TLVs.
+	for _, mmr := range mb.Meta.Mmrs {
+		tlv, err := newMmrRefTlv(mmr.Area)
+		if err != nil {
+			return meta, err
+		}
+
+		meta.Tlvs = append(meta.Tlvs, tlv)
+	}
+
+	// Fill in region size in footer now that we know the value.
+	meta.Footer.Size = uint16(meta.Size())
+
+	return meta, nil
+}
+
+// Builds a manufacturing image.
+func (mb *MfgBuilder) Build() (mfg.Mfg, error) {
+	parts, err := mb.parts()
+	if err != nil {
+		return mfg.Mfg{}, err
+	}
+
+	bin, err := PartsBytes(parts)
+	if err != nil {
+		return mfg.Mfg{}, err
+	}
+
+	var metaOff int
+	var metap *mfg.Meta
+	if mb.Meta != nil {
+		meta, err := mb.buildMeta()
+		if err != nil {
+			return mfg.Mfg{}, err
+		}
+		metap = &meta
+		metaOff = mb.Meta.Area.Offset + mb.Meta.Area.Size - meta.Size()
+	}
+
+	return mfg.Mfg{
+		Bin:     bin,
+		Meta:    metap,
+		MetaOff: metaOff,
+	}, nil
+}
diff --git a/newt/mfg/create.go b/newt/mfg/create.go
deleted file mode 100644
index 4930329..0000000
--- a/newt/mfg/create.go
+++ /dev/null
@@ -1,535 +0,0 @@
-/**
- * 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 mfg
-
-import (
-	"encoding/json"
-	"fmt"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"sort"
-	"time"
-
-	"mynewt.apache.org/newt/artifact/flash"
-	"mynewt.apache.org/newt/newt/builder"
-	"mynewt.apache.org/newt/newt/pkg"
-	"mynewt.apache.org/newt/newt/target"
-	"mynewt.apache.org/newt/util"
-)
-
-type mfgManifest struct {
-	BuildTime   string `json:"build_time"`
-	MfgHash     string `json:"mfg_hash"`
-	Version     string `json:"version"`
-	MetaSection int    `json:"meta_section"`
-	MetaOffset  int    `json:"meta_offset"`
-}
-
-type mfgSection struct {
-	offset int
-	blob   []byte
-}
-
-type createState struct {
-	// {0:[section0], 1:[section1], ...}
-	dsMap      map[int]mfgSection
-	metaOffset int
-	hashOffset int
-	hash       []byte
-}
-
-func insertPartIntoBlob(section mfgSection, part mfgPart) {
-	partEnd := part.offset + len(part.data)
-
-	if len(section.blob) < partEnd {
-		panic("internal error; mfg blob too small")
-	}
-
-	copy(section.blob[part.offset:partEnd], part.data)
-}
-
-func (mi *MfgImage) partFromImage(
-	imgPath string, flashAreaName string) (mfgPart, error) {
-
-	part := mfgPart{
-		// Boot loader and images always go in device 0.
-		device: 0,
-	}
-
-	area, ok := mi.bsp.FlashMap.Areas[flashAreaName]
-	if !ok {
-		return part, util.FmtNewtError(
-			"Image at \"%s\" requires undefined flash area \"%s\"",
-			imgPath, flashAreaName)
-	}
-
-	part.name = fmt.Sprintf("%s (%s)", flashAreaName, filepath.Base(imgPath))
-	part.offset = area.Offset
-
-	var err error
-
-	part.data, err = ioutil.ReadFile(imgPath)
-	if err != nil {
-		return part, util.ChildNewtError(err)
-	}
-
-	overflow := len(part.data) - area.Size
-	if overflow > 0 {
-		return part, util.FmtNewtError(
-			"Image \"%s\" is too large to fit in flash area \"%s\"; "+
-				"image-size=%d flash-area-size=%d overflow=%d",
-			imgPath, flashAreaName, len(part.data), area.Size, overflow)
-	}
-
-	// If an image slot is used, the entire flash area is unwritable.  This
-	// restriction comes from the boot loader's need to write status at the end
-	// of an area.  Pad out part with unwriten flash (0xff).  This probably
-	// isn't terribly efficient...
-	for i := 0; i < -overflow; i++ {
-		part.data = append(part.data, 0xff)
-	}
-
-	return part, nil
-}
-
-func partFromRawEntry(entry MfgRawEntry, entryIdx int) mfgPart {
-	return mfgPart{
-		name:   fmt.Sprintf("entry-%d (%s)", entryIdx, entry.filename),
-		offset: entry.offset,
-		data:   entry.data,
-	}
-}
-
-func (mi *MfgImage) targetParts() ([]mfgPart, error) {
-	parts := []mfgPart{}
-
-	bootPath := mi.dstBootBinPath()
-	if bootPath != "" {
-		bootPart, err := mi.partFromImage(
-			bootPath, flash.FLASH_AREA_NAME_BOOTLOADER)
-		if err != nil {
-			return nil, err
-		}
-
-		parts = append(parts, bootPart)
-	}
-
-	for i := 0; i < 2; i++ {
-		imgPath := mi.dstImgPath(i)
-		if imgPath != "" {
-			areaName, err := areaNameFromImgIdx(i)
-			if err != nil {
-				return nil, err
-			}
-
-			part, err := mi.partFromImage(imgPath, areaName)
-			if err != nil {
-				return nil, err
-			}
-			parts = append(parts, part)
-		}
-	}
-
-	return parts, nil
-}
-
-func sectionSize(parts []mfgPart) (int, int) {
-	greatest := 0
-	lowest := 0
-	if len(parts) > 0 {
-		lowest = parts[0].offset
-	}
-	for _, part := range parts {
-		lowest = util.IntMin(lowest, part.offset)
-	}
-	for _, part := range parts {
-		end := part.offset + len(part.data)
-		greatest = util.IntMax(greatest, end)
-	}
-
-	return lowest, greatest
-}
-
-func sectionFromParts(parts []mfgPart) mfgSection {
-	offset, sectionSize := sectionSize(parts)
-	blob := make([]byte, sectionSize)
-
-	section := mfgSection{
-		offset: offset,
-		blob:   blob,
-	}
-
-	// Initialize section 0's data as unwritten flash (0xff).
-	for i, _ := range blob {
-		blob[i] = 0xff
-	}
-
-	for _, part := range parts {
-		insertPartIntoBlob(section, part)
-	}
-
-	return section
-}
-
-func (mi *MfgImage) devicePartMap() (map[int][]mfgPart, error) {
-	dpMap := map[int][]mfgPart{}
-
-	// Create parts from the raw entries.
-	for i, entry := range mi.rawEntries {
-		part := partFromRawEntry(entry, i)
-		dpMap[entry.device] = append(dpMap[entry.device], part)
-	}
-
-	// Insert the boot loader and image parts into section 0.
-	targetParts, err := mi.targetParts()
-	if err != nil {
-		return nil, err
-	}
-	dpMap[0] = append(dpMap[0], targetParts...)
-
-	// Sort each part slice by offset.
-	for device, _ := range dpMap {
-		sortParts(dpMap[device])
-	}
-
-	return dpMap, nil
-}
-
-func (mi *MfgImage) deviceSectionMap() (map[int]mfgSection, error) {
-	dpMap, err := mi.devicePartMap()
-	if err != nil {
-		return nil, err
-	}
-
-	// Convert each part slice into a section.
-	dsMap := map[int]mfgSection{}
-	for device, parts := range dpMap {
-		dsMap[device] = sectionFromParts(parts)
-	}
-
-	return dsMap, nil
-}
-
-func (mi *MfgImage) createSections() (createState, error) {
-	cs := createState{}
-
-	var err error
-
-	if err := mi.detectOverlaps(); err != nil {
-		return cs, err
-	}
-
-	cs.dsMap, err = mi.deviceSectionMap()
-	if err != nil {
-		return cs, err
-	}
-
-	if len(cs.dsMap) < 1 {
-		panic("Invalid state; no section 0")
-	}
-
-	cs.metaOffset, cs.hashOffset, err = insertMeta(cs.dsMap[0].blob,
-		mi.bsp.FlashMap)
-	if err != nil {
-		return cs, err
-	}
-
-	// Calculate manufacturing hash.
-	devices := make([]int, 0, len(cs.dsMap))
-	for device, _ := range cs.dsMap {
-		devices = append(devices, device)
-	}
-	sort.Ints(devices)
-
-	sections := make([][]byte, len(devices))
-	for i, device := range devices {
-		sections[i] = cs.dsMap[device].blob
-	}
-	cs.hash = calcMetaHash(sections)
-	copy(cs.dsMap[0].blob[cs.hashOffset:cs.hashOffset+META_HASH_SZ], cs.hash)
-
-	return cs, nil
-}
-
-func areaNameFromImgIdx(imgIdx int) (string, error) {
-	switch imgIdx {
-	case 0:
-		return flash.FLASH_AREA_NAME_IMAGE_0, nil
-	case 1:
-		return flash.FLASH_AREA_NAME_IMAGE_1, nil
-	default:
-		return "", util.FmtNewtError("invalid image index: %d", imgIdx)
-	}
-}
-
-func bootLoaderFromPaths(t *target.Target) []string {
-	return []string{
-		/* boot.elf */
-		builder.AppElfPath(t.Name(), builder.BUILD_NAME_APP, t.App().Name()),
-
-		/* boot.elf.bin */
-		builder.AppBinPath(t.Name(), builder.BUILD_NAME_APP, t.App().Name()),
-
-		/* manifest.json */
-		builder.ManifestPath(t.Name(), builder.BUILD_NAME_APP, t.App().Name()),
-	}
-}
-
-func loaderFromPaths(t *target.Target) []string {
-	if t.LoaderName == "" {
-		return nil
-	}
-
-	return []string{
-		/* <loader>.elf */
-		builder.AppElfPath(t.Name(), builder.BUILD_NAME_LOADER,
-			t.Loader().Name()),
-
-		/* <app>.img */
-		builder.AppImgPath(t.Name(), builder.BUILD_NAME_LOADER,
-			t.Loader().Name()),
-	}
-}
-
-func appFromPaths(t *target.Target) []string {
-	return []string{
-		/* <app>.elf */
-		builder.AppElfPath(t.Name(), builder.BUILD_NAME_APP, t.App().Name()),
-
-		/* <app>.img */
-		builder.AppImgPath(t.Name(), builder.BUILD_NAME_APP, t.App().Name()),
-
-		/* manifest.json */
-		builder.ManifestPath(t.Name(), builder.BUILD_NAME_APP, t.App().Name()),
-	}
-}
-
-func imageFromPaths(t *target.Target) []string {
-	paths := loaderFromPaths(t)
-	paths = append(paths, appFromPaths(t)...)
-	return paths
-}
-
-func (mi *MfgImage) copyBinFile(srcPath string, dstDir string) error {
-	dstPath := dstDir + "/" + filepath.Base(srcPath)
-
-	util.StatusMessage(util.VERBOSITY_VERBOSE, "copying file %s --> %s\n",
-		srcPath, dstPath)
-
-	if err := util.CopyFile(srcPath, dstPath); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func (mi *MfgImage) copyBinFiles() error {
-	dstPath := MfgBinDir(mi.basePkg.Name())
-	if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
-		return util.ChildNewtError(err)
-	}
-
-	bootPaths := bootLoaderFromPaths(mi.boot)
-	for _, path := range bootPaths {
-		dstDir := MfgBootDir(mi.basePkg.Name())
-		if err := mi.copyBinFile(path, dstDir); err != nil {
-			return err
-		}
-	}
-
-	for i, imgTarget := range mi.images {
-		imgPaths := imageFromPaths(imgTarget)
-		dstDir := MfgImageBinDir(mi.basePkg.Name(), i)
-		for _, path := range imgPaths {
-			if err := mi.copyBinFile(path, dstDir); err != nil {
-				return err
-			}
-		}
-	}
-
-	return nil
-}
-
-func (mi *MfgImage) dstBootBinPath() string {
-	if mi.boot == nil {
-		return ""
-	}
-
-	return fmt.Sprintf("%s/%s.elf.bin",
-		MfgBootDir(mi.basePkg.Name()),
-		pkg.ShortName(mi.boot.App()))
-}
-
-func (mi *MfgImage) dstImgPath(slotIdx int) string {
-	var pack *pkg.LocalPackage
-	var imgIdx int
-
-	if len(mi.images) >= 1 {
-		switch slotIdx {
-		case 0:
-			if mi.images[0].LoaderName != "" {
-				pack = mi.images[0].Loader()
-			} else {
-				pack = mi.images[0].App()
-			}
-			imgIdx = 0
-
-		case 1:
-			if mi.images[0].LoaderName != "" {
-				pack = mi.images[0].App()
-				imgIdx = 0
-			} else {
-				if len(mi.images) >= 2 {
-					pack = mi.images[1].App()
-				}
-				imgIdx = 1
-			}
-
-		default:
-			panic(fmt.Sprintf("invalid image index: %d", imgIdx))
-		}
-	}
-
-	if pack == nil {
-		return ""
-	}
-
-	return fmt.Sprintf("%s/%s.img",
-		MfgImageBinDir(mi.basePkg.Name(), imgIdx), pkg.ShortName(pack))
-}
-
-// Returns a slice containing the path of each file required to build the
-// manufacturing image.
-func (mi *MfgImage) FromPaths() []string {
-	paths := []string{}
-
-	if mi.boot != nil {
-		paths = append(paths, bootLoaderFromPaths(mi.boot)...)
-	}
-	if len(mi.images) >= 1 {
-		paths = append(paths, imageFromPaths(mi.images[0])...)
-	}
-	if len(mi.images) >= 2 {
-		paths = append(paths, imageFromPaths(mi.images[1])...)
-	}
-
-	for _, raw := range mi.rawEntries {
-		paths = append(paths, raw.filename)
-	}
-
-	return paths
-}
-
-func (mi *MfgImage) build() (createState, error) {
-	if err := mi.copyBinFiles(); err != nil {
-		return createState{}, err
-	}
-
-	cs, err := mi.createSections()
-	if err != nil {
-		return cs, err
-	}
-
-	return cs, nil
-}
-
-func (mi *MfgImage) createManifest(cs createState) ([]byte, error) {
-	manifest := mfgManifest{
-		BuildTime:   time.Now().Format(time.RFC3339),
-		Version:     mi.version.String(),
-		MfgHash:     fmt.Sprintf("%x", cs.hash),
-		MetaSection: 0,
-		MetaOffset:  cs.metaOffset,
-	}
-	buffer, err := json.MarshalIndent(manifest, "", "  ")
-	if err != nil {
-		return nil, util.FmtNewtError("Failed to encode mfg manifest: %s",
-			err.Error())
-	}
-
-	return buffer, nil
-}
-
-func appendNonEmptyStr(dst []string, src string) []string {
-	if src != "" {
-		dst = append(dst, src)
-	}
-
-	return dst
-}
-
-func (mi *MfgImage) ToPaths() []string {
-	paths := []string{}
-
-	paths = appendNonEmptyStr(paths, mi.BootBinPath())
-	paths = appendNonEmptyStr(paths, mi.BootElfPath())
-	paths = appendNonEmptyStr(paths, mi.BootManifestPath())
-
-	for i := 0; i < len(mi.images); i++ {
-		paths = appendNonEmptyStr(paths, mi.LoaderImgPath(i))
-		paths = appendNonEmptyStr(paths, mi.LoaderElfPath(i))
-		paths = appendNonEmptyStr(paths, mi.AppImgPath(i))
-		paths = appendNonEmptyStr(paths, mi.AppElfPath(i))
-		paths = appendNonEmptyStr(paths, mi.ImageManifestPath(i))
-	}
-
-	paths = append(paths, mi.SectionBinPaths()...)
-	paths = append(paths, mi.SectionHexPaths()...)
-	paths = append(paths, mi.ManifestPath())
-
-	return paths
-}
-
-// @return                      [paths-of-artifacts], error
-func (mi *MfgImage) CreateMfgImage() ([]string, error) {
-	cs, err := mi.build()
-	if err != nil {
-		return nil, err
-	}
-
-	sectionDir := MfgSectionBinDir(mi.basePkg.Name())
-	if err := os.MkdirAll(sectionDir, 0755); err != nil {
-		return nil, util.ChildNewtError(err)
-	}
-
-	for device, section := range cs.dsMap {
-		sectionPath := MfgSectionBinPath(mi.basePkg.Name(), device)
-		err := ioutil.WriteFile(sectionPath, section.blob[section.offset:], 0644)
-		if err != nil {
-			return nil, util.ChildNewtError(err)
-		}
-		hexPath := MfgSectionHexPath(mi.basePkg.Name(), device)
-		mi.compiler.ConvertBinToHex(sectionPath, hexPath, section.offset)
-	}
-
-	manifest, err := mi.createManifest(cs)
-	if err != nil {
-		return nil, err
-	}
-
-	manifestPath := mi.ManifestPath()
-	if err := ioutil.WriteFile(manifestPath, manifest, 0644); err != nil {
-		return nil, util.FmtNewtError("Failed to write mfg manifest file: %s",
-			err.Error())
-	}
-
-	return mi.ToPaths(), nil
-}
diff --git a/newt/mfg/decode.go b/newt/mfg/decode.go
new file mode 100644
index 0000000..802e88d
--- /dev/null
+++ b/newt/mfg/decode.go
@@ -0,0 +1,310 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// This file contains functionality for loading mfg definitions from `mfg.yml`
+// files.
+
+package mfg
+
+import (
+	"github.com/spf13/cast"
+
+	"mynewt.apache.org/newt/newt/ycfg"
+	"mynewt.apache.org/newt/util"
+)
+
+// Indicates that an element is located at the end of a flash area.
+const OFFSET_END = -1
+
+type DecodedTarget struct {
+	Name   string
+	Area   string
+	Offset int
+}
+
+type DecodedRaw struct {
+	Filename string
+	Area     string
+	Offset   int
+}
+
+type DecodedMmrRef struct {
+	Area string
+}
+
+type DecodedMeta struct {
+	Area     string
+	Hash     bool
+	FlashMap bool
+	Mmrs     []DecodedMmrRef
+}
+
+type DecodedMfg struct {
+	Targets []DecodedTarget
+	Raws    []DecodedRaw
+	Meta    *DecodedMeta
+
+	// Only required if no targets present.
+	Bsp string
+}
+
+func decodeOffsetStr(offsetStr string) (int, error) {
+	if offsetStr == "end" {
+		return OFFSET_END, nil
+	}
+
+	offsetInt, err := cast.ToIntE(offsetStr)
+	if err != nil {
+		return 0, util.FmtNewtError("invalid offset value: \"%s\"", offsetStr)
+	}
+
+	return offsetInt, nil
+}
+
+func decodeBool(kv map[string]interface{}, key string) (*bool, error) {
+	var bp *bool
+
+	val := kv[key]
+	if val != nil {
+		b, err := cast.ToBoolE(val)
+		if err != nil {
+			return nil, util.FmtNewtError(
+				"invalid `%s` value \"%v\"; "+
+					"value must be either \"true\" or \"false\"", key, val)
+		}
+
+		bp = &b
+	}
+
+	return bp, nil
+}
+
+func decodeBoolDflt(kv map[string]interface{}, key string,
+	dflt bool) (bool, error) {
+
+	bp, err := decodeBool(kv, key)
+	if err != nil {
+		return false, err
+	}
+
+	if bp == nil {
+		return dflt, nil
+	} else {
+		return *bp, nil
+	}
+}
+
+func decodeTarget(yamlTarget interface{}) (DecodedTarget, error) {
+	dt := DecodedTarget{}
+
+	kv, err := cast.ToStringMapE(yamlTarget)
+	if err != nil {
+		return dt, util.FmtNewtError(
+			"mfg contains invalid `mfg.targets` map: %s", err.Error())
+	}
+
+	nameVal := kv["name"]
+	if nameVal == nil {
+		return dt, util.FmtNewtError(
+			"mfg target entry missing required field \"name\"")
+	}
+	dt.Name = cast.ToString(nameVal)
+
+	areaVal := kv["area"]
+	if areaVal == nil {
+		return dt, util.FmtNewtError(
+			"target entry \"%s\" missing required field \"area\"", dt.Name)
+	}
+	dt.Area = cast.ToString(areaVal)
+
+	offsetVal := kv["offset"]
+	if offsetVal == nil {
+		return dt, util.FmtNewtError(
+			"target entry \"%s\" missing required field \"offset\"", dt.Name)
+	}
+	offsetStr := cast.ToString(offsetVal)
+	offsetInt, err := decodeOffsetStr(offsetStr)
+	if err != nil {
+		return dt, util.FmtNewtError(
+			"in target entry \"%s\": %s", dt.Name, err.Error())
+	}
+	dt.Offset = offsetInt
+
+	return dt, nil
+}
+
+func decodeRaw(yamlRaw interface{}, entryIdx int) (DecodedRaw, error) {
+	dr := DecodedRaw{}
+
+	kv, err := cast.ToStringMapE(yamlRaw)
+	if err != nil {
+		return dr, util.FmtNewtError(
+			"mfg contains invalid `mfg.raw` map: %s", err.Error())
+	}
+
+	areaVal := kv["area"]
+	if areaVal == nil {
+		return dr, util.FmtNewtError(
+			"raw entry missing required field \"area\"")
+	}
+	dr.Area = cast.ToString(areaVal)
+
+	offsetVal := kv["offset"]
+	if offsetVal == nil {
+		return dr, util.FmtNewtError(
+			"mfg raw entry missing required field \"offset\"")
+	}
+	offsetStr := cast.ToString(offsetVal)
+	offsetInt, err := decodeOffsetStr(offsetStr)
+	if err != nil {
+		return dr, util.FmtNewtError(
+			"in raw entry %d: %s", entryIdx, err.Error())
+	}
+	dr.Offset = offsetInt
+
+	filenameVal := kv["name"]
+	if filenameVal == nil {
+		return dr, util.FmtNewtError(
+			"mfg raw entry missing required field \"filename\"")
+	}
+	dr.Filename = cast.ToString(filenameVal)
+
+	return dr, nil
+}
+
+func decodeMmr(yamlMmr interface{}) (DecodedMmrRef, error) {
+	dm := DecodedMmrRef{}
+
+	kv, err := cast.ToStringMapE(yamlMmr)
+	if err != nil {
+		return dm, util.FmtNewtError(
+			"mfg meta contains invalid `mmrs` sequence: %s", err.Error())
+	}
+
+	areaVal := kv["area"]
+	if areaVal == nil {
+		return dm, util.FmtNewtError(
+			"mmr entry missing required field \"area\"")
+	}
+	dm.Area = cast.ToString(areaVal)
+
+	return dm, nil
+}
+
+func decodeMmrs(yamlMmrs interface{}) ([]DecodedMmrRef, error) {
+	yamlSlice, err := cast.ToSliceE(yamlMmrs)
+	if err != nil {
+		return nil, util.FmtNewtError(
+			"mfg meta contains invalid `mmrs` sequence: %s", err.Error())
+	}
+
+	mmrs := []DecodedMmrRef{}
+	for _, yamlMmr := range yamlSlice {
+		mmr, err := decodeMmr(yamlMmr)
+		if err != nil {
+			return nil, err
+		}
+		mmrs = append(mmrs, mmr)
+	}
+
+	return mmrs, nil
+}
+
+func decodeMeta(
+	kv map[string]interface{}) (DecodedMeta, error) {
+
+	dm := DecodedMeta{}
+
+	areaVal := kv["area"]
+	if areaVal == nil {
+		return dm, util.FmtNewtError(
+			"meta map missing required field \"area\"")
+	}
+	dm.Area = cast.ToString(areaVal)
+
+	hash, err := decodeBoolDflt(kv, "hash", false)
+	if err != nil {
+		return dm, err
+	}
+	dm.Hash = hash
+
+	fm, err := decodeBoolDflt(kv, "flash_map", false)
+	if err != nil {
+		return dm, err
+	}
+	dm.FlashMap = fm
+
+	yamlMmrs := kv["mmrs"]
+	if yamlMmrs != nil {
+		mmrs, err := decodeMmrs(yamlMmrs)
+		if err != nil {
+			return dm, err
+		}
+		dm.Mmrs = mmrs
+	}
+
+	return dm, nil
+}
+
+func decodeMfg(yc ycfg.YCfg) (DecodedMfg, error) {
+	dm := DecodedMfg{}
+
+	yamlTargets := yc.GetValSlice("mfg.targets", nil)
+	if yamlTargets != nil {
+		for _, yamlTarget := range yamlTargets {
+			t, err := decodeTarget(yamlTarget)
+			if err != nil {
+				return dm, err
+			}
+
+			dm.Targets = append(dm.Targets, t)
+		}
+	}
+
+	dm.Bsp = yc.GetValString("mfg.bsp", nil)
+
+	if len(dm.Targets) == 0 && dm.Bsp == "" {
+		return dm, util.FmtNewtError(
+			"\"mfg.bsp\" field required for mfg images without any targets")
+	}
+
+	itf := yc.GetValSlice("mfg.raw", nil)
+	slice := cast.ToSlice(itf)
+	if slice != nil {
+		for i, yamlRaw := range slice {
+			raw, err := decodeRaw(yamlRaw, i)
+			if err != nil {
+				return dm, err
+			}
+
+			dm.Raws = append(dm.Raws, raw)
+		}
+	}
+
+	yamlMeta := yc.GetValStringMap("mfg.meta", nil)
+	if yamlMeta != nil {
+		meta, err := decodeMeta(yamlMeta)
+		if err != nil {
+			return dm, err
+		}
+		dm.Meta = &meta
+	}
+
+	return dm, nil
+}
diff --git a/newt/mfg/emit.go b/newt/mfg/emit.go
new file mode 100644
index 0000000..b977fe7
--- /dev/null
+++ b/newt/mfg/emit.go
@@ -0,0 +1,342 @@
+/**
+ * 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 mfg
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"time"
+
+	"mynewt.apache.org/newt/artifact/flash"
+	"mynewt.apache.org/newt/artifact/image"
+	"mynewt.apache.org/newt/artifact/manifest"
+	"mynewt.apache.org/newt/artifact/mfg"
+	"mynewt.apache.org/newt/newt/builder"
+	"mynewt.apache.org/newt/newt/flashmap"
+	"mynewt.apache.org/newt/newt/target"
+	"mynewt.apache.org/newt/util"
+)
+
+// Current manufacturing image binary format version.
+const MANIFEST_FORMAT = 2
+
+// Represents a file copy operation.
+type CpEntry struct {
+	From string
+	To   string
+}
+
+type MfgEmitTarget struct {
+	Name         string
+	Offset       int
+	IsBoot       bool
+	BinPath      string
+	ElfPath      string
+	ManifestPath string
+}
+
+type MfgEmitRaw struct {
+	Filename string
+	Offset   int
+}
+
+type MfgEmitMetaMmr struct {
+	Area flash.FlashArea
+}
+
+type MfgEmitMeta struct {
+	Offset   int
+	Hash     bool
+	FlashMap bool
+	Mmrs     []MfgEmitMetaMmr
+}
+
+type MfgEmitter struct {
+	Name    string
+	Ver     image.ImageVersion
+	Targets []MfgEmitTarget
+	Raws    []MfgEmitRaw
+	Meta    *MfgEmitMeta
+
+	Mfg      mfg.Mfg
+	Device   int
+	FlashMap flashmap.FlashMap
+}
+
+// Calculates the source path of a target's binary.  Boot loader targets use
+// `.bin` files; image targets use `.img`.
+func targetSrcBinPath(t *target.Target, isBoot bool) string {
+	if isBoot {
+		return builder.AppBinPath(t.Name(), builder.BUILD_NAME_APP,
+			t.App().Name())
+	} else {
+		return builder.AppImgPath(t.Name(), builder.BUILD_NAME_APP,
+			t.App().Name())
+	}
+}
+
+// Calculates the source path of a target's `.elf` file.
+func targetSrcElfPath(t *target.Target) string {
+	return builder.AppElfPath(t.Name(), builder.BUILD_NAME_APP, t.App().Name())
+}
+
+// Calculates the source path of a target's manifest file.
+func targetSrcManifestPath(t *target.Target) string {
+	return builder.ManifestPath(t.Name(), builder.BUILD_NAME_APP,
+		t.App().Name())
+}
+
+func newMfgEmitTarget(bt MfgBuildTarget) (MfgEmitTarget, error) {
+	return MfgEmitTarget{
+		Name:    bt.Target.FullName(),
+		Offset:  bt.Area.Offset + bt.Offset,
+		IsBoot:  bt.IsBoot,
+		BinPath: targetSrcBinPath(bt.Target, bt.IsBoot),
+		ElfPath: targetSrcElfPath(bt.Target),
+		ManifestPath: builder.ManifestPath(bt.Target.Name(),
+			builder.BUILD_NAME_APP, bt.Target.App().Name()),
+	}, nil
+}
+
+func newMfgEmitRaw(br MfgBuildRaw) MfgEmitRaw {
+	return MfgEmitRaw{
+		Filename: br.Filename,
+		Offset:   br.Area.Offset + br.Offset,
+	}
+}
+
+func newMfgEmitMeta(bm MfgBuildMeta, metaOff int) MfgEmitMeta {
+	mmrs := []MfgEmitMetaMmr{}
+	for _, bmmr := range bm.Mmrs {
+		mmr := MfgEmitMetaMmr{
+			Area: bmmr.Area,
+		}
+		mmrs = append(mmrs, mmr)
+	}
+
+	return MfgEmitMeta{
+		Offset:   bm.Area.Offset + metaOff,
+		Hash:     bm.Hash,
+		FlashMap: bm.FlashMap,
+		Mmrs:     mmrs,
+	}
+}
+
+// NewMfgEmitter creates an mfg emitter from an mfg builder.
+func NewMfgEmitter(mb MfgBuilder, name string, ver image.ImageVersion,
+	device int) (MfgEmitter, error) {
+
+	me := MfgEmitter{
+		Name:     name,
+		Ver:      ver,
+		Device:   device,
+		FlashMap: mb.Bsp.FlashMap,
+	}
+
+	m, err := mb.Build()
+	if err != nil {
+		return me, err
+	}
+	me.Mfg = m
+
+	for _, bt := range mb.Targets {
+		et, err := newMfgEmitTarget(bt)
+		if err != nil {
+			return me, err
+		}
+
+		me.Targets = append(me.Targets, et)
+	}
+
+	for _, br := range mb.Raws {
+		et := newMfgEmitRaw(br)
+		me.Raws = append(me.Raws, et)
+	}
+
+	if mb.Meta != nil {
+		mm := newMfgEmitMeta(*mb.Meta, me.Mfg.MetaOff)
+		me.Meta = &mm
+	}
+
+	return me, nil
+}
+
+// Calculates the necessary file copy operations for emitting an mfg image.
+func (me *MfgEmitter) calcCpEntries() []CpEntry {
+	entries := []CpEntry{}
+	for i, mt := range me.Targets {
+		var binTo string
+		if mt.IsBoot {
+			binTo = MfgTargetBinPath(i)
+		} else {
+			binTo = MfgTargetImgPath(i)
+		}
+
+		entry := CpEntry{
+			From: mt.BinPath,
+			To:   MfgBinDir(me.Name) + "/" + binTo,
+		}
+		entries = append(entries, entry)
+
+		entry = CpEntry{
+			From: mt.ElfPath,
+			To: MfgBinDir(me.Name) + "/" +
+				MfgTargetElfPath(i),
+		}
+		entries = append(entries, entry)
+
+		entry = CpEntry{
+			From: mt.ManifestPath,
+			To: MfgBinDir(me.Name) + "/" +
+				MfgTargetManifestPath(i),
+		}
+		entries = append(entries, entry)
+	}
+
+	return entries
+}
+
+func copyBinFiles(entries []CpEntry) error {
+	for _, entry := range entries {
+		if err := os.MkdirAll(filepath.Dir(entry.To), 0755); err != nil {
+			return util.ChildNewtError(err)
+		}
+
+		util.StatusMessage(util.VERBOSITY_VERBOSE, "copying file %s --> %s\n",
+			entry.From, entry.To)
+
+		if err := util.CopyFile(entry.From, entry.To); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// emitManifest generates an mfg manifest.
+func (me *MfgEmitter) emitManifest() ([]byte, error) {
+	hashBytes := me.Mfg.Meta.Hash()
+	if hashBytes == nil {
+		// No hash TLV; calculate hash manually.
+		bin, err := me.Mfg.Bytes(0xff)
+		if err != nil {
+			return nil, err
+		}
+		hashBytes = mfg.CalcHash(bin)
+	}
+	hashStr := fmt.Sprintf("%x", hashBytes)
+
+	mm := manifest.MfgManifest{
+		Name:       me.Name,
+		BuildTime:  time.Now().Format(time.RFC3339),
+		Format:     MANIFEST_FORMAT,
+		MfgHash:    hashStr,
+		Version:    me.Ver.String(),
+		Device:     me.Device,
+		BinPath:    mfg.MFG_IMG_FILENAME,
+		FlashAreas: me.FlashMap.SortedAreas(),
+	}
+
+	for i, t := range me.Targets {
+		mmt := manifest.MfgManifestTarget{
+			Name:         t.Name,
+			ManifestPath: MfgTargetManifestPath(i),
+			Offset:       t.Offset,
+		}
+
+		if t.IsBoot {
+			mmt.BinPath = MfgTargetBinPath(i)
+		} else {
+			mmt.ImagePath = MfgTargetImgPath(i)
+		}
+
+		mm.Targets = append(mm.Targets, mmt)
+	}
+
+	if me.Meta != nil {
+		mmm := manifest.MfgManifestMeta{
+			EndOffset: me.Mfg.MetaOff + int(me.Mfg.Meta.Footer.Size),
+			Size:      int(me.Mfg.Meta.Footer.Size),
+		}
+
+		mmm.Hash = me.Meta.Hash
+		mmm.FlashMap = me.Meta.FlashMap
+
+		for _, mmr := range me.Meta.Mmrs {
+			mmm.Mmrs = append(mmm.Mmrs, manifest.MfgManifestMetaMmr{
+				Area:      mmr.Area.Name,
+				Device:    mmr.Area.Device,
+				EndOffset: mmr.Area.Offset + mmr.Area.Size,
+			})
+		}
+
+		mm.Meta = &mmm
+	}
+
+	return mm.MarshalJson()
+}
+
+// @return                      [source-paths], [dest-paths], error
+func (me *MfgEmitter) Emit() ([]string, []string, error) {
+	mbin, err := me.Mfg.Bytes(0xff)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	cpEntries := me.calcCpEntries()
+	if err := copyBinFiles(cpEntries); err != nil {
+		return nil, nil, err
+	}
+
+	// Write mfgimg.bin
+	binPath := MfgBinPath(me.Name)
+	if err := os.MkdirAll(filepath.Dir(binPath), 0755); err != nil {
+		return nil, nil, util.ChildNewtError(err)
+	}
+	if err := ioutil.WriteFile(binPath, mbin, 0644); err != nil {
+		return nil, nil, err
+	}
+
+	// Write manifest.
+	manifest, err := me.emitManifest()
+	if err != nil {
+		return nil, nil, err
+	}
+
+	manifestPath := MfgManifestPath(me.Name)
+	if err := ioutil.WriteFile(manifestPath, manifest, 0644); err != nil {
+		return nil, nil, util.FmtNewtError(
+			"Failed to write mfg manifest file: %s", err.Error())
+	}
+
+	srcPaths := []string{}
+	dstPaths := []string{
+		binPath,
+		manifestPath,
+	}
+	for _, entry := range cpEntries {
+		srcPaths = append(srcPaths, entry.From)
+		dstPaths = append(dstPaths, entry.To)
+	}
+
+	return srcPaths, dstPaths, nil
+}
diff --git a/newt/mfg/load.go b/newt/mfg/load.go
deleted file mode 100644
index e17635b..0000000
--- a/newt/mfg/load.go
+++ /dev/null
@@ -1,317 +0,0 @@
-/**
- * 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 mfg
-
-import (
-	"fmt"
-	"io/ioutil"
-	"sort"
-	"strconv"
-	"strings"
-
-	"github.com/spf13/cast"
-
-	"mynewt.apache.org/newt/newt/newtutil"
-	"mynewt.apache.org/newt/newt/pkg"
-	"mynewt.apache.org/newt/newt/project"
-	"mynewt.apache.org/newt/newt/target"
-	"mynewt.apache.org/newt/newt/toolchain"
-	"mynewt.apache.org/newt/util"
-)
-
-const MFG_YAML_FILENAME string = "mfg.yml"
-
-type partSorter struct {
-	parts []mfgPart
-}
-
-func (s partSorter) Len() int {
-	return len(s.parts)
-}
-func (s partSorter) Swap(i, j int) {
-	s.parts[i], s.parts[j] = s.parts[j], s.parts[i]
-}
-func (s partSorter) Less(i, j int) bool {
-	return s.parts[i].offset < s.parts[j].offset
-}
-
-func sortParts(parts []mfgPart) []mfgPart {
-	sorter := partSorter{
-		parts: parts,
-	}
-
-	sort.Sort(sorter)
-	return sorter.parts
-}
-
-func (mi *MfgImage) loadError(
-	msg string, args ...interface{}) *util.NewtError {
-
-	return util.FmtNewtError("Error in %s mfg: %s", mi.basePkg.Name(),
-		fmt.Sprintf(msg, args...))
-
-}
-
-func (mi *MfgImage) loadTarget(targetName string) (
-	*target.Target, error) {
-
-	tgt := target.GetTargets()[targetName]
-	if tgt == nil {
-		return nil, mi.loadError("cannot resolve referenced target \"%s\"",
-			targetName)
-	}
-
-	return tgt, nil
-}
-
-func (mi *MfgImage) loadRawEntry(
-	entryIdx int, rawEntry map[string]string) (MfgRawEntry, error) {
-
-	raw := MfgRawEntry{}
-
-	var err error
-
-	deviceStr := rawEntry["device"]
-	if deviceStr == "" {
-		return raw, mi.loadError(
-			"raw entry %d missing required \"device\" field", entryIdx)
-	}
-
-	raw.device, err = util.AtoiNoOct(deviceStr)
-	if err != nil {
-		return raw, mi.loadError(
-			"raw entry %d contains invalid offset: %s", entryIdx, deviceStr)
-	}
-
-	offsetStr := rawEntry["offset"]
-	if offsetStr == "" {
-		return raw, mi.loadError(
-			"raw entry %d missing required \"offset\" field", entryIdx)
-	}
-
-	raw.offset, err = util.AtoiNoOct(offsetStr)
-	if err != nil {
-		return raw, mi.loadError(
-			"raw entry %d contains invalid offset: %s", entryIdx, offsetStr)
-	}
-
-	raw.filename = rawEntry["file"]
-	if raw.filename == "" {
-		return raw, mi.loadError(
-			"raw entry %d missing required \"file\" field", entryIdx)
-	}
-
-	if !strings.HasPrefix(raw.filename, "/") {
-		raw.filename = mi.basePkg.BasePath() + "/" + raw.filename
-	}
-
-	raw.data, err = ioutil.ReadFile(raw.filename)
-	if err != nil {
-		return raw, mi.loadError(
-			"error loading file for raw entry %d; filename=%s: %s",
-			entryIdx, raw.filename, err.Error())
-	}
-
-	return raw, nil
-}
-
-func (mi *MfgImage) detectInvalidDevices() error {
-	sectionIds := mi.sectionIds()
-	deviceIds := mi.bsp.FlashMap.DeviceIds()
-
-	deviceMap := map[int]struct{}{}
-	for _, device := range deviceIds {
-		deviceMap[device] = struct{}{}
-	}
-
-	invalidIds := []int{}
-	for _, sectionId := range sectionIds {
-		if _, ok := deviceMap[sectionId]; !ok {
-			invalidIds = append(invalidIds, sectionId)
-		}
-	}
-
-	if len(invalidIds) == 0 {
-		return nil
-	}
-
-	listStr := ""
-	for i, id := range invalidIds {
-		if i != 0 {
-			listStr += ", "
-		}
-		listStr += strconv.Itoa(id)
-	}
-
-	return util.FmtNewtError(
-		"image specifies flash devices that are not present in the BSP's "+
-			"flash map: %s", listStr)
-}
-
-func (mi *MfgImage) detectOverlaps() error {
-	type overlap struct {
-		part0 mfgPart
-		part1 mfgPart
-	}
-
-	overlaps := []overlap{}
-
-	dpMap, err := mi.devicePartMap()
-	if err != nil {
-		return err
-	}
-
-	// Iterate flash devices in order.
-	devices := make([]int, 0, len(dpMap))
-	for device, _ := range dpMap {
-		devices = append(devices, device)
-	}
-	sort.Ints(devices)
-
-	for _, device := range devices {
-		parts := dpMap[device]
-		for i, part0 := range parts[:len(parts)-1] {
-			part0End := part0.offset + len(part0.data)
-			for _, part1 := range parts[i+1:] {
-				// Parts are sorted by offset, so only one comparison is
-				// necessary to detect overlap.
-				if part1.offset < part0End {
-					overlaps = append(overlaps, overlap{
-						part0: part0,
-						part1: part1,
-					})
-				}
-			}
-		}
-	}
-
-	if len(overlaps) > 0 {
-		str := "flash overlaps detected:"
-		for _, overlap := range overlaps {
-
-			part0End := overlap.part0.offset + len(overlap.part0.data)
-			part1End := overlap.part1.offset + len(overlap.part1.data)
-			str += fmt.Sprintf("\n    * s%d [%s] (%d - %d) <=> [%s] (%d - %d)",
-				overlap.part0.device,
-				overlap.part0.name, overlap.part0.offset, part0End,
-				overlap.part1.name, overlap.part1.offset, part1End)
-		}
-
-		return util.NewNewtError(str)
-	}
-
-	return nil
-}
-
-func Load(basePkg *pkg.LocalPackage) (*MfgImage, error) {
-	v, err := newtutil.ReadConfig(basePkg.BasePath(),
-		strings.TrimSuffix(MFG_YAML_FILENAME, ".yml"))
-	if err != nil {
-		return nil, err
-	}
-
-	mi := &MfgImage{
-		basePkg: basePkg,
-	}
-
-	bootName := v.GetValString("mfg.bootloader", nil)
-	if bootName == "" {
-		return nil, mi.loadError("mfg.bootloader field required")
-	}
-	mi.boot, err = mi.loadTarget(bootName)
-	if err != nil {
-		return nil, err
-	}
-
-	imgNames := v.GetValStringSlice("mfg.images", nil)
-	if imgNames != nil {
-		for _, imgName := range imgNames {
-			imgTarget, err := mi.loadTarget(imgName)
-			if err != nil {
-				return nil, err
-			}
-
-			mi.images = append(mi.images, imgTarget)
-		}
-	}
-
-	if len(mi.images) > 2 {
-		return nil, mi.loadError("too many images (%d); maximum is 2",
-			len(mi.images))
-	}
-
-	itf := v.GetFirstVal("mfg.raw", nil)
-	slice := cast.ToSlice(itf)
-	if slice != nil {
-		for i, entryItf := range slice {
-			yamlEntry := cast.ToStringMapString(entryItf)
-			entry, err := mi.loadRawEntry(i, yamlEntry)
-			if err != nil {
-				return nil, err
-			}
-
-			mi.rawEntries = append(mi.rawEntries, entry)
-		}
-	}
-
-	proj := project.GetProject()
-
-	bspLpkg, err := proj.ResolvePackage(mi.basePkg.Repo(),
-		mi.boot.BspName)
-	if err != nil {
-		return nil, mi.loadError(
-			"could not resolve boot loader BSP package: %s",
-			mi.boot.BspName)
-	}
-	mi.bsp, err = pkg.NewBspPackage(bspLpkg)
-	if err != nil {
-		return nil, mi.loadError(err.Error())
-	}
-
-	compilerPkg, err := proj.ResolvePackage(mi.bsp.Repo(), mi.bsp.CompilerName)
-	if err != nil {
-		return nil, mi.loadError(err.Error())
-	}
-	mi.compiler, err = toolchain.NewCompiler(compilerPkg.BasePath(), "",
-		target.DEFAULT_BUILD_PROFILE)
-	if err != nil {
-		return nil, mi.loadError(err.Error())
-	}
-
-	for _, imgTarget := range mi.images {
-		if len(mi.images) > 1 && imgTarget.LoaderName != "" {
-			return nil, mi.loadError("only one image allowed in "+
-				"split image mode (%s is a split build)", imgTarget.Name())
-		}
-
-		if imgTarget.Bsp() != mi.bsp.LocalPackage {
-			return nil, mi.loadError(
-				"image target \"%s\" specified conflicting BSP; "+
-					"boot loader uses %s, image uses %s",
-				imgTarget.Name(), mi.bsp.Name(), imgTarget.BspName)
-		}
-	}
-
-	if err := mi.detectInvalidDevices(); err != nil {
-		return nil, err
-	}
-
-	return mi, nil
-}
diff --git a/newt/mfg/meta.go b/newt/mfg/meta.go
deleted file mode 100644
index 5e6e5ad..0000000
--- a/newt/mfg/meta.go
+++ /dev/null
@@ -1,248 +0,0 @@
-/**
- * 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 mfg
-
-import (
-	"bytes"
-	"crypto/sha256"
-	"encoding/binary"
-
-	"mynewt.apache.org/newt/artifact/flash"
-	"mynewt.apache.org/newt/newt/flashmap"
-	"mynewt.apache.org/newt/util"
-)
-
-// The "manufacturing meta region" is located at the end of the boot loader
-// flash area.  This region has the following structure.
-//
-//  0                   1                   2                   3
-//  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
-// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-// |Version (0x01) |                  0xff padding                 |
-// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-// |   TLV type    |   TLV size    | TLV data ("TLV size" bytes)   ~
-// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               ~
-// ~                                                               ~
-// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-// |   TLV type    |   TLV size    | TLV data ("TLV size" bytes)   ~
-// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               ~
-// ~                                                               ~
-// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-// |   Region size                 |         0xff padding          |
-// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-// |                       Magic (0x3bb2a269)                      |
-// +-+-+-+-+-+--+-+-+-+-end of boot loader area+-+-+-+-+-+-+-+-+-+-+
-//
-// The number of TLVs is variable; two are shown above for illustrative
-// purposes.
-//
-// Fields:
-// <Header>
-// 1. Version: Manufacturing meta version number; always 0x01.
-//
-// <TLVs>
-// 2. TLV type: Indicates the type of data to follow.
-// 3. TLV size: The number of bytes of data to follow.
-// 4. TLV data: TLV-size bytes of data.
-//
-// <Footer>
-// 5. Region size: The size, in bytes, of the entire manufacturing meta region;
-//    includes header, TLVs, and footer.
-// 6. Magic: indicates the presence of the manufacturing meta region.
-
-const META_MAGIC = 0x3bb2a269
-const META_VERSION = 1
-const META_TLV_CODE_HASH = 0x01
-const META_TLV_CODE_FLASH_AREA = 0x02
-
-const META_HASH_SZ = 32
-const META_FOOTER_SZ = 8
-const META_TLV_HASH_SZ = META_HASH_SZ
-const META_TLV_FLASH_AREA_SZ = 12
-
-type metaHeader struct {
-	version uint8  // 1
-	pad8    uint8  // 0xff
-	pad16   uint16 // 0xffff
-}
-
-type metaFooter struct {
-	size  uint16 // Includes header, TLVs, and footer.
-	pad16 uint16 // 0xffff
-	magic uint32 // META_MAGIC
-}
-
-type metaTlvHeader struct {
-	typ  uint8 // Indicates the type of data to follow.
-	size uint8 // The number of bytes of data to follow.
-}
-
-type metaTlvFlashArea struct {
-	header   metaTlvHeader
-	areaId   uint8  // Unique value identifying this flash area.
-	deviceId uint8  // Indicates host flash device (aka section number).
-	pad16    uint16 // 0xffff
-	offset   uint32 // The byte offset within the flash device.
-	size     uint32 // Size, in bytes, of entire flash area.
-}
-
-type metaTlvHash struct {
-	header metaTlvHeader
-	hash   [META_HASH_SZ]byte
-}
-
-func writeElem(elem interface{}, buf *bytes.Buffer) error {
-	/* XXX: Assume target platform uses little endian. */
-	if err := binary.Write(buf, binary.LittleEndian, elem); err != nil {
-		return util.ChildNewtError(err)
-	}
-	return nil
-}
-
-func writeHeader(buf *bytes.Buffer) error {
-	hdr := metaHeader{
-		version: META_VERSION,
-		pad8:    0xff,
-		pad16:   0xffff,
-	}
-	return writeElem(hdr, buf)
-}
-
-func writeFooter(buf *bytes.Buffer) error {
-	ftr := metaFooter{
-		size:  uint16(buf.Len() + META_FOOTER_SZ),
-		pad16: 0xffff,
-		magic: META_MAGIC,
-	}
-	return writeElem(ftr, buf)
-}
-
-func writeTlvHeader(typ uint8, size uint8, buf *bytes.Buffer) error {
-	tlvHdr := metaTlvHeader{
-		typ:  typ,
-		size: size,
-	}
-	return writeElem(tlvHdr, buf)
-}
-
-// Writes a single entry of the flash map TLV.
-func writeFlashMapEntry(area flash.FlashArea, buf *bytes.Buffer) error {
-	tlv := metaTlvFlashArea{
-		header: metaTlvHeader{
-			typ:  META_TLV_CODE_FLASH_AREA,
-			size: META_TLV_FLASH_AREA_SZ,
-		},
-		areaId:   uint8(area.Id),
-		deviceId: uint8(area.Device),
-		pad16:    0xffff,
-		offset:   uint32(area.Offset),
-		size:     uint32(area.Size),
-	}
-	return writeElem(tlv, buf)
-}
-
-// Writes a zeroed-out hash TLV.  The hash's original value must be zero for
-// the actual hash to be calculated later.  After the actual value is
-// calculated, it replaces the zeros in the TLV.
-func writeZeroHash(buf *bytes.Buffer) error {
-	tlv := metaTlvHash{
-		header: metaTlvHeader{
-			typ:  META_TLV_CODE_HASH,
-			size: META_TLV_HASH_SZ,
-		},
-		hash: [META_HASH_SZ]byte{},
-	}
-	return writeElem(tlv, buf)
-}
-
-// @return						meta-offset, hash-offset, error
-func insertMeta(section0Data []byte, flashMap flashmap.FlashMap) (
-	int, int, error) {
-
-	buf := &bytes.Buffer{}
-
-	if err := writeHeader(buf); err != nil {
-		return 0, 0, err
-	}
-
-	for _, area := range flashMap.SortedAreas() {
-		if err := writeFlashMapEntry(area, buf); err != nil {
-			return 0, 0, err
-		}
-	}
-
-	if err := writeZeroHash(buf); err != nil {
-		return 0, 0, err
-	}
-	hashSubOff := buf.Len() - META_HASH_SZ
-
-	if err := writeFooter(buf); err != nil {
-		return 0, 0, err
-	}
-
-	// The meta region gets placed at the very end of the boot loader slot.
-	bootArea, ok := flashMap.Areas[flash.FLASH_AREA_NAME_BOOTLOADER]
-	if !ok {
-		return 0, 0,
-			util.NewNewtError("Required boot loader flash area missing")
-	}
-
-	if bootArea.Size < buf.Len() {
-		return 0, 0, util.FmtNewtError(
-			"Boot loader flash area too small to accommodate meta region; "+
-				"boot=%d meta=%d", bootArea.Size, buf.Len())
-	}
-
-	metaOff := bootArea.Offset + bootArea.Size - buf.Len()
-	for i := metaOff; i < bootArea.Size; i++ {
-		if section0Data[i] != 0xff {
-			return 0, 0, util.FmtNewtError(
-				"Boot loader extends into meta region; "+
-					"meta region starts at offset %d", metaOff)
-		}
-	}
-
-	// Copy the meta region into the manufacturing image.  The meta hash is
-	// still zeroed.
-	copy(section0Data[metaOff:], buf.Bytes())
-
-	return metaOff, metaOff + hashSubOff, nil
-}
-
-// Calculates the SHA256 hash, using the full manufacturing image as input.
-// Hash-calculation algorithm is as follows:
-// 1. Concatenate sections in ascending order of index.
-// 2. Zero out the 32 bytes that will contain the hash.
-// 3. Apply SHA256 to the result.
-//
-// This function assumes that the 32 bytes of hash data have already been
-// zeroed.
-func calcMetaHash(sections [][]byte) []byte {
-	// Concatenate all sections.
-	blob := []byte{}
-	for _, section := range sections {
-		blob = append(blob, section...)
-	}
-
-	// Calculate hash.
-	hash := sha256.Sum256(blob)
-
-	return hash[:]
-}
diff --git a/newt/mfg/mfg.go b/newt/mfg/mfg.go
deleted file mode 100644
index 10c6c8e..0000000
--- a/newt/mfg/mfg.go
+++ /dev/null
@@ -1,98 +0,0 @@
-/**
- * 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 mfg
-
-import (
-	"sort"
-
-	"mynewt.apache.org/newt/artifact/image"
-	"mynewt.apache.org/newt/newt/pkg"
-	"mynewt.apache.org/newt/newt/target"
-	"mynewt.apache.org/newt/newt/toolchain"
-)
-
-type MfgRawEntry struct {
-	device   int
-	offset   int
-	filename string
-	data     []byte
-}
-
-// A chunk of data in the manufacturing image.  Can be a firmware image or a
-// raw entry (contents of a data file).
-type mfgPart struct {
-	device int
-	offset int
-	data   []byte
-	name   string
-}
-
-type MfgImage struct {
-	basePkg *pkg.LocalPackage
-
-	bsp      *pkg.BspPackage
-	compiler *toolchain.Compiler
-
-	boot       *target.Target
-	images     []*target.Target
-	rawEntries []MfgRawEntry
-
-	version image.ImageVersion
-}
-
-func (mi *MfgImage) SetVersion(ver image.ImageVersion) {
-	mi.version = ver
-}
-
-func (mi *MfgImage) imgApps(imageIdx int) (
-	app *pkg.LocalPackage, loader *pkg.LocalPackage) {
-
-	if imageIdx >= len(mi.images) {
-		return
-	}
-
-	t := mi.images[imageIdx]
-	app = t.App()
-	loader = t.Loader()
-	return
-}
-
-func (mi *MfgImage) sectionIds() []int {
-	idMap := map[int]struct{}{}
-
-	// The bootloader and images always go in section 0.
-	idMap[0] = struct{}{}
-
-	for _, entry := range mi.rawEntries {
-		idMap[entry.device] = struct{}{}
-	}
-
-	ids := make([]int, 0, len(idMap))
-	for id, _ := range idMap {
-		ids = append(ids, id)
-	}
-	sort.Ints(ids)
-
-	return ids
-}
-
-func (mi *MfgImage) NumImages() int {
-	return len(mi.images)
-}
diff --git a/newt/mfg/misc.go b/newt/mfg/misc.go
new file mode 100644
index 0000000..73d911d
--- /dev/null
+++ b/newt/mfg/misc.go
@@ -0,0 +1,92 @@
+/**
+ * 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 mfg
+
+import (
+	"strings"
+
+	"mynewt.apache.org/newt/artifact/image"
+	"mynewt.apache.org/newt/newt/builder"
+	"mynewt.apache.org/newt/newt/newtutil"
+	"mynewt.apache.org/newt/newt/pkg"
+)
+
+func loadDecodedMfg(basePath string) (DecodedMfg, error) {
+	yc, err := newtutil.ReadConfig(basePath,
+		strings.TrimSuffix(YAML_FILENAME, ".yml"))
+	if err != nil {
+		return DecodedMfg{}, err
+	}
+
+	dm, err := decodeMfg(yc)
+	if err != nil {
+		return DecodedMfg{}, err
+	}
+
+	return dm, nil
+}
+
+func LoadMfgEmitter(basePkg *pkg.LocalPackage,
+	ver image.ImageVersion) (MfgEmitter, error) {
+
+	dm, err := loadDecodedMfg(basePkg.BasePath())
+	if err != nil {
+		return MfgEmitter{}, err
+	}
+
+	mb, err := newMfgBuilder(basePkg, dm, ver)
+	if err != nil {
+		return MfgEmitter{}, err
+	}
+
+	device, err := mb.calcDevice()
+	if err != nil {
+		return MfgEmitter{}, err
+	}
+
+	me, err := NewMfgEmitter(mb, basePkg.Name(), ver, device)
+	if err != nil {
+		return MfgEmitter{}, err
+	}
+
+	return me, nil
+}
+
+func Upload(basePkg *pkg.LocalPackage) (string, error) {
+	dm, err := loadDecodedMfg(basePkg.BasePath())
+	if err != nil {
+		return "", err
+	}
+
+	mb, err := newMfgBuilder(basePkg, dm, image.ImageVersion{})
+	if err != nil {
+		return "", err
+	}
+
+	envSettings := map[string]string{"MFG_IMAGE": "1"}
+	binPath := MfgBinPath(basePkg.Name())
+	basePath := strings.TrimSuffix(binPath, ".bin")
+
+	if err := builder.Load(basePath, mb.Bsp, envSettings); err != nil {
+		return "", err
+	}
+
+	return binPath, nil
+}
diff --git a/newt/mfg/part.go b/newt/mfg/part.go
new file mode 100644
index 0000000..ca84d29
--- /dev/null
+++ b/newt/mfg/part.go
@@ -0,0 +1,96 @@
+/**
+ * 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 mfg
+
+import (
+	"bytes"
+	"io"
+	"sort"
+
+	"mynewt.apache.org/newt/util"
+)
+
+// A chunk of data in the manufacturing image.  Can be a firmware image or a
+// raw entry (contents of a data file).
+type Part struct {
+	Name   string
+	Offset int
+	Data   []byte
+}
+
+type partSorter struct {
+	parts []Part
+}
+
+func (s partSorter) Len() int {
+	return len(s.parts)
+}
+func (s partSorter) Swap(i, j int) {
+	s.parts[i], s.parts[j] = s.parts[j], s.parts[i]
+}
+func (s partSorter) Less(i, j int) bool {
+	return s.parts[i].Offset < s.parts[j].Offset
+}
+
+func SortParts(parts []Part) []Part {
+	sorter := partSorter{
+		parts: parts,
+	}
+
+	sort.Sort(sorter)
+	return sorter.parts
+}
+
+func WriteParts(parts []Part, w io.Writer, eraseVal byte) (int, error) {
+	off := 0
+	for _, p := range parts {
+		if p.Offset < off {
+			return off, util.FmtNewtError(
+				"Invalid mfg parts: out of order")
+		}
+
+		// Pad the previous block up to the current offset.
+		for off < p.Offset {
+			if _, err := w.Write([]byte{eraseVal}); err != nil {
+				return off, util.ChildNewtError(err)
+			}
+			off++
+		}
+
+		// Write the current block's data.
+		size, err := w.Write(p.Data)
+		if err != nil {
+			return off, util.ChildNewtError(err)
+		}
+		off += size
+	}
+
+	// Note: the final block does not get padded.
+
+	return off, nil
+}
+
+func PartsBytes(parts []Part) ([]byte, error) {
+	b := &bytes.Buffer{}
+	if _, err := WriteParts(parts, b, 0xff); err != nil {
+		return nil, err
+	}
+	return b.Bytes(), nil
+}
diff --git a/newt/mfg/paths.go b/newt/mfg/paths.go
index 04cf448..3b3e1f6 100644
--- a/newt/mfg/paths.go
+++ b/newt/mfg/paths.go
@@ -21,162 +21,42 @@ package mfg
 
 import (
 	"fmt"
-	"path/filepath"
-	"strconv"
 
+	"mynewt.apache.org/newt/artifact/mfg"
 	"mynewt.apache.org/newt/newt/builder"
-	"mynewt.apache.org/newt/newt/pkg"
 )
 
+// Filename containing a manufacturing image definition.
+const YAML_FILENAME string = "mfg.yml"
+
 func MfgBinDir(mfgPkgName string) string {
 	return builder.BinRoot() + "/" + mfgPkgName
 }
 
-func MfgBootDir(mfgPkgName string) string {
-	return MfgBinDir(mfgPkgName) + "/bootloader"
-}
-
-func MfgBootBinPath(mfgPkgName string, appName string) string {
-	return MfgBootDir(mfgPkgName) + "/" + appName + ".elf.bin"
-}
-
-func MfgBootElfPath(mfgPkgName string, appName string) string {
-	return MfgBootDir(mfgPkgName) + "/" + appName + ".elf"
-}
-
-func MfgBootManifestPath(mfgPkgName string, appName string) string {
-	return MfgBootDir(mfgPkgName) + "/manifest.json"
-}
-
-// Image indices start at 0.
-func MfgImageBinDir(mfgPkgName string, imageIdx int) string {
-	return MfgBinDir(mfgPkgName) + "/image" + strconv.Itoa(imageIdx)
-}
-
-func MfgImageImgPath(mfgPkgName string, imageIdx int,
-	appName string) string {
-
-	return MfgImageBinDir(mfgPkgName, imageIdx) + "/" + appName + ".img"
-}
-
-func MfgImageElfPath(mfgPkgName string, imageIdx int,
-	appName string) string {
-
-	return MfgImageBinDir(mfgPkgName, imageIdx) + "/" + appName + ".elf"
-}
-
-func MfgImageManifestPath(mfgPkgName string, imageIdx int) string {
-	return MfgImageBinDir(mfgPkgName, imageIdx) + "/manifest.json"
-}
-
-func MfgSectionBinDir(mfgPkgName string) string {
-	return MfgBinDir(mfgPkgName) + "/sections"
-}
-
-func MfgSectionBinPath(mfgPkgName string, sectionNum int) string {
-	return fmt.Sprintf("%s/%s-s%d.bin", MfgSectionBinDir(mfgPkgName),
-		filepath.Base(mfgPkgName), sectionNum)
-}
-
-func MfgSectionHexPath(mfgPkgName string, sectionNum int) string {
-	return fmt.Sprintf("%s/%s-s%d.hex", MfgSectionBinDir(mfgPkgName),
-		filepath.Base(mfgPkgName), sectionNum)
+func MfgBinPath(mfgPkgName string) string {
+	return MfgBinDir(mfgPkgName) + "/" + mfg.MFG_IMG_FILENAME
 }
 
 func MfgManifestPath(mfgPkgName string) string {
-	return MfgBinDir(mfgPkgName) + "/manifest.json"
+	return MfgBinDir(mfgPkgName) + "/" + mfg.MFG_MANIFEST_FILENAME
 }
 
-func (mi *MfgImage) ManifestPath() string {
-	return MfgManifestPath(mi.basePkg.Name())
+func MfgTargetDir(targetNum int) string {
+	return fmt.Sprintf("targets/%d", targetNum)
 }
 
-func (mi *MfgImage) BootBinPath() string {
-	if mi.boot == nil {
-		return ""
-	}
-
-	return MfgBootBinPath(mi.basePkg.Name(),
-		pkg.ShortName(mi.boot.App()))
-}
-
-func (mi *MfgImage) BootElfPath() string {
-	if mi.boot == nil {
-		return ""
-	}
-
-	return MfgBootElfPath(mi.basePkg.Name(), pkg.ShortName(mi.boot.App()))
-}
-
-func (mi *MfgImage) BootManifestPath() string {
-	if mi.boot == nil {
-		return ""
-	}
-
-	return MfgBootManifestPath(mi.basePkg.Name(),
-		pkg.ShortName(mi.boot.App()))
-}
-
-func (mi *MfgImage) AppImgPath(imageIdx int) string {
-	app, _ := mi.imgApps(imageIdx)
-	if app == nil {
-		return ""
-	}
-
-	return MfgImageImgPath(mi.basePkg.Name(), imageIdx, pkg.ShortName(app))
-}
-
-func (mi *MfgImage) AppElfPath(imageIdx int) string {
-	app, _ := mi.imgApps(imageIdx)
-	if app == nil {
-		return ""
-	}
-
-	return MfgImageElfPath(mi.basePkg.Name(), imageIdx, pkg.ShortName(app))
+func MfgTargetBinPath(targetNum int) string {
+	return fmt.Sprintf("%s/binary.bin", MfgTargetDir(targetNum))
 }
 
-func (mi *MfgImage) LoaderImgPath(imageIdx int) string {
-	_, loader := mi.imgApps(imageIdx)
-	if loader == nil {
-		return ""
-	}
-
-	return MfgImageImgPath(mi.basePkg.Name(), imageIdx, pkg.ShortName(loader))
+func MfgTargetImgPath(targetNum int) string {
+	return fmt.Sprintf("%s/image.img", MfgTargetDir(targetNum))
 }
 
-func (mi *MfgImage) LoaderElfPath(imageIdx int) string {
-	_, loader := mi.imgApps(imageIdx)
-	if loader == nil {
-		return ""
-	}
-
-	return MfgImageElfPath(mi.basePkg.Name(), imageIdx, pkg.ShortName(loader))
+func MfgTargetElfPath(targetNum int) string {
+	return fmt.Sprintf("%s/elf.elf", MfgTargetDir(targetNum))
 }
 
-func (mi *MfgImage) ImageManifestPath(imageIdx int) string {
-	if imageIdx >= len(mi.images) {
-		return ""
-	}
-
-	return MfgImageManifestPath(mi.basePkg.Name(), imageIdx)
-}
-
-func (mi *MfgImage) SectionBinPaths() []string {
-	sectionIds := mi.sectionIds()
-
-	paths := make([]string, len(sectionIds))
-	for i, sectionId := range sectionIds {
-		paths[i] = MfgSectionBinPath(mi.basePkg.Name(), sectionId)
-	}
-	return paths
-}
-
-func (mi *MfgImage) SectionHexPaths() []string {
-	sectionIds := mi.sectionIds()
-
-	paths := make([]string, len(sectionIds))
-	for i, sectionId := range sectionIds {
-		paths[i] = MfgSectionHexPath(mi.basePkg.Name(), sectionId)
-	}
-	return paths
+func MfgTargetManifestPath(targetNum int) string {
+	return fmt.Sprintf("%s/manifest.json", MfgTargetDir(targetNum))
 }