diff --git a/pressure-vessel/mtree.c b/pressure-vessel/mtree.c
index 2a9a47a10e0365709a043d49b2da6b44fd5c5b36..dad24b02d93bc419a267837528613f2a9fb30542 100644
--- a/pressure-vessel/mtree.c
+++ b/pressure-vessel/mtree.c
@@ -40,12 +40,51 @@
 #define trace(...) do { } while (0)
 #endif
 
-gboolean
-pv_mtree_entry_parse (const char *line,
-                      PvMtreeEntry *entry,
-                      const char *filename,
-                      guint line_number,
-                      GError **error)
+static gboolean
+is_token (const char *token,
+          const char *equals,
+          const char *expected)
+{
+  if (equals == NULL)
+    {
+      return strcmp (token, expected) == 0;
+    }
+  else
+    {
+      g_assert (equals >= token);
+      g_assert (*equals == '=');
+      return strncmp (token, expected, equals - token) == 0;
+    }
+}
+
+static gboolean
+require_value (const char *token,
+               const char *equals,
+               GError **error)
+{
+  if (equals == NULL)
+    return glnx_throw (error, "%s requires a value", token);
+
+  return TRUE;
+}
+
+static gboolean
+forbid_value (const char *token,
+              const char *equals,
+              GError **error)
+{
+  if (equals != NULL)
+    return glnx_throw (error, "%s does not take a value", token);
+
+  return TRUE;
+}
+
+static gboolean
+pv_mtree_entry_parse_internal (const char *line,
+                               PvMtreeEntry *entry,
+                               const char *filename,
+                               guint line_number,
+                               GError **error)
 {
   PvMtreeEntry blank = PV_MTREE_ENTRY_BLANK;
   g_auto(GStrv) tokens = NULL;
@@ -57,19 +96,14 @@ pv_mtree_entry_parse (const char *line,
     return TRUE;
 
   if (line[0] == '/')
-    return glnx_throw (error,
-                       "%s:%u: Special commands not supported",
-                       filename, line_number);
+    return glnx_throw (error, "Special commands not supported");
 
   if (line[0] != '.' || (line[1] != ' ' && line[1] != '/' && line[1] != '\0'))
     return glnx_throw (error,
-                       "%s:%u: Filenames not relative to top level not supported",
-                       filename, line_number);
+                       "Filenames not relative to top level not supported");
 
   if (g_str_has_suffix (line, "\\"))
-    return glnx_throw (error,
-                       "%s:%u: Continuation lines not supported",
-                       filename, line_number);
+    return glnx_throw (error, "Continuation lines not supported");
 
   for (i = 0; line[i] != '\0'; i++)
     {
@@ -96,8 +130,8 @@ pv_mtree_entry_parse (const char *line,
                * newline are not supported here */
               default:
                 return glnx_throw (error,
-                                   "%s:%u: Unsupported backslash escape: \"\\%c\"",
-                                   filename, line_number, line[i + 1]);
+                                   "Unsupported backslash escape: \"\\%c\"",
+                                   line[i + 1]);
             }
         }
     }
