Skip to content

Commit

Permalink
Allow verifying the checksum of a payload
Browse files Browse the repository at this point in the history
This allows us to verify multi-gigabyte payloads without loading the entire
payload into memory.
  • Loading branch information
hughsie committed Dec 31, 2023
1 parent d272562 commit 9743305
Show file tree
Hide file tree
Showing 13 changed files with 477 additions and 21 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,42 @@ We can then check the result using
Signature Algorithm: RSA-SHA256
Signature status: ok

Large Payloads
==============

It may be impractical to load the entire binary into RAM for verification.
For this usercase, jcat supports signing the *checksum of the payload* as the target rather than the payload itself.

$ jcat-tool self-sign firmware.jcat firmware.bin --kind sha256
$ jcat-tool --appstream-id com.redhat.rhughes sign firmware.jcat firmware.bin rhughes_signed.pem rhughes.key --target sha256
$ jcat-tool info firmware.jcat
JcatFile:
Version: 0.1
JcatItem:
ID: firmware.bin
JcatBlob:
Kind: sha256
Flags: is-utf8
Timestamp: 2023-12-15T16:38:11Z
Size: 0x40
Data: a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447
JcatBlob:
Kind: pkcs7
Target: sha256
Flags: is-utf8
AppstreamId: com.redhat.rhughes
Timestamp: 2023-12-15T16:38:15Z
Size: 0xdcc
Data: -----BEGIN PKCS7-----
MIIKCwYJKoZIhvcNAQcCoIIJ/DCCCfgCAQExDTALBglghkgBZQMEAgEwCwYJKoZI
...
Zjb6fuKL5Rr/ouoImn+x1cYJyqRMmCxpLG9GrXR9Ag==
-----END PKCS7-----

$ jcat-tool --appstream-id com.redhat.rhughes verify firmware.jcat --public-key ACME-CA.pem
firmware.bin:
PASSED pkcs7: O=ACME Corp.,CN=ACME CA

Testing
=======

Expand Down
1 change: 1 addition & 0 deletions data/tests/colorhug/firmware.bin.sha256
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
a196504d09871da4f7d83b874b500f8ee6e0619ab799f074814b316d88f96f7f
15 changes: 15 additions & 0 deletions data/tests/colorhug/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,19 @@ if get_option('pkcs7')
install: true,
install_dir: join_paths(installed_test_bindir, 'colorhug'),
)

# generate self-signed detached signature *of the checksum*
colorhug_pkcs7_signature_hash = custom_target('firmware.bin.sha256.p7c',
input: 'firmware.bin.sha256',
output: 'firmware.bin.sha256.p7c',
command: [certtool, '--p7-detached-sign',
'--p7-time',
'--load-privkey', pkcs7_privkey,
'--load-certificate', pkcs7_certificate,
'--infile', '@INPUT@',
'--outfile', '@OUTPUT@'],
install: true,
install_dir: join_paths(installed_test_bindir, 'colorhug'),
)

endif
50 changes: 49 additions & 1 deletion libjcat/jcat-blob.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

