diff --git a/Makefile-libostree.am b/Makefile-libostree.am
index 27c20b5d95..701653ed1c 100644
--- a/Makefile-libostree.am
+++ b/Makefile-libostree.am
@@ -268,6 +268,9 @@ libostree_1_la_SOURCES += \
src/libostree/ostree-blob-reader-base64.h \
src/libostree/ostree-blob-reader-raw.c \
src/libostree/ostree-blob-reader-raw.h \
+ src/libostree/ostree-blob-reader-pem.c \
+ src/libostree/ostree-blob-reader-pem.h \
+ src/libostree/ostree-blob-reader-private.h \
$(NULL)
if USE_COMPOSEFS
diff --git a/Makefile-tests.am b/Makefile-tests.am
index abf61af2d0..415637a5d1 100644
--- a/Makefile-tests.am
+++ b/Makefile-tests.am
@@ -270,7 +270,7 @@ _installed_or_uninstalled_test_programs = tests/test-varint tests/test-ot-unix-u
tests/test-keyfile-utils tests/test-ot-opt-utils tests/test-ot-tool-util \
tests/test-checksum tests/test-lzma tests/test-rollsum \
tests/test-basic-c tests/test-sysroot-c tests/test-pull-c tests/test-repo tests/test-include-ostree-h tests/test-kargs \
- tests/test-rfc2616-dates
+ tests/test-rfc2616-dates tests/test-pem
if USE_GPGME
_installed_or_uninstalled_test_programs += \
@@ -403,6 +403,12 @@ tests_test_rfc2616_dates_SOURCES = \
tests_test_rfc2616_dates_CFLAGS = $(TESTS_CFLAGS)
tests_test_rfc2616_dates_LDADD = $(TESTS_LDADD)
+tests_test_pem_SOURCES = \
+ src/libostree/ostree-blob-reader-pem.c \
+ tests/test-pem.c
+tests_test_pem_CFLAGS = $(TESTS_CFLAGS)
+tests_test_pem_LDADD = $(TESTS_LDADD)
+
noinst_PROGRAMS += tests/test-commit-sign-sh-ext
tests_test_commit_sign_sh_ext_CFLAGS = $(TESTS_CFLAGS)
tests_test_commit_sign_sh_ext_LDADD = $(TESTS_LDADD)
diff --git a/src/libostree/ostree-blob-reader-pem.c b/src/libostree/ostree-blob-reader-pem.c
new file mode 100644
index 0000000000..9f0a29609f
--- /dev/null
+++ b/src/libostree/ostree-blob-reader-pem.c
@@ -0,0 +1,225 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.0+
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library. If not, see .
+ */
+
+/* This implements a simple parser of the PEM format defined in RFC
+ * 7468, which doesn't allow headers to be encoded alongside the data
+ * (unlike the legacy RFC 1421).
+ */
+
+#include "config.h"
+
+#include "ostree-blob-reader-pem.h"
+#include "ostree-blob-reader-private.h"
+#include
+
+enum
+{
+ PROP_0,
+ PROP_LABEL
+};
+
+struct _OstreeBlobReaderPem
+{
+ GDataInputStream parent_instance;
+
+ gchar *label;
+};
+
+static void ostree_blob_reader_pem_iface_init (OstreeBlobReaderInterface *iface);
+G_DEFINE_TYPE_WITH_CODE (OstreeBlobReaderPem, _ostree_blob_reader_pem, G_TYPE_DATA_INPUT_STREAM,
+ G_IMPLEMENT_INTERFACE (OSTREE_TYPE_BLOB_READER,
+ ostree_blob_reader_pem_iface_init));
+
+static void
+ostree_blob_reader_pem_iface_init (OstreeBlobReaderInterface *iface)
+{
+ iface->read_blob = ostree_blob_reader_pem_read_blob;
+}
+
+static void
+ostree_blob_reader_pem_set_property (GObject *object, guint prop_id, const GValue *value,
+ GParamSpec *pspec)
+{
+ OstreeBlobReaderPem *self = OSTREE_BLOB_READER_PEM (object);
+
+ switch (prop_id)
+ {
+ case PROP_LABEL:
+ self->label = g_value_dup_string (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ostree_blob_reader_pem_get_property (GObject *object, guint prop_id, GValue *value,
+ GParamSpec *pspec)
+{
+ OstreeBlobReaderPem *self = OSTREE_BLOB_READER_PEM (object);
+
+ switch (prop_id)
+ {
+ case PROP_LABEL:
+ g_value_set_string (value, self->label);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ostree_blob_reader_pem_finalize (GObject *object)
+{
+ OstreeBlobReaderPem *self = OSTREE_BLOB_READER_PEM (object);
+
+ g_free (self->label);
+
+ G_OBJECT_CLASS (_ostree_blob_reader_pem_parent_class)->finalize (object);
+}
+
+static void
+_ostree_blob_reader_pem_class_init (OstreeBlobReaderPemClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->set_property = ostree_blob_reader_pem_set_property;
+ gobject_class->get_property = ostree_blob_reader_pem_get_property;
+ gobject_class->finalize = ostree_blob_reader_pem_finalize;
+
+ /*
+ * OstreeBlobReaderPem:label:
+ *
+ * The label to filter the PEM blocks.
+ */
+ g_object_class_install_property (
+ gobject_class, PROP_LABEL,
+ g_param_spec_string ("label", "", "", "", G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+}
+
+static void
+_ostree_blob_reader_pem_init (OstreeBlobReaderPem *self)
+{
+}
+
+OstreeBlobReaderPem *
+_ostree_blob_reader_pem_new (GInputStream *base, const gchar *label)
+{
+ g_assert (G_IS_INPUT_STREAM (base));
+
+ return g_object_new (OSTREE_TYPE_BLOB_READER_PEM, "base-stream", base, "label", label, NULL);
+}
+
+enum PemInputState
+{
+ PEM_INPUT_STATE_OUTER,
+ PEM_INPUT_STATE_INNER
+};
+
+#define PEM_SUFFIX "-----"
+#define PEM_PREFIX_BEGIN "-----BEGIN "
+#define PEM_PREFIX_END "-----END "
+
+GBytes *
+_ostree_read_pem_block (GDataInputStream *stream, gchar **label, GCancellable *cancellable,
+ GError **error)
+{
+ enum PemInputState state = PEM_INPUT_STATE_OUTER;
+ g_autofree gchar *tmp_label = NULL;
+ g_autoptr (GString) buf = g_string_new ("");
+
+ while (TRUE)
+ {
+ g_autofree gchar *line = NULL;
+ gsize length;
+
+ line = g_data_input_stream_read_line (stream, &length, cancellable, error);
+ if (!line)
+ break;
+
+ line = g_strstrip (line);
+ if (*line == '\0')
+ continue;
+
+ switch (state)
+ {
+ case PEM_INPUT_STATE_OUTER:
+ if (g_str_has_prefix (line, PEM_PREFIX_BEGIN) && g_str_has_suffix (line, PEM_SUFFIX))
+ {
+ const gchar *start = line + sizeof (PEM_PREFIX_BEGIN) - 1;
+ const gchar *end = g_strrstr (start + 1, PEM_SUFFIX);
+
+ tmp_label = g_strndup (start, end - start);
+ state = PEM_INPUT_STATE_INNER;
+ }
+ break;
+
+ case PEM_INPUT_STATE_INNER:
+ if (g_str_has_prefix (line, PEM_PREFIX_END) && g_str_has_suffix (line, PEM_SUFFIX))
+ {
+ const gchar *start = line + sizeof (PEM_PREFIX_END) - 1;
+ const gchar *end = g_strrstr (start + 1, PEM_SUFFIX);
+
+ if (strncmp (tmp_label, start, end - start) != 0)
+ {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "Unmatched PEM header");
+ return NULL;
+ }
+
+ g_base64_decode_inplace (buf->str, &buf->len);
+ GBytes *result = g_bytes_new_take (buf->str, buf->len);
+
+ /* Don't leak the trailing encoded bytes */
+ explicit_bzero (buf->str + buf->len, buf->allocated_len - buf->len);
+ g_string_free (buf, FALSE);
+ buf = NULL;
+
+ if (label)
+ *label = g_steal_pointer (&tmp_label);
+
+ return result;
+ }
+ else
+ g_string_append (buf, line);
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+ }
+
+ return NULL;
+}
+
+GBytes *
+ostree_blob_reader_pem_read_blob (OstreeBlobReader *self, GCancellable *cancellable, GError **error)
+{
+ g_autofree gchar *label = NULL;
+ g_autoptr (GBytes) blob = NULL;
+ OstreeBlobReaderPem *pself = OSTREE_BLOB_READER_PEM (self);
+
+ blob = _ostree_read_pem_block (G_DATA_INPUT_STREAM (self), &label, cancellable, error);
+ if (blob != NULL && !g_str_equal (label, pself->label))
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Unexpected label \"%s\"", label);
+ g_clear_pointer (&blob, g_bytes_unref);
+ }
+ return g_steal_pointer (&blob);
+}
diff --git a/src/libostree/ostree-blob-reader-pem.h b/src/libostree/ostree-blob-reader-pem.h
new file mode 100644
index 0000000000..04efb99d5d
--- /dev/null
+++ b/src/libostree/ostree-blob-reader-pem.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 Red Hat, Inc.
+ *
+ * SPDX-License-Identifier: LGPL-2.0+
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see .
+ */
+
+#pragma once
+
+#include "ostree-blob-reader.h"
+
+G_BEGIN_DECLS
+
+#define OSTREE_TYPE_BLOB_READER_PEM (_ostree_blob_reader_pem_get_type ())
+
+_OSTREE_PUBLIC
+G_DECLARE_FINAL_TYPE (OstreeBlobReaderPem, _ostree_blob_reader_pem, OSTREE, BLOB_READER_PEM,
+ GDataInputStream);
+
+_OSTREE_PUBLIC
+OstreeBlobReaderPem *_ostree_blob_reader_pem_new (GInputStream *base, const gchar *label);
+
+_OSTREE_PUBLIC
+GBytes *ostree_blob_reader_pem_read_blob (OstreeBlobReader *self, GCancellable *cancellable,
+ GError **error);
+
+G_END_DECLS
diff --git a/src/libostree/ostree-blob-reader-private.h b/src/libostree/ostree-blob-reader-private.h
new file mode 100644
index 0000000000..52b589be65
--- /dev/null
+++ b/src/libostree/ostree-blob-reader-private.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 Red Hat, Inc.
+ *
+ * SPDX-License-Identifier: LGPL-2.0+
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see .
+ */
+
+#pragma once
+
+#ifndef __GI_SCANNER__
+
+#include
+
+G_BEGIN_DECLS
+
+GBytes *_ostree_read_pem_block (GDataInputStream *stream, gchar **label, GCancellable *cancellable,
+ GError **error);
+
+G_END_DECLS
+
+#endif
diff --git a/tests/test-pem.c b/tests/test-pem.c
new file mode 100644
index 0000000000..1bee2963fe
--- /dev/null
+++ b/tests/test-pem.c
@@ -0,0 +1,70 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.0+
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library. If not, see .
+ */
+
+#include "config.h"
+
+#include
+
+#include "ostree-blob-reader-private.h"
+
+static const gchar pem_pubkey_ed25519[]
+ = "-----BEGIN PUBLIC KEY-----\n"
+ "MCowBQYDK2VwAyEANgkGafNSseN+1LXjTFJrfdu6N2qs5rlf9d3xlaVclgk=\n"
+ "-----END PUBLIC KEY-----\n";
+static const guint8 pubkey_ed25519[]
+ = { 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, 0x36, 0x09, 0x06,
+ 0x69, 0xf3, 0x52, 0xb1, 0xe3, 0x7e, 0xd4, 0xb5, 0xe3, 0x4c, 0x52, 0x6b, 0x7d, 0xdb, 0xba,
+ 0x37, 0x6a, 0xac, 0xe6, 0xb9, 0x5f, 0xf5, 0xdd, 0xf1, 0x95, 0xa5, 0x5c, 0x96, 0x09 };
+
+static const struct
+{
+ const gchar *pem_data;
+ gsize pem_size;
+ const guint8 *data;
+ gsize size;
+} tests[] = {
+ { pem_pubkey_ed25519, sizeof (pem_pubkey_ed25519), pubkey_ed25519, sizeof (pubkey_ed25519) },
+};
+
+static void
+test_ostree_read_pem_block (void)
+{
+ for (gsize i = 0; i < G_N_ELEMENTS (tests); i++)
+ {
+ g_autoptr (GInputStream) stream = NULL;
+ g_autoptr (GDataInputStream) data_stream = NULL;
+ g_autofree char *label = NULL;
+ g_autoptr (GBytes) bytes = NULL;
+ g_autoptr (GError) error = NULL;
+ g_autoptr (GBytes) expected_bytes = NULL;
+
+ stream = g_memory_input_stream_new_from_data (tests[i].pem_data, tests[i].pem_size, NULL);
+ data_stream = g_data_input_stream_new (stream);
+
+ bytes = _ostree_read_pem_block (data_stream, &label, NULL, &error);
+ expected_bytes = g_bytes_new_static (tests[i].data, tests[i].size);
+ g_assert (g_bytes_equal (bytes, expected_bytes));
+ }
+}
+
+int
+main (int argc, char **argv)
+{
+ g_test_init (&argc, &argv, NULL);
+ g_test_add_func ("/ostree_read_pem_block", test_ostree_read_pem_block);
+ return g_test_run ();
+}