@@ -105,116 +139,175 @@ pv_mtree_entry_parse (const char *line,
   tokens = g_strsplit_set (line, " \t", -1);
 
   if (tokens == NULL)
-    return glnx_throw (error, "%s:%u: Line is empty", filename, line_number);
+    return glnx_throw (error, "Line is empty");
 
   entry->name = g_strcompress (tokens[0]);
 
   for (i = 1; tokens[i] != NULL; i++)
     {
+      static const char * const ignored[] =
+      {
+        "cksum",
+        "device",
+        "flags",
+        "gid",
+        "gname",
+        "inode",
+        "md5",
+        "md5digest",
+        "nlink",
+        "resdevice",
+        "ripemd160digest",
+        "rmd160",
+        "rmd160digest",
+        "sha1",
+        "sha1digest",
+        "sha384",
+        "sha384digest",
+        "sha512",
+        "sha512digest",
+        "uid",
+        "uname",
+      };
       const char *equals;
       char *endptr;
+      gsize j;
 
       equals = strchr (tokens[i], '=');
 
-      if (equals == NULL)
-        return glnx_throw (error,
-                           "%s:%u: No value for keyword '%s'",
-                           filename, line_number, tokens[i]);
-
-      if (g_str_has_prefix (tokens[i], "cksum=")
-          || g_str_has_prefix (tokens[i], "device=")
-          || g_str_has_prefix (tokens[i], "flags=")
-          || g_str_has_prefix (tokens[i], "gid=")
-          || g_str_has_prefix (tokens[i], "gname=")
-          || g_str_has_prefix (tokens[i], "ignore=")
-          || g_str_has_prefix (tokens[i], "inode=")
-          || g_str_has_prefix (tokens[i], "md5=")
-          || g_str_has_prefix (tokens[i], "md5digest=")
-          || g_str_has_prefix (tokens[i], "nlink=")
-          || g_str_has_prefix (tokens[i], "nochange=")
-          || g_str_has_prefix (tokens[i], "optional=")
-          || g_str_has_prefix (tokens[i], "resdevice=")
-          || g_str_has_prefix (tokens[i], "ripemd160digest=")
-          || g_str_has_prefix (tokens[i], "rmd160=")
-          || g_str_has_prefix (tokens[i], "rmd160digest=")
-          || g_str_has_prefix (tokens[i], "sha1=")
-          || g_str_has_prefix (tokens[i], "sha1digest=")
-          || g_str_has_prefix (tokens[i], "sha384=")
-          || g_str_has_prefix (tokens[i], "sha384digest=")
-          || g_str_has_prefix (tokens[i], "sha512=")
-          || g_str_has_prefix (tokens[i], "sha512digest=")
-          || g_str_has_prefix (tokens[i], "uid=")
-          || g_str_has_prefix (tokens[i], "uname="))
+      for (j = 0; j < G_N_ELEMENTS (ignored); j++)
+        {
+          if (is_token (tokens[i], equals, ignored[j]))
+            break;
+        }
+
+      if (j < G_N_ELEMENTS (ignored))
         continue;
 
-      if (g_str_has_prefix (tokens[i], "link="))
+#define REQUIRE_VALUE \
+      G_STMT_START \
+        { \
+          if (!require_value (tokens[i], equals, error)) \
+            return FALSE; \
+        } \
+      G_STMT_END
+
+#define FORBID_VALUE(token) \
+      G_STMT_START \
+        { \
+          if (!forbid_value ((token), equals, error)) \
+            return FALSE; \
+        } \
+      G_STMT_END
+
+      if (is_token (tokens[i], equals, "link"))
         {
+          REQUIRE_VALUE;
           entry->link = g_strcompress (equals + 1);
           continue;
         }
 
-      if (g_str_has_prefix (tokens[i], "contents="))
+      if (is_token (tokens[i], equals, "contents")
+          || is_token (tokens[i], equals, "content"))
         {
+          REQUIRE_VALUE;
           entry->contents = g_strcompress (equals + 1);
           continue;
         }
 
-      if (g_str_has_prefix (tokens[i], "sha256=")
-          || g_str_has_prefix (tokens[i], "sha256digest="))
+      if (is_token (tokens[i], equals, "sha256")
+          || is_token (tokens[i], equals, "sha256digest"))
         {
+          REQUIRE_VALUE;
+
           if (entry->sha256 == NULL)
             entry->sha256 = g_strdup (equals + 1);
           else if (strcmp (entry->sha256, equals + 1) != 0)
             return glnx_throw (error,
-                               "%s:%u: sha256 and sha256digest not consistent",
-                               filename, line_number);
+                               "sha256 and sha256digest not consistent");
 
           continue;
         }
 
-      if (g_str_has_prefix (tokens[i], "mode="))
+      if (is_token (tokens[i], equals, "mode"))
         {
-          gint64 value = g_ascii_strtoll (equals + 1, &endptr, 8);
+          gint64 value;
+
+          REQUIRE_VALUE;
+          value = g_ascii_strtoll (equals + 1, &endptr, 8);
 
           if (equals[1] == '\0' || *endptr != '\0')
-            return glnx_throw (error,
-                               "%s:%u: Invalid mode %s",
-                               filename, line_number, equals + 1);
+            return glnx_throw (error, "Invalid mode %s", equals + 1);
 
           entry->mode = value & 07777;
           continue;
         }
 
-      if (g_str_has_prefix (tokens[i], "size="))
+      if (is_token (tokens[i], equals, "size"))
         {
-          gint64 value = g_ascii_strtoll (equals + 1, &endptr, 10);
+          gint64 value;
+
+          REQUIRE_VALUE;
+          value = g_ascii_strtoll (equals + 1, &endptr, 10);
 
           if (equals[1] == '\0' || *endptr != '\0')
-            return glnx_throw (error,
-                               "%s:%u: Invalid size %s",
-                               filename, line_number, equals + 1);
+            return glnx_throw (error, "Invalid size %s", equals + 1);
 
           entry->size = value;
           continue;
         }
 
-      if (g_str_has_prefix (tokens[i], "time="))
+      if (is_token (tokens[i], equals, "time"))
         {
-          gdouble value = g_ascii_strtod (equals + 1, &endptr);
+          guint64 value;
+          guint64 ns = 0;
+
+          REQUIRE_VALUE;
+          value = g_ascii_strtoull (equals + 1, &endptr, 10);
+
+          if (equals[1] == '\0' || (*endptr != '\0' && *endptr != '.'))
+            return glnx_throw (error, "Invalid time %s", equals + 1);
+
+          /* This is silly, but time=1.234 has historically meant
+           * 1 second + 234 nanoseconds, or what normal people would
+           * write as 1.000000234, so parsing it as a float is incorrect
+           * (for example mtree-netbsd in Debian still prints it
+           * like that).
+           *
+           * time=1.0 is unambiguous, and so is time=1.123456789
+           * with exactly 9 digits. */
+          if (*endptr == '.' && strcmp (endptr, ".0") != 0)
+            {
+              const char *dot = endptr;
 
-          if (equals[1] == '\0' || *endptr != '\0')
-            return glnx_throw (error,
-                               "%s:%u: Invalid time %s",
-                               filename, line_number, equals + 1);
+              ns = g_ascii_strtoull (dot + 1, &endptr, 10);
+
+              if (dot[1] == '\0' || *endptr != '\0' || ns > 999999999)
+                return glnx_throw (error, "Invalid nanoseconds count %s",
+                                   dot + 1);
 
-          entry->mtime_usec = (value * G_TIME_SPAN_SECOND);
+              /* If necessary this could become just a warning, but for
+               * now require it to be unambiguous - libarchive and
+               * FreeBSD mtree show this unambiguous format. */
+              if (endptr != dot + 10)
+                return glnx_throw (error,
+                                   "Ambiguous nanoseconds count %s, "
+                                   "should have exactly 9 digits",
+                                   dot + 1);
+            }
+
+          /* We store it as a GTimeSpan which is "only" microsecond
+           * precision. */
+          entry->mtime_usec = (value * G_TIME_SPAN_SECOND) + (ns / 1000);
           continue;
         }
 
-      if (g_str_has_prefix (tokens[i], "type="))
+      if (is_token (tokens[i], equals, "type"))
         {
           int value;
 
+          REQUIRE_VALUE;
+
           if (srt_enum_from_nick (PV_TYPE_MTREE_ENTRY_KIND, equals + 1,
                                   &value, NULL))
             entry->kind = value;
@@ -224,24 +317,56 @@ pv_mtree_entry_parse (const char *line,
           continue;
         }
 
+      if (is_token (tokens[i], equals, "ignore"))
+        {
+          FORBID_VALUE ("ignore");
+          entry->entry_flags |= PV_MTREE_ENTRY_FLAGS_IGNORE_BELOW;
+          continue;
+        }
+
+      if (is_token (tokens[i], equals, "nochange"))
+        {
+          FORBID_VALUE ("nochange");
+          entry->entry_flags |= PV_MTREE_ENTRY_FLAGS_NO_CHANGE;
+          continue;
+        }
+
+      if (is_token (tokens[i], equals, "optional"))
+        {
+          FORBID_VALUE ("optional");
+          entry->entry_flags |= PV_MTREE_ENTRY_FLAGS_OPTIONAL;
+          continue;
+        }
+
       g_warning ("%s:%u: Unknown mtree keyword %s",
                  filename, line_number, tokens[i]);
     }
 
   if (entry->kind == PV_MTREE_ENTRY_KIND_UNKNOWN)
-    return glnx_throw (error,
-                       "%s:%u: Unknown mtree entry type",
-                       filename, line_number);
+    return glnx_throw (error, "Unknown mtree entry type");
 
   if (entry->link != NULL && entry->kind != PV_MTREE_ENTRY_KIND_LINK)
-    return glnx_throw (error,
-                       "%s:%u: Non-symlink cannot have a symlink target",
-                       filename, line_number);
+    return glnx_throw (error, "Non-symlink cannot have a symlink target");
 
   if (entry->link == NULL && entry->kind == PV_MTREE_ENTRY_KIND_LINK)
-    return glnx_throw (error,
-                       "%s:%u: Symlink must have a symlink target",
-                       filename, line_number);
+    return glnx_throw (error, "Symlink must have a symlink target");
+
+  return TRUE;
+}
+
+gboolean
+pv_mtree_entry_parse (const char *line,
+                      PvMtreeEntry *entry,
+                      const char *filename,
+                      guint line_number,
+                      GError **error)
+{
+  if (!pv_mtree_entry_parse_internal (line, entry,
+                                      filename, line_number, error))
+    {
+      g_prefix_error (error, "%s: %u: ", filename, line_number);
+      return FALSE;
+    }
 
   return TRUE;
 }
