depot_download: option for unverified downloads

This patch equips the depot_download subsystem with the option to
explicitly skip the signature verification for downloads by specifying
the attribute 'verify="no"' for an <installation> item. This is useful
in scenarios where the lack of integrity of downloaded content does not
pose a risk, e.g., for untrusted applications that are rigidly
sandboxed, or during development.

Note that this option does not entirely discarge the signature checking.
Whenever an download has dependencies that are verifyable - for
which the public key exists in the depot - the dependencies are still
verified. This allows untrusted content to depend of verifyable content
while protecting the integrity the verifyable content.

Issue #4804
This commit is contained in:
Norman Feske 2023-03-24 17:14:41 +01:00 committed by Christian Helmuth
parent b6bb338011
commit f8fd202a1c
10 changed files with 219 additions and 90 deletions

View File

@ -58,11 +58,18 @@ void Depot_download_manager::gen_depot_query_start_content(Xml_generator &xml,
fn(node); });
};
auto propagate_verify_attr = [&] (Xml_generator &xml, Xml_node const &node)
{
if (node.attribute_value("verify", true) == false)
xml.attribute("require_verify", "no");
};
for_each_install_sub_node("archive", [&] (Xml_node const &archive) {
xml.node("dependencies", [&] () {
xml.attribute("path", archive.attribute_value("path", Archive::Path()));
xml.attribute("source", archive.attribute_value("source", true));
xml.attribute("binary", archive.attribute_value("binary", true));
propagate_verify_attr(xml, archive);
});
});
@ -75,6 +82,7 @@ void Depot_download_manager::gen_depot_query_start_content(Xml_generator &xml,
xml.node("index", [&] () {
xml.attribute("user", Archive::user(path));
xml.attribute("version", Archive::_path_element<Archive::Version>(path, 2));
propagate_verify_attr(xml, index);
});
});
@ -87,6 +95,7 @@ void Depot_download_manager::gen_depot_query_start_content(Xml_generator &xml,
xml.node("image", [&] () {
xml.attribute("user", Archive::user(path));
xml.attribute("name", Archive::name(path));
propagate_verify_attr(xml, image);
});
});
@ -97,7 +106,9 @@ void Depot_download_manager::gen_depot_query_start_content(Xml_generator &xml,
return;
}
xml.node("image_index", [&] () {
xml.attribute("user", Archive::user(path)); });
xml.attribute("user", Archive::user(path));
propagate_verify_attr(xml, image_index);
});
});
if (next_user.valid())

View File