typedef struct {
JcatBlobKind kind;
JcatBlobKind target;
JcatBlobFlags flags;
GBytes *data;
gchar *appstream_id;
Expand Down Expand Up @@ -161,6 +162,12 @@ jcat_blob_add_string(JcatBlob *self, guint idt, GString *str)
JcatBlobPrivate *priv = GET_PRIVATE(self);
jcat_string_append_kv(str, idt, G_OBJECT_TYPE_NAME(self), NULL);
jcat_string_append_kv(str, idt + 1, "Kind", jcat_blob_kind_to_string(priv->kind));
if (priv->target != JCAT_BLOB_KIND_UNKNOWN) {
jcat_string_append_kv(str,
idt + 1,
"Target",
jcat_blob_kind_to_string(priv->target));
}
jcat_string_append_kv(str,
idt + 1,
"Flags",
Expand Down Expand Up @@ -228,11 +235,13 @@ jcat_blob_import(JsonObject *obj, JcatImportFlags flags, GError **error)
priv->kind = json_object_get_int_member(obj, "Kind");
priv->flags = json_object_get_int_member(obj, "Flags");

/* both optional */
/* all optional */
if (json_object_has_member(obj, "Timestamp"))
priv->timestamp = json_object_get_int_member(obj, "Timestamp");
if (json_object_has_member(obj, "AppstreamId"))
priv->appstream_id = g_strdup(json_object_get_string_member(obj, "AppstreamId"));
if (json_object_has_member(obj, "Target"))
priv->target = json_object_get_int_member(obj, "Target");

/* get compressed data */
data_str = json_object_get_string_member(obj, "Data");
Expand All @@ -258,6 +267,10 @@ jcat_blob_export(JcatBlob *self, JcatExportFlags flags, JsonBuilder *builder)
/* add metadata */
json_builder_set_member_name(builder, "Kind");
json_builder_add_int_value(builder, priv->kind);
if (priv->target != JCAT_BLOB_KIND_UNKNOWN) {
json_builder_set_member_name(builder, "Target");
json_builder_add_int_value(builder, priv->target);
}
json_builder_set_member_name(builder, "Flags");
json_builder_add_int_value(builder, priv->flags);
if (priv->appstream_id != NULL) {
Expand Down Expand Up @@ -430,6 +443,41 @@ jcat_blob_new_full(JcatBlobKind kind, GBytes *data, JcatBlobFlags flags)
return self;
}

/**
* jcat_blob_get_target:
* @self: #JcatBlob
*
* Gets the blob target.
*
* Returns: #JcatBlobKind, e.g. %JCAT_BLOB_KIND_SHA256
*
* Since: 0.2.0
**/
JcatBlobKind
jcat_blob_get_target(JcatBlob *self)
{
JcatBlobPrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(JCAT_IS_BLOB(self), 0);
return priv->target;
}

/**
* jcat_blob_set_target:
* @self: #JcatBlob
* @target: a #JcatBlobKind, e.g. %JCAT_BLOB_KIND_SHA256
*
* Sets the blob target.
*
* Since: 0.2.0
**/
void
jcat_blob_set_target(JcatBlob *self, JcatBlobKind target)
{
JcatBlobPrivate *priv = GET_PRIVATE(self);
g_return_if_fail(JCAT_IS_BLOB(self));
priv->target = target;
}

/**
* jcat_blob_new:
* @kind: #JcatBlobKind, e.g. %JCAT_BLOB_KIND_SHA256
Expand Down
4 changes: 4 additions & 0 deletions libjcat/jcat-blob.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ gchar *
jcat_blob_get_data_as_string(JcatBlob *self) G_GNUC_NON_NULL(1);
JcatBlobKind
jcat_blob_get_kind(JcatBlob *self) G_GNUC_NON_NULL(1);
JcatBlobKind
jcat_blob_get_target(JcatBlob *self) G_GNUC_NON_NULL(1);
void
jcat_blob_set_target(JcatBlob *self, JcatBlobKind target) G_GNUC_NON_NULL(1);
gint64
jcat_blob_get_timestamp(JcatBlob *self) G_GNUC_NON_NULL(1);
void
Expand Down
144 changes: 144 additions & 0 deletions libjcat/jcat-context.c
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,150 @@ jcat_context_verify_item(JcatContext *self,
return g_steal_pointer(&results);
}

/**
* jcat_context_verify_target:
* @self: #JcatContext
* @item_target: #JcatItem containing checksums of the data
* @item: #JcatItem
* @flags: #JcatVerifyFlags, e.g. %JCAT_VERIFY_FLAG_REQUIRE_SIGNATURE
* @error: #GError, or %NULL
*
* Verifies a #JcatItem using the target to an item. At least one `verify=CHECKSUM` (e.g. SHA256)
* must exist and all checksum types that do exist must verify correctly.
*
* Returns: (transfer container) (element-type JcatResult): results, or %NULL for failed
*
* Since: 0.2.0
**/
GPtrArray *
jcat_context_verify_target(JcatContext *self,
JcatItem *item_target,
JcatItem *item,
JcatVerifyFlags flags,
GError **error)
{
guint nr_signature = 0;
g_autoptr(GPtrArray) blobs = NULL;
g_autoptr(GPtrArray) results =
g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);

g_return_val_if_fail(JCAT_IS_CONTEXT(self), NULL);
g_return_val_if_fail(JCAT_IS_ITEM(item_target), NULL);
g_return_val_if_fail(JCAT_IS_ITEM(item), NULL);

/* no blobs */
blobs = jcat_item_get_blobs(item);
if (blobs->len == 0) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"no blobs in item");
return NULL;
}

