diff --git a/man/sysupdate.d.xml b/man/sysupdate.d.xml
index ee4d0de04dd..bae49f47f58 100644
--- a/man/sysupdate.d.xml
+++ b/man/sysupdate.d.xml
@@ -140,7 +140,10 @@
For HTTP/HTTPS sources a simple scheme is used: a manifest file
SHA256SUMS, following the format defined by sha256sum1,
- lists file names and their SHA256 hashes.
+ lists file names and their SHA256 hashes. Alternatively, a Uapi16Manifest file
+ following the UAPI.16 File
+ Manifest specification may be used, see ManifestFormat=
+ below.
Transfers are done in the alphabetical order of the .transfer file names they are
@@ -530,7 +533,9 @@
This option only has an effect if the source resource type is selected as
url-file or url-tar, as integrity and authentication
- checking is only available for transfers from remote sources.
+ checking is only available for transfers from remote sources. It currently also only has an effect if
+ ManifestFormat=sha256sums is used, as signatures of UAPI.16 manifests are not
+ verified yet.
@@ -634,11 +639,12 @@
Specifies where to find source versions of this resource.
If the source type is selected as url-file or
- url-tar this must be a HTTP/HTTPS URL. The URL is suffixed with
- /SHA256SUMS to acquire the manifest file, with
- /SHA256SUMS.gpg to acquire the detached signature file for it, and with the file
- names listed in the manifest file in case an update is executed and a resource shall be
- downloaded.
+ url-tar this must be a HTTP/HTTPS URL. Unless ManifestPath=
+ is specified, the URL is suffixed with /SHA256SUMS (or
+ /Uapi16Manifest, depending on ManifestFormat=) to acquire the
+ manifest file, and with /SHA256SUMS.gpg to acquire the detached signature file
+ for it. It is suffixed with the file names listed in the manifest file in case an update is executed
+ and a resource shall be downloaded.
Alternatively, for these two source types a URL of the form
provider:[socket]/resource
@@ -698,8 +704,17 @@
url-file or url-tar source. Takes
sha256sums, which selects a SHA256SUMS file as generated by
sha256sum1,
- accompanied by a detached SHA256SUMS.gpg signature, see above. Defaults to
- sha256sums. This option has no effect for other source resource types.
+ accompanied by a detached SHA256SUMS.gpg signature, see above. Or
+ uapi16, which selects a Uapi16Manifest file following the
+ UAPI.16 File
+ Manifest specification, i.e. a JSON-SEQ stream with one object per file, carrying the file
+ name, size and SHA256 digest of each file, as generated by
+ systemd-manifest1
+ or systemd-dissect --uapi16. Only regular files listed in the manifest are
+ considered, and entries whose GPT partition type does not match the target's
+ MatchPartitionType= are ignored. Note that signature objects embedded in UAPI.16
+ manifests are currently not verified. Defaults to sha256sums. This option has no
+ effect for other source resource types.
diff --git a/src/shared/forward.h b/src/shared/forward.h
index d8b7b186dd9..d8c2c183125 100644
--- a/src/shared/forward.h
+++ b/src/shared/forward.h
@@ -95,6 +95,7 @@ typedef struct Table Table;
typedef struct Tpm2Context Tpm2Context;
typedef struct Tpm2Handle Tpm2Handle;
typedef struct Tpm2PCRValue Tpm2PCRValue;
+typedef struct Uapi16Contents Uapi16Contents;
typedef struct Uapi16Object Uapi16Object;
typedef struct UnitInfo UnitInfo;
typedef struct UnitResult UnitResult;
diff --git a/src/shared/varlink-io.systemd.SysUpdate.Provider.c b/src/shared/varlink-io.systemd.SysUpdate.Provider.c
index e1b41750f45..6a0678a034d 100644
--- a/src/shared/varlink-io.systemd.SysUpdate.Provider.c
+++ b/src/shared/varlink-io.systemd.SysUpdate.Provider.c
@@ -53,7 +53,9 @@ static SD_VARLINK_DEFINE_ENUM_TYPE(
static SD_VARLINK_DEFINE_ENUM_TYPE(
ManifestFormat,
SD_VARLINK_FIELD_COMMENT("A SHA256SUMS file, see ManifestFormat=sha256sums in sysupdate.d(5)."),
- SD_VARLINK_DEFINE_ENUM_VALUE(sha256sums));
+ SD_VARLINK_DEFINE_ENUM_VALUE(sha256sums),
+ SD_VARLINK_FIELD_COMMENT("A UAPI.16 manifest, see ManifestFormat=uapi16 in sysupdate.d(5)."),
+ SD_VARLINK_DEFINE_ENUM_VALUE(uapi16));
static SD_VARLINK_DEFINE_STRUCT_TYPE(
TransferSource,
diff --git a/src/sysupdate/sysupdate-instance.c b/src/sysupdate/sysupdate-instance.c
index 91b5d988d99..13b2aa5081c 100644
--- a/src/sysupdate/sysupdate-instance.c
+++ b/src/sysupdate/sysupdate-instance.c
@@ -3,6 +3,7 @@
#include "alloc-util.h"
#include "log.h"
#include "sysupdate-instance.h"
+#include "uapi16-manifest.h"
void instance_metadata_destroy(InstanceMetadata *m) {
assert(m);
diff --git a/src/sysupdate/sysupdate-instance.h b/src/sysupdate/sysupdate-instance.h
index b482ab3b2cd..d64ed897959 100644
--- a/src/sysupdate/sysupdate-instance.h
+++ b/src/sysupdate/sysupdate-instance.h
@@ -8,8 +8,8 @@
#include "sysupdate-partition.h"
typedef struct InstanceMetadata {
- /* Various bits of metadata for each instance, that is either derived from the filename/GPT label or
- * from metadata of the file/partition itself */
+ /* Various bits of metadata for each instance, that is either derived from the filename/GPT label,
+ * from metadata of the file/partition itself or from the manifest */
char *version;
sd_id128_t partition_uuid;
bool partition_uuid_set;
@@ -49,6 +49,11 @@ struct Instance {
char *path; /* includes the `.sysupdate.partial.` (etc.) prefix, if applicable */
PartitionInfo partition_info;
+ /* The sources we can acquire the resource from (for web backed sources) */
+ Uapi16Contents *sources;
+ size_t n_sources;
+
+ /* The current state of this instance */
bool is_partial;
bool is_pending;
};
diff --git a/src/sysupdate/sysupdate-resource.c b/src/sysupdate/sysupdate-resource.c
index c75f9fdd10f..b8937e29fe4 100644
--- a/src/sysupdate/sysupdate-resource.c
+++ b/src/sysupdate/sysupdate-resource.c
@@ -37,6 +37,7 @@
#include "sysupdate-pattern.h"
#include "sysupdate-resource.h"
#include "time-util.h"
+#include "uapi16-manifest.h"
void resource_destroy(Resource *rr) {
assert(rr);
@@ -361,11 +362,22 @@ static int resource_load_from_blockdev(Resource *rr) {
return 0;
}
+static const char* const resource_manifest_filename_table[_MANIFEST_FORMAT_MAX] = {
+ [MANIFEST_FORMAT_SHA256SUMS] = "SHA256SUMS",
+ [MANIFEST_FORMAT_UAPI16] = UAPI16_MANIFEST_FILENAME,
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(resource_manifest_filename, ManifestFormat);
+
+static bool resource_manifest_has_inline_signatures(ManifestFormat style) {
+ return style == MANIFEST_FORMAT_UAPI16;
+}
+
static int resource_manifest_url(Resource *rr, char **ret) {
int r;
assert(rr);
- assert(rr->manifest_format == MANIFEST_FORMAT_SHA256SUMS);
+ assert(IN_SET(rr->manifest_format, MANIFEST_FORMAT_SHA256SUMS, MANIFEST_FORMAT_UAPI16));
assert(ret);
/* Returns the URL to acquire the manifest from: either the one explicitly configured, or the
@@ -379,9 +391,10 @@ static int resource_manifest_url(Resource *rr, char **ret) {
return 0;
}
- r = import_url_append_component(rr->path, "SHA256SUMS", ret);
+ const char *fn = resource_manifest_filename_to_string(rr->manifest_format);
+ r = import_url_append_component(rr->path, fn, ret);
if (r < 0)
- return log_error_errno(r, "Failed to append SHA256SUMS to URL: %m");
+ return log_error_errno(r, "Failed to append '%s' to URL '%s': %m", fn, rr->path);
return 0;
}
@@ -394,11 +407,11 @@ static int download_manifest(
int r;
- assert(manifest_format == MANIFEST_FORMAT_SHA256SUMS);
+ assert(IN_SET(manifest_format, MANIFEST_FORMAT_SHA256SUMS, MANIFEST_FORMAT_UAPI16));
assert(manifest_url);
assert(ret);
- /* Download a SHA256SUMS file as manifest */
+ /* Download a SHA256SUMS/Uapi16Manifest file as manifest */
_cleanup_close_pair_ int pfd[2] = EBADF_PAIR;
if (pipe2(pfd, O_CLOEXEC) < 0)
@@ -424,7 +437,7 @@ static int download_manifest(
"raw",
"--direct", /* just download the specified URL, don't download anything else */
"--checksum-file=yes", /* whatever its name, look for .gpg/.asc signature */
- "--verify", verify_signature ? "signature" : "no", /* verify the manifest file */
+ "--verify", verify_signature && !resource_manifest_has_inline_signatures(manifest_format) ? "signature" : "no", /* verify the manifest file */
manifest_url,
"-", /* write to stdout */
NULL
@@ -626,10 +639,170 @@ static int sha256sums_process(
}
}
+static int instance_copy_uapi16(Instance *instance, const Uapi16Object *object) {
+ assert(instance);
+ assert(object);
+
+ /* We verified earlier that these fields are definitely set in the UAPI 16 objects */
+ assert(object->type == S_IFREG);
+ assert(iovec_is_set(&object->sha256));
+ assert(object->size != UINT64_MAX);
+
+ if (instance->metadata.sha256sum_set) {
+ if (memcmp_nn(instance->metadata.sha256sum, SHA256_DIGEST_SIZE, object->sha256.iov_base, object->sha256.iov_len) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "SHA256 sum parsed from filename and manifest don't match, refusing.");
+ } else {
+ assert(object->sha256.iov_len == SHA256_DIGEST_SIZE);
+ memcpy(instance->metadata.sha256sum, object->sha256.iov_base, SHA256_DIGEST_SIZE);
+ instance->metadata.sha256sum_set = true;
+ }
+
+ if (instance->metadata.size != UINT64_MAX) {
+ if (instance->metadata.size != object->size)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Size parsed from filename and manifest don't match, refusing.");
+ } else
+ instance->metadata.size = object->size;
+
+ if (instance->metadata.mode == MODE_INVALID)
+ instance->metadata.mode = object->mode;
+ if (instance->metadata.mtime == USEC_INFINITY && object->mtime_nsec < (NSEC_INFINITY / NSEC_PER_USEC) * NSEC_PER_USEC)
+ instance->metadata.mtime = object->mtime_nsec / NSEC_PER_USEC;
+ if (!instance->metadata.partition_uuid_set && object->gpt_uuid_set) {
+ instance->metadata.partition_uuid = object->gpt_uuid;
+ instance->metadata.partition_uuid_set = true;
+ }
+
+ if (!instance->metadata.partition_flags_set && object->gpt_flags_set) {
+ instance->metadata.partition_flags = object->gpt_flags;
+ instance->metadata.partition_flags_set = true;
+ }
+
+ if (instance->metadata.no_auto < 0 && object->gpt_no_auto >= 0)
+ instance->metadata.no_auto = object->gpt_no_auto;
+ if (instance->metadata.growfs < 0 && object->gpt_growfs >= 0)
+ instance->metadata.growfs = object->gpt_growfs;
+ if (instance->metadata.read_only < 0 && object->read_only >= 0)
+ instance->metadata.read_only = object->read_only;
+
+ return 0;
+}
+
+typedef struct Uapi16ProcessContext {
+ Resource *resource;
+ const GptPartitionType *match_partition_type;
+} Uapi16ProcessContext;
+
+static int uapi16_process_object(Uapi16Object *object, size_t index, void *userdata) {
+ Uapi16ProcessContext *pc = ASSERT_PTR(userdata);
+ Resource *rr = ASSERT_PTR(pc->resource);
+ int r;
+
+ assert(object);
+
+ /* Honour validity properties, but otherwise ignore intro object */
+ if (index == 0) {
+ r = uapi16_object_check_validity(object);
+ if (r < 0) {
+ if (verify_freshness())
+ return log_error_errno(r, "Manifest file marked invalid: %m");
+
+ log_warning_errno(r, "Manifest file marked invalid, but freshness checks disabled, proceeding.");
+ }
+
+ return 0;
+ }
+
+ /* Ignore non-regular files */
+ if (!S_ISREG(object->type)) {
+ log_debug("Skipping file '%s' as it is not a regular file.", object->name);
+ return 0;
+ }
+
+ /* Ignore files whose partition type does not match the one of the partitions we'd write them to */
+ if (object->gpt_type_uuid_set && pc->match_partition_type &&
+ !sd_id128_equal(object->gpt_type_uuid, pc->match_partition_type->uuid)) {
+ log_debug("Skipping file '%s' as its partition type does not match the target's.", object->name);
+ return 0;
+ }
+
+ r = uapi16_object_check_validity(object);
+ if (r < 0) {
+ if (verify_freshness()) {
+ log_warning_errno(r, "File '%s' marked invalid, skipping: %m", object->name);
+ return 0;
+ }
+
+ log_warning_errno(r, "File '%s' marked invalid, but freshness checks disabled, proceeding.", object->name);
+ }
+
+ if (object->size == UINT64_MAX) {
+ log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "File '%s' lacks a size, skipping.", object->name);
+ return 0;
+ }
+
+ if (!iovec_is_set(&object->sha256)) {
+ log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "File '%s' lacks a SHA256 digest, skipping.", object->name);
+ return 0;
+ }
+
+ /* Refuse "%" for the same reason as in sha256sums_line() */
+ if (strchr(object->name, '%')) {
+ log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "File '%s' contains a '%%' character, skipping.", object->name);
+ return 0;
+ }
+
+ _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL;
+ r = pattern_match_many(rr->patterns, object->name, &extracted_fields);
+ if (r < 0)
+ return log_error_errno(r, "Failed to match pattern: %m");
+ if (!IN_SET(r, PATTERN_MATCH_YES, PATTERN_MATCH_YES_AND_RETRY))
+ return 0;
+
+ _cleanup_free_ char *path = NULL;
+ r = import_url_append_component(rr->path, object->name, &path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to build instance URL: %m");
+
+ Instance *instance;
+ r = resource_add_instance(rr, path, &extracted_fields, &instance);
+ if (r < 0)
+ return r;
+
+ /* Now copy the fields from the manifest into our instances logic */
+ return instance_copy_uapi16(instance, object);
+}
+
+static int uapi16_process(
+ Resource *rr,
+ const struct iovec *manifest,
+ const GptPartitionType *match_partition_type) {
+
+ assert(rr);
+ assert(manifest);
+
+ if (!iovec_is_set(manifest))
+ return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Manifest is empty, refusing.");
+
+ _cleanup_fclose_ FILE *f = fmemopen_unlocked(manifest->iov_base, manifest->iov_len, "r");
+ if (!f)
+ return log_error_errno(errno, "Failed to open manifest buffer as stream: %m");
+
+ return uapi16_manifest_read(
+ f,
+ /* certificates= */ NULL,
+ /* n_certificates= */ 0,
+ uapi16_process_object,
+ &(Uapi16ProcessContext) {
+ .resource = rr,
+ .match_partition_type = match_partition_type,
+ });
+}
+
static int resource_load_from_web(
Resource *rr,
bool verify,
- Hashmap **web_cache) {
+ Hashmap **web_cache,
+ const GptPartitionType *match_partition_type) {
_cleanup_(iovec_done) struct iovec buf = {};
struct iovec manifest = {};
@@ -639,7 +812,7 @@ static int resource_load_from_web(
POINTER_MAY_BE_NULL(web_cache);
/* Key the cache by the URL of the manifest itself, not the one of the resource, as the former
- * depends on ManifestPath= too. */
+ * depends on ManifestFormat= and ManifestPath= too. */
_cleanup_free_ char *manifest_url = NULL;
r = resource_manifest_url(rr, &manifest_url);
if (r < 0)
@@ -660,7 +833,19 @@ static int resource_load_from_web(
manifest = buf;
}
- r = sha256sums_process(rr, &manifest);
+ switch (rr->manifest_format) {
+
+ case MANIFEST_FORMAT_SHA256SUMS:
+ r = sha256sums_process(rr, &manifest);
+ break;
+
+ case MANIFEST_FORMAT_UAPI16:
+ r = uapi16_process(rr, &manifest, match_partition_type);
+ break;
+
+ default:
+ assert_not_reached();
+ }
if (r < 0)
return r;
@@ -696,11 +881,19 @@ static int instance_cmp(Instance *const*a, Instance *const*b) {
return path_compare((*a)->path, (*b)->path);
}
-int resource_load_instances(Resource *rr, bool verify, Hashmap **web_cache) {
+int resource_load_instances(
+ Resource *rr,
+ bool verify,
+ Hashmap **web_cache,
+ const GptPartitionType *match_partition_type) {
int r;
assert(rr);
+ /* If match_partition_type is specified, files listed in UAPI.16 manifests that declare a different
+ * partition type are skipped. This is used to filter the sources by the partition type of the
+ * target the files would be written to. */
+
switch (rr->type) {
case RESOURCE_TAR:
@@ -719,7 +912,7 @@ int resource_load_instances(Resource *rr, bool verify, Hashmap **web_cache) {
case RESOURCE_URL_FILE:
case RESOURCE_URL_TAR:
- r = resource_load_from_web(rr, verify, web_cache);
+ r = resource_load_from_web(rr, verify, web_cache, match_partition_type);
break;
default:
diff --git a/src/sysupdate/sysupdate-resource.h b/src/sysupdate/sysupdate-resource.h
index 2465ad92c4f..ab364666f9c 100644
--- a/src/sysupdate/sysupdate-resource.h
+++ b/src/sysupdate/sysupdate-resource.h
@@ -79,7 +79,7 @@ typedef struct Resource {
char **patterns;
GptPartitionType partition_type;
bool partition_type_set;
- ManifestFormat manifest_format; /* Only MANIFEST_FORMAT_SHA256SUMS for now */
+ ManifestFormat manifest_format; /* MANIFEST_FORMAT_SHA256SUMS or MANIFEST_FORMAT_UAPI16 */
char *manifest_path;
/* All instances of this resource we found */
@@ -94,7 +94,11 @@ void resource_destroy(Resource *rr);
bool resource_has_glob_directory_pattern(Resource *rr);
-int resource_load_instances(Resource *rr, bool verify, Hashmap **web_cache);
+int resource_load_instances(
+ Resource *rr,
+ bool verify,
+ Hashmap **web_cache,
+ const GptPartitionType *match_partition_type);
Instance* resource_find_instance(Resource *rr, const char *version);
diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c
index 8d098666751..64e04e3b2fb 100644
--- a/src/sysupdate/sysupdate-transfer.c
+++ b/src/sysupdate/sysupdate-transfer.c
@@ -638,7 +638,7 @@ static int transfer_finalize(Transfer *t, const char *origin, unsigned line, Has
"Source path is not a valid HTTP, HTTPS, file, or provider URL: %s", t->source.path);
}
- if (t->source.manifest_format != MANIFEST_FORMAT_SHA256SUMS)
+ if (!IN_SET(t->source.manifest_format, MANIFEST_FORMAT_SHA256SUMS, MANIFEST_FORMAT_UAPI16))
return log_syntax(NULL, LOG_ERR, origin, line, SYNTHETIC_ERRNO(EINVAL),
"Manifest format '%s' not supported for source specifications, refusing.",
manifest_format_to_string(t->source.manifest_format));
diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c
index dc0844aece2..4d3a321ac70 100644
--- a/src/sysupdate/sysupdate.c
+++ b/src/sysupdate/sysupdate.c
@@ -572,7 +572,8 @@ static int context_load_installed_instances(Context *c) {
r = resource_load_instances(
&t->target,
c->verify >= 0 ? c->verify : t->verify != 0,
- &c->web_cache);
+ &c->web_cache,
+ /* match_partition_type= */ NULL);
if (r < 0)
return r;
}
@@ -583,7 +584,8 @@ static int context_load_installed_instances(Context *c) {
r = resource_load_instances(
&t->target,
c->verify >= 0 ? c->verify : t->verify != 0,
- &c->web_cache);
+ &c->web_cache,
+ /* match_partition_type= */ NULL);
if (r < 0)
return r;
}
@@ -604,7 +606,8 @@ static int context_load_available_instances(Context *c) {
r = resource_load_instances(
&t->source,
c->verify >= 0 ? c->verify : t->verify != 0,
- &c->web_cache);
+ &c->web_cache,
+ t->target.partition_type_set ? &t->target.partition_type : NULL);
if (r < 0)
return r;
}
diff --git a/test/units/TEST-72-SYSUPDATE.sh b/test/units/TEST-72-SYSUPDATE.sh
index 3bbab09e12e..c52b2667076 100755
--- a/test/units/TEST-72-SYSUPDATE.sh
+++ b/test/units/TEST-72-SYSUPDATE.sh
@@ -1022,9 +1022,9 @@ cmp "$MP/payload/mp-v1.bin" "$MP/target/mp-v1.bin"
# there is none
sed 's|^ManifestPath=.*|&\nManifestPath=|' "$MP/definitions/01-mp.transfer" >"$MP/bad-definitions/01-mp.transfer"
(! "$SYSUPDATE" --definitions="$MP/bad-definitions" --verify=no list)
-# Only SHA256SUMS manifests are supported for now
-sed 's|^ManifestFormat=.*|ManifestFormat=uapi16|' "$MP/definitions/01-mp.transfer" >"$MP/bad-definitions/01-mp.transfer"
-(! "$SYSUPDATE" --definitions="$MP/bad-definitions" --verify=no list) |& grep -F "Manifest format 'uapi16' not supported" >/dev/null
+# Only SHA256SUMS and UAPI.16 manifests are supported
+sed 's|^ManifestFormat=.*|ManifestFormat=mtree|' "$MP/definitions/01-mp.transfer" >"$MP/bad-definitions/01-mp.transfer"
+(! "$SYSUPDATE" --definitions="$MP/bad-definitions" --verify=no list) |& grep -F "Manifest format 'mtree' not supported" >/dev/null
# ManifestPath= must be a URL, and is only supported for URL sources
sed 's|^ManifestPath=.*|ManifestPath=/foo/SHA256SUMS|' "$MP/definitions/01-mp.transfer" >"$MP/bad-definitions/01-mp.transfer"
(! "$SYSUPDATE" --definitions="$MP/bad-definitions" --verify=no list) |& grep -F "Manifest path is not a valid" >/dev/null