@ -55,7 +55,7 @@ void Depot_download_manager::gen_extract_start_content(Xml_generator &xml,
});
});
import.for_each_verified_archive([&] (Archive::Path const &path) {
import.for_each_verified_or_blessed_archive([&] (Archive::Path const &path) {
typedef String<160> Path;

View File

@ -16,6 +16,7 @@
void Depot_download_manager::gen_fetchurl_start_content(Xml_generator &xml,
Import const &import,
Url const &current_user_url,
Pubkey_known pubkey_known,
Fetchurl_version version)
{
xml.attribute("version", version.value);
@ -23,40 +24,40 @@ void Depot_download_manager::gen_fetchurl_start_content(Xml_generator &xml,
gen_common_start_content(xml, "fetchurl",
Cap_quota{500}, Ram_quota{8*1024*1024});
xml.node("config", [&] () {
xml.node("libc", [&] () {
xml.node("config", [&] {
xml.node("libc", [&] {
xml.attribute("stdout", "/dev/log");
xml.attribute("stderr", "/dev/log");
xml.attribute("rtc", "/dev/rtc");
xml.attribute("socket", "/socket");
});
xml.node("report", [&] () {
xml.node("report", [&] {
xml.attribute("progress", "yes");
xml.attribute("delay_ms", 250);
});
xml.node("vfs", [&] () {
xml.node("dir", [&] () {
xml.node("vfs", [&] {
xml.node("dir", [&] {
xml.attribute("name", "download");
xml.node("fs", [&] () {
xml.node("fs", [&] {
xml.attribute("buffer_size", 144u << 10);
xml.attribute("label", "download"); });
});
xml.node("dir", [&] () {
xml.node("dir", [&] {
xml.attribute("name", "dev");
xml.node("log", [&] () { });
xml.node("null", [&] () { });
xml.node("inline", [&] () {
xml.node("log", [&] { });
xml.node("null", [&] { });
xml.node("inline", [&] {
xml.attribute("name", "rtc");
String<64> date("2000-01-01 00:00");
xml.append(date.string());
});
xml.node("inline", [&] () {
xml.node("inline", [&] {
xml.attribute("name", "random");
String<64> entropy("01234567890123456789");
xml.append(entropy.string());
});
});
xml.node("fs", [&] () {
xml.node("fs", [&] {
xml.attribute("label", "tcpip"); });
});
@ -69,29 +70,31 @@ void Depot_download_manager::gen_fetchurl_start_content(Xml_generator &xml,
Remote const remote (current_user_url, "/", file_path);
Local const local ("/download/", file_path);
xml.node("fetch", [&] () {
xml.node("fetch", [&] {
xml.attribute("url", remote);
xml.attribute("path", local);
});
xml.node("fetch", [&] () {
xml.attribute("url", Remote(remote, ".sig"));
xml.attribute("path", Local (local, ".sig"));
});
if (pubkey_known.value) {
xml.node("fetch", [&] {
xml.attribute("url", Remote(remote, ".sig"));
xml.attribute("path", Local (local, ".sig"));
});
}
});
});
xml.node("route", [&] () {
xml.node("service", [&] () {
xml.node("route", [&] {
xml.node("service", [&] {
xml.attribute("name", File_system::Session::service_name());
xml.attribute("label", "download");
xml.node("parent", [&] () {
xml.node("parent", [&] {
xml.attribute("label", "public_rw"); });
});
xml.node("service", [&] () {
xml.node("service", [&] {
xml.attribute("name", File_system::Session::service_name());
xml.attribute("label", "tcpip");
xml.node("parent", [&] () {
xml.node("parent", [&] {
xml.attribute("label", "tcpip"); });
});
gen_parent_unscoped_rom_route(xml, "fetchurl");

View File

@ -63,12 +63,15 @@ class Depot_download_manager::Import
Archive::Path const path;
bool const require_verify;
enum State { DOWNLOAD_IN_PROGRESS,
DOWNLOAD_COMPLETE,
DOWNLOAD_UNAVAILABLE,
VERIFICATION_IN_PROGRESS,
VERIFIED,
VERIFICATION_FAILED,
BLESSED, /* verification deliberately skipped */
UNPACKED };
State state = DOWNLOAD_IN_PROGRESS;
@ -78,12 +81,15 @@ class Depot_download_manager::Import
return state == DOWNLOAD_IN_PROGRESS
|| state == DOWNLOAD_COMPLETE
|| state == VERIFICATION_IN_PROGRESS
|| state == VERIFIED;
|| state == VERIFIED
|| state == BLESSED;
}
Item(Registry<Item> &registry, Archive::Path const &path)
Item(Registry<Item> &registry, Archive::Path const &path,
Require_verify require_verify)
:
_element(registry, *this), path(path)
_element(registry, *this), path(path),
require_verify(require_verify.value)
{ }
char const *state_text() const
@ -95,6 +101,7 @@ class Depot_download_manager::Import
case VERIFICATION_IN_PROGRESS: return "verify";
case VERIFIED: return "extract";
case VERIFICATION_FAILED: return "corrupted";
case BLESSED: return "extract";
case UNPACKED: return "done";
};
return "";
@ -103,6 +110,8 @@ class Depot_download_manager::Import
Allocator &_alloc;
bool const _pubkey_known;
Registry<Item> _items { };
template <typename FN>
@ -155,16 +164,16 @@ class Depot_download_manager::Import
FN const &fn)
{
dependencies.for_each_sub_node("missing", [&] (Xml_node const &item) {
fn(_depdendency_path(item)); });
fn(_depdendency_path(item), Require_verify::from_xml(item)); });
index.for_each_sub_node("missing", [&] (Xml_node const &item) {
fn(_index_path(item)); });
fn(_index_path(item), Require_verify::from_xml(item)); });
image.for_each_sub_node("missing", [&] (Xml_node const &item) {
fn(_image_path(item)); });
fn(_image_path(item), Require_verify::from_xml(item)); });
image_index.for_each_sub_node("missing", [&] (Xml_node const &item) {
fn(_image_index_path(item)); });
fn(_image_index_path(item), Require_verify::from_xml(item)); });
}
public:
@ -200,18 +209,20 @@ class Depot_download_manager::Import
* items that match the 'user'. The remaining sub nodes are imported in
* a future iteration.
*/
Import(Allocator &alloc, Archive::User const &user,
Xml_node const &dependencies,
Xml_node const &index,
Xml_node const &image,
Xml_node const &image_index)
Import(Allocator &alloc,
Archive::User const &user,
Pubkey_known const pubkey_known,
Xml_node const &dependencies,
Xml_node const &index,
Xml_node const &image,
Xml_node const &image_index)
:
_alloc(alloc)
_alloc(alloc), _pubkey_known(pubkey_known.value)
{
_for_each_missing_depot_path(dependencies, index, image, image_index,
[&] (Archive::Path const &path) {
[&] (Archive::Path const &path, Require_verify require_verify) {
if (Archive::user(path) == user)
new (alloc) Item(_items, path); });
new (alloc) Item(_items, path, require_verify); });
}
~Import()
@ -234,9 +245,10 @@ class Depot_download_manager::Import
return _item_state_exists(Item::VERIFICATION_IN_PROGRESS);
}
bool verified_archives_available() const
bool verified_or_blessed_archives_available() const
{
return _item_state_exists(Item::VERIFIED);
return _item_state_exists(Item::VERIFIED)
|| _item_state_exists(Item::BLESSED);
}
template <typename FN>
@ -252,9 +264,10 @@ class Depot_download_manager::Import
}
template <typename FN>
void for_each_verified_archive(FN const &fn) const
void for_each_verified_or_blessed_archive(FN const &fn) const
{
_for_each_item(Item::VERIFIED, fn);
_for_each_item(Item::BLESSED, fn);
}
template <typename FN>
@ -277,11 +290,23 @@ class Depot_download_manager::Import
item.state = Item::DOWNLOAD_COMPLETE; });
}
void verify_all_downloaded_archives()
void verify_or_bless_all_downloaded_archives()
{
_items.for_each([&] (Item &item) {
if (item.state == Item::DOWNLOAD_COMPLETE)
item.state = Item::VERIFICATION_IN_PROGRESS; });
if (item.state == Item::DOWNLOAD_COMPLETE) {
/*
* If verification is not required, still verify whenever
* a depot user's public key exists. This way, verifiable
* archives referred to by non-verified archives end up in
* verified form in the depot.
*/
if (item.require_verify || _pubkey_known)
item.state = Item::VERIFICATION_IN_PROGRESS;
else
item.state = Item::BLESSED;
}
});
}
void apply_download_progress(Download_progress const &progress)
@ -319,10 +344,10 @@ class Depot_download_manager::Import
item.state = Item::VERIFICATION_FAILED; });
}
void all_verified_archives_extracted()
void all_verified_or_blessed_archives_extracted()
{
_items.for_each([&] (Item &item) {
if (item.state == Item::VERIFIED)
if (item.state == Item::VERIFIED || item.state == Item::BLESSED)
item.state = Item::UNPACKED; });
}

View File

@ -87,6 +87,11 @@ struct Depot_download_manager::Main : Import::Download_progress
return _current_user.xml().attribute_value("name", Archive::User());
}
Pubkey_known _current_user_has_pubkey() const
{
return Pubkey_known { _current_user.xml().has_sub_node("pubkey") };
}
Path _current_user_path() const
{
return Path("/depot/", _current_user_name());
@ -326,7 +331,9 @@ void Depot_download_manager::Main::_generate_init_config(Xml_generator &xml)
if (fetchurl_running) {
try {
xml.node("start", [&] () {
gen_fetchurl_start_content(xml, *_import, _current_user_url(),
gen_fetchurl_start_content(xml, *_import,
_current_user_url(),
_current_user_has_pubkey(),
_fetchurl_count); });
}
catch (Invalid_download_url) {
@ -338,7 +345,7 @@ void Depot_download_manager::Main::_generate_init_config(Xml_generator &xml)
xml.node("start", [&] () {
gen_verify_start_content(xml, *_import, _current_user_path()); });
if (_import.constructed() && _import->verified_archives_available()) {
if (_import.constructed() && _import->verified_or_blessed_archives_available()) {
xml.node("start", [&] () {
gen_chroot_start_content(xml, _current_user_name()); });
@ -370,8 +377,7 @@ void Depot_download_manager::Main::_handle_query_result()
Archive::User const name = user.attribute_value("name", Archive::User());
bool const user_info_complete = user.has_sub_node("url")
&& user.has_sub_node("pubkey");
bool const user_info_complete = user.has_sub_node("url");
if (name.valid() && !user_info_complete) {
@ -397,6 +403,8 @@ void Depot_download_manager::Main::_handle_query_result()
Xml_node const image = _image.xml();
Xml_node const image_index = _image_index.xml();
log("query result index: ", index);
/* mark jobs referring to existing depot content as unneccessary */
Import::for_each_present_depot_path(dependencies, index, image, image_index,
[&] (Archive::Path const &path) {
@ -458,7 +466,7 @@ void Depot_download_manager::Main::_handle_query_result()
}
/* start new import */
_import.construct(_heap, _current_user_name(),
_import.construct(_heap, _current_user_name(), _current_user_has_pubkey(),
dependencies, index, image, image_index);
/* mark imported jobs as started */
@ -514,7 +522,7 @@ void Depot_download_manager::Main::_handle_init_state()
}
if (!import.downloads_in_progress() && import.completed_downloads_available()) {
import.verify_all_downloaded_archives();
import.verify_or_bless_all_downloaded_archives();
reconfigure_init = true;
}
@ -545,7 +553,7 @@ void Depot_download_manager::Main::_handle_init_state()
});
}
if (import.verified_archives_available()) {
if (import.verified_or_blessed_archives_available()) {
Child_exit_state const extract_state(_init_state.xml(), "extract");
@ -553,7 +561,7 @@ void Depot_download_manager::Main::_handle_init_state()
error("extract failed with exit code ", extract_state.code);
if (extract_state.exited && extract_state.code == 0)
import.all_verified_archives_extracted();
import.all_verified_or_blessed_archives_extracted();
}
/* flag failed jobs to prevent re-attempts in subsequent import iterations */

View File

@ -28,8 +28,33 @@ namespace Depot_download_manager {
struct Depot_query_version { unsigned value; };
struct Fetchurl_version { unsigned value; };
struct Require_verify;
struct Pubkey_known { bool value; };
}
/**
* Argument type for propagating 'verify' install attributes to imports
*/
struct Depot_download_manager::Require_verify
{
bool value;
static Require_verify from_xml(Xml_node const &node)
{
return Require_verify { node.attribute_value("require_verify", true) };
}
void gen_attr(Xml_generator &xml) const
{
if (!value)
xml.attribute("require_verify", "no");
}
};
namespace Genode {
/**

View File

@ -89,7 +89,7 @@ namespace Depot_download_manager {
List_model<Job> const &);
void gen_fetchurl_start_content(Xml_generator &, Import const &, Url const &,
Fetchurl_version);
Pubkey_known, Fetchurl_version);
void gen_verify_start_content(Xml_generator &, Import const &, Path const &);

View File

@ -191,6 +191,7 @@ void Depot_query::Main::_query_blueprint(Directory::Path const &pkg_path, Xml_ge
void Depot_query::Main::_collect_source_dependencies(Archive::Path const &path,
Dependencies &dependencies,
Require_verify require_verify,
Recursion_limit recursion_limit)
{
try { Archive::type(path); }
@ -199,14 +200,15 @@ void Depot_query::Main::_collect_source_dependencies(Archive::Path const &path,
return;
}
dependencies.record(path);
dependencies.record(path, require_verify);
switch (Archive::type(path)) {
case Archive::PKG: {
_with_file_content(path, "archives", [&] (File_content const &archives) {
archives.for_each_line<Archive::Path>([&] (Archive::Path const &path) {
_collect_source_dependencies(path, dependencies, recursion_limit); }); });
_collect_source_dependencies(path, dependencies, require_verify,
recursion_limit); }); });
break;
}
@ -214,7 +216,8 @@ void Depot_query::Main::_collect_source_dependencies(Archive::Path const &path,
typedef String<160> Api;
_with_file_content(path, "used_apis", [&] (File_content const &used_apis) {
used_apis.for_each_line<Archive::Path>([&] (Api const &api) {
dependencies.record(Archive::Path(Archive::user(path), "/api/", api)); }); });
dependencies.record(Archive::Path(Archive::user(path), "/api/", api),
require_verify); }); });
break;
}
@ -227,6 +230,7 @@ void Depot_query::Main::_collect_source_dependencies(Archive::Path const &path,
void Depot_query::Main::_collect_binary_dependencies(Archive::Path const &path,
Dependencies &dependencies,
Require_verify require_verify,
Recursion_limit recursion_limit)
{
try { Archive::type(path); }
@ -238,22 +242,23 @@ void Depot_query::Main::_collect_binary_dependencies(Archive::Path const &path,
switch (Archive::type(path)) {
case Archive::PKG:
dependencies.record(path);
dependencies.record(path, require_verify);
_with_file_content(path, "archives", [&] (File_content const &archives) {
archives.for_each_line<Archive::Path>([&] (Archive::Path const &archive_path) {
_collect_binary_dependencies(archive_path, dependencies, recursion_limit); }); });
_collect_binary_dependencies(archive_path, dependencies,
require_verify, recursion_limit); }); });
break;
case Archive::SRC:
dependencies.record(Archive::Path(Archive::user(path), "/bin/",
_architecture, "/",
Archive::name(path), "/",
Archive::version(path)));
Archive::version(path)), require_verify);
break;
case Archive::RAW:
dependencies.record(path);
dependencies.record(path, require_verify);
break;
case Archive::IMAGE:
@ -363,13 +368,16 @@ void Depot_query::Main::_gen_index_for_arch(Xml_generator &xml,
void Depot_query::Main::_query_index(Archive::User const &user,
Archive::Version const &version,
bool const content, Xml_generator &xml)
bool const content,
Require_verify const require_verify,
Xml_generator &xml)
{
Directory::Path const index_path("depot/", user, "/index/", version);
if (!_root.file_exists(index_path)) {
xml.node("missing", [&] () {
xml.attribute("user", user);
xml.attribute("version", version);
require_verify.gen_attr(xml);
});
return;
}
@ -377,6 +385,7 @@ void Depot_query::Main::_query_index(Archive::User const &user,
xml.node("index", [&] () {
xml.attribute("user", user);
xml.attribute("version", version);
require_verify.gen_attr(xml);
if (content) {
try {
@ -392,9 +401,10 @@ void Depot_query::Main::_query_index(Archive::User const &user,
}
void Depot_query::Main::_query_image(Archive::User const &user,
Archive::Name const &name,
Xml_generator &xml)
void Depot_query::Main::_query_image(Archive::User const &user,
Archive::Name const &name,
Require_verify const require_verify,
Xml_generator &xml)
{
Directory::Path const image_path("depot/", user, "/image/", name);
char const *node_type = _root.directory_exists(image_path)
@ -402,6 +412,7 @@ void Depot_query::Main::_query_image(Archive::User const &user,
xml.node(node_type, [&] () {
xml.attribute("user", user);
xml.attribute("name", name);
require_verify.gen_attr(xml);
});
}

View File

@ -32,6 +32,7 @@ namespace Depot_query {
typedef String<64> Rom_label;
struct Require_verify;
struct Directory_cache;
struct Recursion_limit;
struct Dependencies;
@ -39,6 +40,26 @@ namespace Depot_query {
}
/**
* Argument type for propagating 'require_verify' query attributes to results
*/
struct Depot_query::Require_verify
{
bool value;
static Require_verify from_xml(Xml_node const &node)
{
return Require_verify { node.attribute_value("require_verify", true) };
}
void gen_attr(Xml_generator &xml) const
{
if (!value)
xml.attribute("require_verify", "no");
}
};
struct Depot_query::Directory_cache : Noncopyable
{
Allocator &_alloc;
@ -165,11 +186,28 @@ class Depot_query::Dependencies
{
private:
using Path = Archive::Path;
struct Collection : Noncopyable
{
Allocator &_alloc;
typedef Registered_no_delete<Archive::Path> Entry;
struct Dependency
{
Path path;
Require_verify require_verify;
Dependency(Path const &path, Require_verify require_verify)
: path(path), require_verify(require_verify) { }
void gen_attr(Xml_generator &xml) const
{
xml.attribute("path", path);
require_verify.gen_attr(xml);
}
};
using Entry = Registered_no_delete<Dependency>;
Registry<Entry> _entries { };
@ -180,20 +218,20 @@ class Depot_query::Dependencies
_entries.for_each([&] (Entry &e) { destroy(_alloc, &e); });
}
bool known(Archive::Path const &path) const
bool known(Path const &path) const
{
bool result = false;
_entries.for_each([&] (Entry const &entry) {
if (path == entry)
if (path == entry.path)
result = true; });
return result;
}
void insert(Archive::Path const &path)
void insert(Path const &path, Require_verify require_verify)
{
if (!known(path))
new (_alloc) Entry(_entries, path);
new (_alloc) Entry(_entries, path, require_verify);
}
template <typename FN>
@ -212,26 +250,26 @@ class Depot_query::Dependencies
_depot(depot), _present(alloc), _missing(alloc)
{ }
bool known(Archive::Path const &path) const
bool known(Path const &path) const
{
return _present.known(path) || _missing.known(path);
}
void record(Archive::Path const &path)
void record(Path const &path, Require_verify require_verify)
{
if (_depot.directory_exists(path))
_present.insert(path);
_present.insert(path, require_verify);
else
_missing.insert(path);
_missing.insert(path, require_verify);
}
void xml(Xml_generator &xml) const
{
_present.for_each([&] (Archive::Path const &path) {
xml.node("present", [&] () { xml.attribute("path", path); }); });
_present.for_each([&] (Collection::Entry const &entry) {
xml.node("present", [&] () { entry.gen_attr(xml); }); });
_missing.for_each([&] (Archive::Path const &path) {
xml.node("missing", [&] () { xml.attribute("path", path); }); });
_missing.for_each([&] (Collection::Entry const &entry) {
xml.node("missing", [&] () { entry.gen_attr(xml); }); });
}
};
@ -336,15 +374,15 @@ struct Depot_query::Main
void _gen_inherited_rom_path_nodes(Xml_generator &, Xml_node const &,
Archive::Path const &, Recursion_limit);
void _query_blueprint(Directory::Path const &, Xml_generator &);
void _collect_source_dependencies(Archive::Path const &, Dependencies &, Recursion_limit);
void _collect_binary_dependencies(Archive::Path const &, Dependencies &, Recursion_limit);
void _collect_source_dependencies(Archive::Path const &, Dependencies &, Require_verify, Recursion_limit);
void _collect_binary_dependencies(Archive::Path const &, Dependencies &, Require_verify, Recursion_limit);
void _scan_user(Archive::User const &, Xml_generator &);
void _query_user(Archive::User const &, Xml_generator &);
void _gen_index_node_rec(Xml_generator &, Xml_node const &, unsigned) const;
void _gen_index_for_arch(Xml_generator &, Xml_node const &) const;
void _query_index(Archive::User const &, Archive::Version const &, bool, Xml_generator &);
void _query_image(Archive::User const &, Archive::Name const &, Xml_generator &);
void _query_image_index(Xml_node const &, Xml_generator &);
void _query_index(Archive::User const &, Archive::Version const &, bool, Require_verify, Xml_generator &);
void _query_image(Archive::User const &, Archive::Name const &, Require_verify, Xml_generator &);
void _query_image_index(Xml_node const &, Require_verify, Xml_generator &);
void _handle_config()
{
@ -431,13 +469,16 @@ struct Depot_query::Main
Dependencies dependencies(_heap, _depot_dir);
query.for_each_sub_node("dependencies", [&] (Xml_node node) {
Archive::Path const path = node.attribute_value("path", Archive::Path());
Archive::Path const path = node.attribute_value("path", Archive::Path());
Require_verify const require_verify = Require_verify::from_xml(node);
if (node.attribute_value("source", false))
_collect_source_dependencies(path, dependencies, Recursion_limit{8});
_collect_source_dependencies(path, dependencies, require_verify,
Recursion_limit{8});
if (node.attribute_value("binary", false))
_collect_binary_dependencies(path, dependencies, Recursion_limit{8});
_collect_binary_dependencies(path, dependencies, require_verify,
Recursion_limit{8});
});
dependencies.xml(xml);
});
@ -457,17 +498,19 @@ struct Depot_query::Main
_query_index(node.attribute_value("user", Archive::User()),
node.attribute_value("version", Archive::Version()),
node.attribute_value("content", false),
Require_verify::from_xml(node),
xml); }); });
_gen_versioned_report(_image_reporter, version, [&] (Xml_generator &xml) {
query.for_each_sub_node("image", [&] (Xml_node node) {
_query_image(node.attribute_value("user", Archive::User()),
node.attribute_value("name", Archive::Name()),
Require_verify::from_xml(node),
xml); }); });
_gen_versioned_report(_image_index_reporter, version, [&] (Xml_generator &xml) {
query.for_each_sub_node("image_index", [&] (Xml_node node) {
_query_image_index(node, xml); }); });
_query_image_index(node, Require_verify::from_xml(node), xml); }); });
}
Main(Env &env) : _env(env)

View File

@ -19,6 +19,7 @@
void Depot_query::Main::_query_image_index(Xml_node const &index_query,
Require_verify require_verify,
Xml_generator &xml)
{
using User = Archive::User;
@ -150,7 +151,9 @@ void Depot_query::Main::_query_image_index(Xml_node const &index_query,
* file.
*/
xml.node(index_exists ? "present" : "missing", [&] () {
xml.attribute("user", user); });
xml.attribute("user", user);
require_verify.gen_attr(xml);
});
/*
* Report aggregated image information with the newest version first.