/* all checksum engines must verify */
for (guint i = 0; i < blobs->len; i++) {
JcatBlob *blob = g_ptr_array_index(blobs, i);
g_autoptr(GError) error_local = NULL;
g_autoptr(JcatEngine) engine = NULL;
g_autoptr(JcatResult) result = NULL;
g_autoptr(JcatBlob) blob_target = NULL;
g_autofree gchar *checksum = NULL;
g_autofree gchar *checksum_target = NULL;

/* get engine */
engine = jcat_context_get_engine(self, jcat_blob_get_kind(blob), &error_local);
if (engine == NULL) {
g_debug("%s", error_local->message);
continue;
}
if (jcat_engine_get_method(engine) != JCAT_BLOB_METHOD_CHECKSUM)
continue;
blob_target =
jcat_item_get_blob_by_kind(item_target, jcat_blob_get_kind(blob), &error_local);
if (blob_target == NULL) {
g_debug("no target value: %s", error_local->message);
continue;
}

/* checksum is as expected */
checksum = jcat_blob_get_data_as_string(blob);
checksum_target = jcat_blob_get_data_as_string(blob_target);
if (g_strcmp0(checksum, checksum_target) != 0) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"%s checksum was %s but target is %s",
jcat_blob_kind_to_string(jcat_blob_get_kind(blob)),
checksum,
checksum_target);
return NULL;
}
g_ptr_array_add(results, g_object_new(JCAT_TYPE_RESULT, "engine", engine, NULL));
}
if (flags & JCAT_VERIFY_FLAG_REQUIRE_CHECKSUM && results->len == 0) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"checksums were required, but none supplied");
return NULL;
}

/* we only have to have one non-checksum method to verify */
for (guint i = 0; i < blobs->len; i++) {
JcatBlob *blob = g_ptr_array_index(blobs, i);
g_autofree gchar *result_str = NULL;
g_autoptr(GError) error_local = NULL;
g_autoptr(JcatBlob) blob_target = NULL;
g_autoptr(JcatEngine) engine = NULL;
g_autoptr(JcatResult) result = NULL;

engine = jcat_context_get_engine(self, jcat_blob_get_kind(blob), &error_local);
if (engine == NULL) {
g_debug("%s", error_local->message);
continue;
}
if (jcat_engine_get_method(engine) != JCAT_BLOB_METHOD_SIGNATURE)
continue;
if (jcat_blob_get_target(blob) == JCAT_BLOB_KIND_UNKNOWN) {
g_debug("blob has no target");
continue;
}
blob_target = jcat_item_get_blob_by_kind(item_target,
jcat_blob_get_target(blob),
&error_local);
if (blob_target == NULL) {
g_debug("no target for %s: %s",
jcat_blob_kind_to_string(jcat_blob_get_target(blob)),
error_local->message);
continue;
}
result = jcat_engine_pubkey_verify(engine,
jcat_blob_get_data(blob_target),
jcat_blob_get_data(blob),
flags,
&error_local);
if (result == NULL) {
g_debug("signature failure: %s", error_local->message);
continue;
}
result_str = jcat_result_to_string(result);
g_debug("verified: %s", result_str);
g_ptr_array_add(results, g_steal_pointer(&result));
nr_signature++;
}
if (flags & JCAT_VERIFY_FLAG_REQUIRE_SIGNATURE && nr_signature == 0) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"signatures were required, but none supplied");
return NULL;
}

/* success */
return g_steal_pointer(&results);
}

/**
* jcat_context_new:
*
Expand Down
6 changes: 6 additions & 0 deletions libjcat/jcat-context.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ jcat_context_verify_item(JcatContext *self,
JcatItem *item,
JcatVerifyFlags flags,
GError **error) G_GNUC_NON_NULL(1, 2, 3);
GPtrArray *
jcat_context_verify_target(JcatContext *self,
JcatItem *item_target,
JcatItem *item,
JcatVerifyFlags flags,
GError **error) G_GNUC_NON_NULL(1, 2, 3);
void
jcat_context_blob_kind_allow(JcatContext *self, JcatBlobKind kind) G_GNUC_NON_NULL(1);
void
Expand Down
Loading

0 comments on commit 9743305

Please sign in to comment.