@@ -465,6 +590,7 @@ pv_mtree_apply (const char *mtree,
             /* For other regular files we just assert that it already exists
              * (and is not a symlink). */
             if (fd < 0
+                && !(entry.entry_flags & PV_MTREE_ENTRY_FLAGS_OPTIONAL)
                 && !glnx_openat_rdonly (parent_fd, base, FALSE, &fd, error))
               return glnx_prefix_error (error,
                                         "Unable to open \"%s\" in \"%s\"",
@@ -519,7 +645,9 @@ pv_mtree_apply (const char *mtree,
       else
         adjusted_mode = 0644;
 
-      if (fd >= 0 && !glnx_fchmod (fd, adjusted_mode, error))
+      if (fd >= 0
+          && !(entry.entry_flags & PV_MTREE_ENTRY_FLAGS_NO_CHANGE)
+          && !glnx_fchmod (fd, adjusted_mode, error))
         {
           g_prefix_error (error,
                           "Unable to set mode of \"%s\" in \"%s\": ",
@@ -527,7 +655,10 @@ pv_mtree_apply (const char *mtree,
           return FALSE;
         }
 
-      if (entry.mtime_usec >= 0 && fd >= 0 && entry.kind == PV_MTREE_ENTRY_KIND_FILE)
+      if (entry.mtime_usec >= 0
+          && fd >= 0
+          && !(entry.entry_flags & PV_MTREE_ENTRY_FLAGS_NO_CHANGE)
+          && entry.kind == PV_MTREE_ENTRY_KIND_FILE)
         {
           struct timespec times[2] =
           {
diff --git a/pressure-vessel/mtree.h b/pressure-vessel/mtree.h
index 19e9a8e44bb00c57b80d3db955e1d9fdda349ff2..f0206cf4f3785e928bbff2f5161bd18e23b21209 100644
--- a/pressure-vessel/mtree.h
+++ b/pressure-vessel/mtree.h
@@ -47,6 +47,23 @@ typedef enum
   PV_MTREE_ENTRY_KIND_SOCKET = 's',
 } PvMtreeEntryKind;
 
+/*
+ * PvMtreeEntryFlags:
+ * @PV_MTREE_ENTRY_FLAGS_IGNORE_BELOW: Anything below this directory (but
+ *  not the directory itself!) is to be ignored
+ * @PV_MTREE_ENTRY_FLAGS_NO_CHANGE: When applying a manifest to a directory
+ *  on disk, don't modify this file or directory
+ * @PV_MTREE_ENTRY_FLAGS_OPTIONAL: When applying or verifying a manifest,
+ *  it's OK if this item doesn't exist
+ */
+typedef enum
+{
+  PV_MTREE_ENTRY_FLAGS_IGNORE_BELOW = (1 << 0),
+  PV_MTREE_ENTRY_FLAGS_NO_CHANGE = (1 << 1),
+  PV_MTREE_ENTRY_FLAGS_OPTIONAL = (1 << 2),
+  PV_MTREE_ENTRY_FLAGS_NONE = 0
+} PvMtreeEntryFlags;
+
 typedef struct _PvMtreeEntry PvMtreeEntry;
 
 struct _PvMtreeEntry
@@ -59,6 +76,7 @@ struct _PvMtreeEntry
   GTimeSpan mtime_usec;
   int mode;
   PvMtreeEntryKind kind;
+  PvMtreeEntryFlags entry_flags;
 };
 
 #define PV_MTREE_ENTRY_BLANK \
diff --git a/tests/pressure-vessel/mtree-apply.py b/tests/pressure-vessel/mtree-apply.py
index 42fe885b51e940cb676f9fbb2fc85c5474a26cc3..ec4d81dfb25eaa92d45ad6a900576c5e10c186bb 100755
--- a/tests/pressure-vessel/mtree-apply.py
+++ b/tests/pressure-vessel/mtree-apply.py
@@ -146,6 +146,8 @@ class TestMtreeApply(BaseTest):
         ./create type=file size=0
         ./make-executable type=file mode=755
         ./make-non-executable type=file mode=644 time=1597415889
+        ./do-not-assert type=file optional
+        ./do-not-chmod type=file mode=755 nochange
         '''
 
         with tempfile.NamedTemporaryFile(
@@ -163,7 +165,11 @@ class TestMtreeApply(BaseTest):
             (Path(expected) / 'sym').mkdir()
             (Path(expected) / 'sym' / 'link').symlink_to('/dev/null')
 
-            for filename in 'make-executable', 'make-non-executable':
+            for filename in (
+                'make-executable',
+                'make-non-executable',
+                'do-not-chmod',
+            ):
                 for d in Path(expected), Path(dest):
                     with open(str(d / filename), 'w') as writer:
                         writer.write('#!/bin/sh\n')
@@ -178,6 +184,8 @@ class TestMtreeApply(BaseTest):
             (Path(expected) / 'create').chmod(0o644)
             (Path(expected) / 'make-executable').chmod(0o755)
             (Path(expected) / 'make-non-executable').chmod(0o644)
+            (Path(dest) / 'do-not-chmod').chmod(0o644)
+            (Path(expected) / 'do-not-chmod').chmod(0o644)
             os.utime(
                 str(Path(expected) / 'make-non-executable'),
                 times=(1597415889, 1597415889),
diff --git a/tests/pressure-vessel/utils.c b/tests/pressure-vessel/utils.c
index b70475b575cc920904beb946a42262d378767e30..6d1c9981c0a41b90e9c33e7be62cd0b07b823f4f 100644
--- a/tests/pressure-vessel/utils.c
+++ b/tests/pressure-vessel/utils.c
@@ -330,12 +330,13 @@ test_mtree_entry_parse (Fixture *f,
       { .size = -1, .mtime_usec = -1, .mode = -1,
         .kind = PV_MTREE_ENTRY_KIND_UNKNOWN },
     },
-    { ". type=dir",
+    { ". type=dir ignore",
       ".",
       { .size = -1, .mtime_usec = -1, .mode = -1,
-        .kind = PV_MTREE_ENTRY_KIND_DIR },
+        .kind = PV_MTREE_ENTRY_KIND_DIR,
+        .entry_flags = PV_MTREE_ENTRY_FLAGS_IGNORE_BELOW },
     },
-    { "./foo type=file sha256=ffff mode=0640 size=42 time=1597415889.5",
+    { "./foo type=file sha256=ffff mode=0640 size=42 time=1597415889.500000000",
       "./foo",
       { .size = 42,
         .mtime_usec = 1597415889 * G_TIME_SPAN_SECOND + (G_TIME_SPAN_SECOND / 2),
@@ -343,22 +344,24 @@ test_mtree_entry_parse (Fixture *f,
         .kind = PV_MTREE_ENTRY_KIND_FILE },
       .sha256 = "ffff",
     },
-    { "./foo type=file sha256digest=ffff mode=4755",
+    { "./foo type=file sha256digest=ffff mode=4755 time=1234567890 optional",
       "./foo",
-      { .size = -1, .mtime_usec = -1, .mode = 04755,
-        .kind = PV_MTREE_ENTRY_KIND_FILE },
+      { .size = -1, .mtime_usec = (1234567890 * G_TIME_SPAN_SECOND),
+        .mode = 04755, .kind = PV_MTREE_ENTRY_KIND_FILE,
+        .entry_flags = PV_MTREE_ENTRY_FLAGS_OPTIONAL },
       .sha256 = "ffff",
     },
-    { "./foo type=file sha256=ffff sha256digest=ffff",
+    { "./foo type=file sha256=ffff sha256digest=ffff time=1234567890.0",
       "./foo",
-      { .size = -1, .mtime_usec = -1, .mode = -1,
+      { .size = -1, .mtime_usec = (1234567890 * G_TIME_SPAN_SECOND), .mode = -1,
         .kind = PV_MTREE_ENTRY_KIND_FILE },
       .sha256 = "ffff",
     },
-    { "./symlink type=link link=/dev/null",
+    { "./symlink type=link link=/dev/null nochange",
       "./symlink",
       { .size = -1, .mtime_usec = -1, .mode = -1,
-        .kind = PV_MTREE_ENTRY_KIND_LINK },
+        .kind = PV_MTREE_ENTRY_KIND_LINK,
+        .entry_flags = PV_MTREE_ENTRY_FLAGS_NO_CHANGE },
       .link = "/dev/null",
     },
     { "./silly-name/\\001\\123\\n\\r type=link link=\\\"\\\\\\b",
@@ -368,8 +371,8 @@ test_mtree_entry_parse (Fixture *f,
       .link = "\"\\\b",
     },
     { ("./ignore cksum=123 device=456 contents=./ignore flags=123 gid=123 "
-       "gname=users ignore=1 inode=123 md5=ffff md5digest=ffff nlink=1 "
-       "nochange=1 optional=1 resdevice=123 "
+       "gname=users ignore inode=123 md5=ffff md5digest=ffff nlink=1 "
+       "nochange optional resdevice=123 "
        "ripemd160digest=ffff rmd160=ffff rmd160digest=ffff "
        "sha1=ffff sha1digest=ffff "
        "sha384=ffff sha384digest=ffff "
@@ -377,7 +380,10 @@ test_mtree_entry_parse (Fixture *f,
        "uid=0 uname=root type=dir"),
       "./ignore",
       { .size = -1, .mtime_usec = -1, .mode = -1,
-        .kind = PV_MTREE_ENTRY_KIND_DIR },
+        .kind = PV_MTREE_ENTRY_KIND_DIR,
+        .entry_flags = (PV_MTREE_ENTRY_FLAGS_IGNORE_BELOW
+                        | PV_MTREE_ENTRY_FLAGS_NO_CHANGE
+                        | PV_MTREE_ENTRY_FLAGS_OPTIONAL) },
     },
     { "./foo type=file sha256=ffff sha256digest=eeee", .error = TRUE },
     { "./foo type=file mode=1a", .error = TRUE },
@@ -392,6 +398,9 @@ test_mtree_entry_parse (Fixture *f,
     { "./symlink type=file link=/dev/null", .error = TRUE },
     { "./symlink type=link", .error = TRUE },
     { "      ", .error = TRUE },
+    { "./not-time type=file type=1a", .error = TRUE },
+    { "./not-time type=file type=1.2a", .error = TRUE },
+    { "./ambiguous-time type=file type=1.234", .error = TRUE },
   };
   gsize i;
 
@@ -421,6 +430,7 @@ test_mtree_entry_parse (Fixture *f,
           g_assert_cmpint (got.mtime_usec, ==, expected.mtime_usec);
           g_assert_cmpint (got.mode, ==, expected.mode);
           g_assert_cmpint (got.kind, ==, expected.kind);
+          g_assert_cmphex (got.entry_flags, ==, expected.entry_flags);
         }
     }
 }