aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKyle Evans <kevans@FreeBSD.org>2024-12-17 04:16:27 +0000
committerKyle Evans <kevans@FreeBSD.org>2024-12-17 04:16:27 +0000
commitefff9f8a94ab4118bc5f5a1a65bf6938f4dedadf (patch)
tree8807f99f562557b030855432e6399c20869132d4
Import libder v1.0vendor/libder/1.0
libder will be used by pkg(7) to read DER-encoded keys and signatures in the upcoming ECC support.
-rw-r--r--.cirrus.yml16
-rw-r--r--.github/workflows/build.yml41
-rw-r--r--.gitignore11
-rw-r--r--1.0.tar.gzbin0 -> 32406 bytes
-rw-r--r--CMakeLists.txt28
-rw-r--r--LICENSE22
-rw-r--r--README.md28
-rw-r--r--derdump/.gitignore1
-rw-r--r--derdump/CMakeLists.txt6
-rw-r--r--derdump/derdump.151
-rw-r--r--derdump/derdump.c52
-rw-r--r--libder/CMakeLists.txt12
-rw-r--r--libder/libder.3179
-rw-r--r--libder/libder.c119
-rw-r--r--libder/libder.h181
-rw-r--r--libder/libder_error.c76
-rw-r--r--libder/libder_obj.3138
-rw-r--r--libder/libder_obj.c1192
-rw-r--r--libder/libder_private.h178
-rw-r--r--libder/libder_read.3101
-rw-r--r--libder/libder_read.c864
-rw-r--r--libder/libder_type.371
-rw-r--r--libder/libder_type.c150
-rw-r--r--libder/libder_write.354
-rw-r--r--libder/libder_write.c229
-rw-r--r--tests/.gitignore12
-rw-r--r--tests/CMakeLists.txt41
-rw-r--r--tests/fuzz_parallel.c111
-rw-r--r--tests/fuzz_stream.c246
-rw-r--r--tests/fuzz_write.c79
-rw-r--r--tests/fuzzers.h40
-rw-r--r--tests/make_corpus.c137
-rw-r--r--tests/repo.privbin0 -> 64 bytes
-rw-r--r--tests/repo.pubbin0 -> 88 bytes
-rw-r--r--tests/test_common.h29
-rw-r--r--tests/test_privkey.c175
-rw-r--r--tests/test_pubkey.c143
37 files changed, 4813 insertions, 0 deletions
diff --git a/.cirrus.yml b/.cirrus.yml
new file mode 100644
index 000000000000..a63de71d8bf4
--- /dev/null
+++ b/.cirrus.yml
@@ -0,0 +1,16 @@
+build_task:
+ matrix:
+ - name: FreeBSD 13
+ freebsd_instance:
+ image: freebsd-13-2-release-amd64
+ - name: FreeBSD 14
+ freebsd_instance:
+ image: freebsd-14-0-release-amd64-ufs
+ setup_script:
+ sudo pkg install -y cmake
+ configure_script:
+ - cmake -B build -DCMAKE_BUILD_TYPE=Debug
+ build_script:
+ make -C build
+ test_script:
+ make -C build check
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 000000000000..a10daa25e38f
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,41 @@
+name: Build libder
+on:
+ push:
+ branches: ['**']
+ pull_request:
+ types: [opened, reopened, edited, synchronize]
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ name: Build ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-20.04, ubuntu-22.04, macos-latest]
+ include:
+ - os: ubuntu-20.04
+ - os: ubuntu-22.04
+ - os: macos-latest
+ steps:
+ - name: checkout
+ uses: actions/checkout@v4
+ - name: install system packages (Ubuntu)
+ if: runner.os == 'Linux'
+ run: |
+ sudo apt-get update --quiet || true
+ sudo apt-get -yq --no-install-suggests --no-install-recommends install cmake
+ - name: install system packages (macOS)
+ if: runner.os == 'macOS'
+ run: |
+ brew update --quiet || true
+ brew install cmake coreutils
+ - name: configure
+ run: |
+ cmake -B build -DCMAKE_BUILD_TYPE=Debug
+ - name: build libder
+ run: make -C build
+ - name: Run self-tests
+ run: make -C build check
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000000..34fb4e06c50b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+.*.swp
+.depend*
+*.a
+*.so
+*.so.*
+*.o
+*.pico
+*.debug
+*.full
+
+build/
diff --git a/1.0.tar.gz b/1.0.tar.gz
new file mode 100644
index 000000000000..63cb6ee1df5b
--- /dev/null
+++ b/1.0.tar.gz
Binary files differ
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 000000000000..cf0d39e32489
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,28 @@
+cmake_minimum_required(VERSION 3.18)
+
+project(libder)
+
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+ if(NOT CMAKE_SYSTEM_NAME STREQUAL "OpenBSD")
+ add_compile_options(-fsanitize=address,undefined -fstrict-aliasing)
+ add_link_options(-fsanitize=address,undefined -fstrict-aliasing)
+ endif()
+
+ add_compile_options(-Werror)
+endif()
+
+# AppleClang is excluded for the time being; the version used in GitHub Action
+# runners doesn't seem to have that part of libclang_rt installed, though the
+# -fsanitize=fuzzer-no-link instrumentation seems to be fine. Maybe re-evaluate
+# this for MATCHES as a possibility later.
+if(CMAKE_C_COMPILER_ID STREQUAL "Clang" AND NOT CMAKE_SYSTEM_NAME STREQUAL "OpenBSD")
+ set(BUILD_FUZZERS TRUE
+ CACHE BOOL "Build the libFuzzer fuzzers (needs llvm)")
+else()
+ set(BUILD_FUZZERS FALSE
+ CACHE BOOL "Build the libFuzzer fuzzers (needs llvm)")
+endif()
+
+add_subdirectory(libder)
+add_subdirectory(derdump)
+add_subdirectory(tests)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000000..477af8f22e4c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 000000000000..9f700493520d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,28 @@
+# libder
+
+## What is libder?
+
+libder is a small library for encoding/decoding DER-encoded objects. It is
+expected to be able to decode any BER-encoded buffer, and an attempt to
+re-encode the resulting tree would apply any normalization expected by a DER
+decoder. The author's use is primarily to decode/encode ECC keys for
+interoperability with OpenSSL.
+
+The authoritative source for this software is located at
+https://git.kevans.dev/kevans/libder, but it's additionally mirrored to
+[GitHub](https://github.com/kevans91/libder) for user-facing interactions.
+Pull requests and issues are open on GitHub.
+
+## What is libder not?
+
+libder is not intended to be a general-purpose library for working with DER/BER
+specified objects. It may provide some helpers for building more primitive
+data types, but libder will quickly punt on anything even remotely complex and
+require the library consumer to supply it as a type/payload/size triple that it
+will treat as relatively opaque (modulo some encoding normalization rules that
+can be applied without deeply understanding the data contained within).
+
+libder also doesn't do strict validation of what it reads in today, for better
+or worse. e.g., a boolean may occupy more than one byte and libder will happily
+present it to the application in that way. It would be normalized on
+re-encoding to 0xff or 0x00 depending on whether any bits are set or not.
diff --git a/derdump/.gitignore b/derdump/.gitignore
new file mode 100644
index 000000000000..a35adcc4b71d
--- /dev/null
+++ b/derdump/.gitignore
@@ -0,0 +1 @@
+derdump
diff --git a/derdump/CMakeLists.txt b/derdump/CMakeLists.txt
new file mode 100644
index 000000000000..11657426fbc9
--- /dev/null
+++ b/derdump/CMakeLists.txt
@@ -0,0 +1,6 @@
+file(GLOB derdump_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*.c)
+
+add_executable(derdump ${derdump_SOURCES})
+
+target_include_directories(derdump PRIVATE "${CMAKE_SOURCE_DIR}/libder")
+target_link_libraries(derdump der_static)
diff --git a/derdump/derdump.1 b/derdump/derdump.1
new file mode 100644
index 000000000000..414799f3055f
--- /dev/null
+++ b/derdump/derdump.1
@@ -0,0 +1,51 @@
+.\"
+.\" SPDX-Copyright-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (C) 2024 Kyle Evans <kevans@FreeBSD.org>
+.\"
+.Dd March 4, 2024
+.Dt DERDUMP 1
+.Os
+.Sh NAME
+.Nm derdump
+.Nd dumping contents of DER encoded files
+.Sh SYNOPSIS
+.Nm
+.Ar file1
+.Oo Ar fileN ... Oc
+.Sh DESCRIPTION
+The
+.Nm
+utility dumps the contents of one or more DER encoded
+Ar file
+in a more human readable format.
+This is similar to the
+.Xr asn1parse 1
+utility distributed with OpenSSL when used with the
+.Fl inform
+.Ar DER
+option.
+.Pp
+A representation of the object will be output to
+.Em stdout ,
+with indentation to denote objects that are encoded within other constructed
+objects.
+Note that
+.Nm
+does not make much attempt to interpret the contents of any particular object.
+If an object uses one of the universal types, then a friendly name will be
+displayed for that object.
+If an object uses any other type, then
+.Nm
+will display the raw hex value of the type used.
+Values of primitive objects are output as raw hex, and no effort is made to
+try and print a friendly representation.
+.Sh SEE ALSO
+.Xr asn1parse 1 ,
+.Xr libder 3
+.Sh BUGS
+.Nm
+does not currently make any attempt to render a type that uses the long encoded
+format.
+Instead, it will render as
+.Dq { ... } .
diff --git a/derdump/derdump.c b/derdump/derdump.c
new file mode 100644
index 000000000000..7ea3768524d8
--- /dev/null
+++ b/derdump/derdump.c
@@ -0,0 +1,52 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <err.h>
+#include <stdio.h>
+
+#include <libder.h>
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp;
+ struct libder_ctx *ctx;
+ struct libder_object *root;
+ size_t rootsz;
+ bool first = true;
+
+ if (argc < 2) {
+ fprintf(stderr, "usage: %s file [file...]\n", argv[0]);
+ return (1);
+ }
+
+ ctx = libder_open();
+ libder_set_verbose(ctx, 2);
+ for (int i = 1; i < argc; i++) {
+ fp = fopen(argv[i], "rb");
+ if (fp == NULL) {
+ warn("%s", argv[i]);
+ continue;
+ }
+
+ if (!first)
+ fprintf(stderr, "\n");
+ fprintf(stdout, "[%s]\n", argv[i]);
+ root = libder_read_file(ctx, fp, &rootsz);
+ if (root != NULL) {
+ libder_obj_dump(root, stdout);
+ libder_obj_free(root);
+ root = NULL;
+ }
+
+ first = false;
+ fclose(fp);
+ }
+
+ libder_close(ctx);
+
+ return (0);
+}
diff --git a/libder/CMakeLists.txt b/libder/CMakeLists.txt
new file mode 100644
index 000000000000..8e6f3426d649
--- /dev/null
+++ b/libder/CMakeLists.txt
@@ -0,0 +1,12 @@
+file(GLOB libder_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*.c)
+
+add_library(der SHARED ${libder_SOURCES})
+add_library(der_static STATIC ${libder_SOURCES})
+
+if(BUILD_FUZZERS AND CMAKE_BUILD_TYPE STREQUAL "Debug")
+ target_compile_options(der PUBLIC -fsanitize=fuzzer-no-link)
+ target_link_options(der PUBLIC -fsanitize=fuzzer-no-link)
+
+ target_compile_options(der_static PUBLIC -fsanitize=fuzzer-no-link)
+ target_link_options(der_static PUBLIC -fsanitize=fuzzer-no-link)
+endif()
diff --git a/libder/libder.3 b/libder/libder.3
new file mode 100644
index 000000000000..0e06254ef3fb
--- /dev/null
+++ b/libder/libder.3
@@ -0,0 +1,179 @@
+.\"
+.\" SPDX-Copyright-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (C) 2024 Kyle Evans <kevans@FreeBSD.org>
+.\"
+.Dd March 2, 2024
+.Dt LIBDER 3
+.Os
+.Sh NAME
+.Nm libder ,
+.Nm libder_open ,
+.Nm libder_close ,
+.Nm libder_abort ,
+.Nm libder_get_error ,
+.Nm libder_has_error ,
+.Nm libder_get_normalize ,
+.Nm libder_set_normalize ,
+.Nm libder_get_strict ,
+.Nm libder_set_strict ,
+.Nm libder_get_verbose ,
+.Nm libder_set_verbose
+.Nd DER encoding and decoding library
+.Sh LIBRARY
+.Lb libder
+.Sh SYNOPSIS
+.In libder.h
+.Ft struct libder_ctx *
+.Fn libder_open "void"
+.Ft void
+.Fn libder_close "struct libder_ctx *ctx"
+.Ft void
+.Fn libder_abort "struct libder_ctx *ctx"
+.Ft const char *
+.Fn libder_get_error "struct libder_ctx *ctx"
+.Ft bool
+.Fn libder_has_error "struct libder_ctx *ctx"
+.Ft uint64_t
+.Fn libder_get_normalize "struct libder_ctx *ctx"
+.Ft uint64_t
+.Fn libder_set_normalize "struct libder_ctx *ctx" "uint64_t normalize"
+.Ft bool
+.Fn libder_get_strict "struct libder_ctx *ctx"
+.Ft bool
+.Fn libder_set_strict "struct libder_ctx *ctx" "bool strict"
+.Ft int
+.Fn libder_get_verbose "struct libder_ctx *ctx"
+.Ft int
+.Fn libder_set_verbose "struct libder_ctx *ctx" "int verbose"
+.Sh DESCRIPTION
+The
+.Nm
+library provides functionality for decoding BER and DER encoded data, and
+DER encoding data subjected to constraints outline in ITU-T
+Recommendation X.690.
+.Nm
+will apply relevant normalization rules on write, unless they've been disabled
+with
+.Ft libder_set_normalize ,
+under the assumption that it may not be reading strictly DER encoded data.
+.Pp
+Note that not all of the DER rules are currently implemented.
+.Nm
+will coalesce constructed types that DER specifies should be primitive.
+.Nm
+will primarily normalize bitstrings, booleans, and integers.
+This library was primarily written to be able to provide interoperability with
+OpenSSL keys and signatures, so the library was written with that in mind.
+Eventually it is intended that
+.Nm
+will support the full set of rules, but currently some responsibility is left
+to the library user.
+.Pp
+Also note that
+.Nm
+does not necessarily provide
+.Dq neat
+ways to construct primitives.
+For example, even booleans and integers currently work just by providing a
+buffer that is expected to be formatted in a sane fashion.
+The library user is expected to build the object tree and generally provide the
+object data in a format reasonably encoded as the data for that type should be,
+then
+.Nm
+will provide the proper framing on write and do any transformations that may
+need to be done for strict conformance.
+.Pp
+The
+.Fn libder_open
+function allocates a new
+.Nm
+context.
+The context does not hold any state about any particular structure.
+All of the state held in the context is generally described in this manpage.
+The
+.Fn libder_close
+function will free the context.
+.Pp
+The
+.Fn libder_abort
+function will abort an in-progress
+.Xr libder_read_fd 3
+operation on the existing
+.Fa ctx
+if it is interrupted by a signal in the middle of a
+.Xr read 2
+syscall.
+See
+.Xr libder_read_fd 3
+for further discussion.
+.Pp
+The
+.Fn libder_get_error
+function will return an error string appropriate for the current error, if any.
+The
+.Fn libder_has_error
+function can be used to check if an error was raised in a previous operation.
+.Pp
+The
+.Fn libder_get_normalize
+and
+.Fn libder_set_normalize
+functions retrieve and manipulate any number of flags that detail how
+functions may be used to check or set the normalization flags given
+.Nm context ,
+which dictates how
+.Nm
+will normalize data on write.
+The following normalization flags may be specified:
+.Bl -column "LIBDER_NORMALIZE_CONSTRUCTED"
+.It LIBDER_NORMALIZE_CONSTRUCTED Ta Coalesce types that may be primitive or constructed
+.It LIBDER_NORMALIZE_TAGS Ta Pack tags into the lowest possible encoded value
+.El
+.Pp
+The
+.Fn LIBDER_NORMALIZE_TYPE_FLAG "enum libder_ber_type"
+macaro may also be used to specify normalization of the given universal type.
+By default, every valid normalization flag is enabled.
+.Pp
+The
+.Fn libder_get_strict
+and
+.Fn libder_set_strict
+functions may used to check or set the strict read state of the given
+.Nm
+context.
+By default,
+.Nm
+operates in strict mode and rejects various methods of expressing data that are
+valid looking but not strictly conformant.
+The
+.Va LDE_STRICT_*
+constants in
+.In libder.h
+describe the various scenarios that strict mode may reject.
+.Pp
+The
+.Fn libder_get_verbose
+and
+.Fn libder_set_verbose
+functions may be used to check or set the verbosity of the given
+.Nm
+context.
+This primarily controls how
+.Nm
+behaves when an error is encountered.
+By default, the library will silently set the error state and return.
+With a verbosity level of 1, an error will be printed when the error state is
+set that contains the string that would be returned by
+.Fn libder_get_error .
+With a verbosity level of 2, the filename and line within
+.Nm
+that the error occurred in will be printed, which is primarily intended for
+debugging
+.Nm .
+.Sh SEE ALSO
+.Xr libder_obj 3 ,
+.Xr libder_read 3 ,
+.Xr libder_type 3 ,
+.Xr libder_write 3
diff --git a/libder/libder.c b/libder/libder.c
new file mode 100644
index 000000000000..2d52fedd62bd
--- /dev/null
+++ b/libder/libder.c
@@ -0,0 +1,119 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "libder_private.h"
+
+#include <stdlib.h>
+#include <unistd.h>
+
+/*
+ * Sets up the context, returns NULL on error.
+ */
+struct libder_ctx *
+libder_open(void)
+{
+ struct libder_ctx *ctx;
+
+ ctx = malloc(sizeof(*ctx));
+ if (ctx == NULL)
+ return (NULL);
+
+ /* Initialize */
+ ctx->error = LDE_NONE;
+ ctx->buffer_size = 0;
+ ctx->verbose = 0;
+ ctx->normalize = LIBDER_NORMALIZE_ALL;
+ ctx->strict = true;
+ ctx->abort = 0;
+
+ return (ctx);
+}
+
+void
+libder_abort(struct libder_ctx *ctx)
+{
+
+ ctx->abort = 1;
+}
+
+LIBDER_PRIVATE size_t
+libder_get_buffer_size(struct libder_ctx *ctx)
+{
+
+ if (ctx->buffer_size == 0) {
+ long psize;
+
+ psize = sysconf(_SC_PAGESIZE);
+ if (psize <= 0)
+ psize = 4096;
+
+ ctx->buffer_size = psize;
+ }
+
+ return (ctx->buffer_size);
+}
+
+uint64_t
+libder_get_normalize(struct libder_ctx *ctx)
+{
+
+ return (ctx->normalize);
+}
+
+/*
+ * Set the normalization flags; returns the previous value.
+ */
+uint64_t
+libder_set_normalize(struct libder_ctx *ctx, uint64_t nmask)
+{
+ uint64_t old = ctx->normalize;
+
+ ctx->normalize = (nmask & LIBDER_NORMALIZE_ALL);
+ return (old);
+}
+
+bool
+libder_get_strict(struct libder_ctx *ctx)
+{
+
+ return (ctx->strict);
+}
+
+bool
+libder_set_strict(struct libder_ctx *ctx, bool strict)
+{
+ bool oval = ctx->strict;
+
+ ctx->strict = strict;
+ return (oval);
+}
+
+int
+libder_get_verbose(struct libder_ctx *ctx)
+{
+
+ return (ctx->verbose);
+}
+
+int
+libder_set_verbose(struct libder_ctx *ctx, int verbose)
+{
+ int oval = ctx->verbose;
+
+ ctx->verbose = verbose;
+ return (oval);
+}
+
+void
+libder_close(struct libder_ctx *ctx)
+{
+
+ if (ctx == NULL)
+ return;
+
+ free(ctx);
+}
+
diff --git a/libder/libder.h b/libder/libder.h
new file mode 100644
index 000000000000..4d28aa3052ba
--- /dev/null
+++ b/libder/libder.h
@@ -0,0 +1,181 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stddef.h>
+
+enum libder_ber_class {
+ BC_UNIVERSAL = 0,
+ BC_APPLICATION = 1,
+ BC_CONTEXT = 2,
+ BC_PRIVATE = 3,
+};
+
+enum libder_ber_type {
+ BT_RESERVED = 0x00,
+ BT_BOOLEAN = 0x01,
+ BT_INTEGER = 0x02,
+ BT_BITSTRING = 0x03,
+ BT_OCTETSTRING = 0x04,
+ BT_NULL = 0x05,
+ BT_OID = 0x06,
+ BT_OBJDESC = 0x07,
+ BT_EXTERNAL = 0x08,
+ BT_REAL = 0x09,
+ BT_ENUMERATED = 0x0a,
+ BT_PDV = 0x0b,
+ BT_UTF8STRING = 0x0c,
+ BT_RELOID = 0x0d,
+
+ /* 0x10, 011 not usable */
+
+ BT_NUMERICSTRING = 0x012,
+ BT_STRING = 0x13,
+ BT_TELEXSTRING = 0x14,
+ BT_VIDEOTEXSTRING = 0x15,
+ BT_IA5STRING = 0x16,
+ BT_UTCTIME = 0x17,
+ BT_GENTIME = 0x18,
+ BT_GFXSTRING = 0x19,
+ BT_VISSTRING = 0x1a,
+ BT_GENSTRING = 0x1b,
+ BT_UNIVSTRING = 0x1c,
+ BT_CHARSTRING = 0x1d,
+ BT_BMPSTRING = 0x1e,
+
+ BT_SEQUENCE = 0x30,
+ BT_SET = 0x31,
+};
+
+#define BER_TYPE_CONSTRUCTED_MASK 0x20 /* Bit 6 */
+#define BER_TYPE_CLASS_MASK 0xc0 /* Bits 7 and 8 */
+
+/*
+ * The difference between the type and the full type is just that the full type
+ * will indicate the class of type, so it may be more useful for some operations.
+ */
+#define BER_FULL_TYPE(tval) \
+ ((tval) & ~(BER_TYPE_CONSTRUCTED_MASK))
+#define BER_TYPE(tval) \
+ ((tval) & ~(BER_TYPE_CLASS_MASK | BER_TYPE_CONSTRUCTED_MASK))
+#define BER_TYPE_CLASS(tval) \
+ (((tval) & BER_TYPE_CLASS_MASK) >> 6)
+#define BER_TYPE_CONSTRUCTED(tval) \
+ (((tval) & BER_TYPE_CONSTRUCTED_MASK) != 0)
+
+enum libder_error {
+ LDE_NONE = 0x00,
+ LDE_NOMEM, /* Out of memory */
+ LDE_INVAL, /* Invalid parameter */
+ LDE_SHORTHDR, /* Header too short */
+ LDE_BADVARLEN, /* Bad variable length encoding */
+ LDE_LONGLEN, /* Encoded length too large (8 byte max) */
+ LDE_SHORTDATA, /* Payload not available */
+ LDE_GARBAGE, /* Garbage after encoded data */
+ LDE_STREAMERR, /* Stream error */
+ LDE_TRUNCVARLEN, /* Variable length object truncated */
+ LDE_COALESCE_BADCHILD, /* Bad child encountered when coalescing */
+ LDE_BADOBJECT, /* Payload not valid for object type */
+
+ /* Strict violations */
+ LDE_STRICT_EOC, /* Strict: end-of-content violation */
+ LDE_STRICT_TAG, /* Strict: tag violation */
+ LDE_STRICT_PVARLEN, /* Strict: primitive using indefinite length */
+ LDE_STRICT_BOOLEAN, /* Strict: boolean encoded incorrectly */
+ LDE_STRICT_NULL, /* Strict: null encoded incorrectly */
+ LDE_STRICT_PRIMITIVE, /* Strict: type must be primitive */
+ LDE_STRICT_CONSTRUCTED, /* Strict: type must be constructed */
+ LDE_STRICT_BITSTRING, /* Strict: malformed constructed bitstring */
+};
+
+struct libder_ctx;
+struct libder_tag;
+struct libder_object;
+
+/*
+ * By default we normalize everything, but we allow some subset of the
+ * functionality to be disabled. Lengths are non-optional and will always be
+ * normalized to a fixed short or long length. The upper 32-bits of
+ * ctx->normalize are reserved for universal types so that we can quickly map
+ * those without assigning them names.
+ */
+
+/* Normalize constructed types that should be coalesced (e.g., strings, time). */
+#define LIBDER_NORMALIZE_CONSTRUCTED 0x0000000000000001ULL
+
+/*
+ * Normalize tags on read. This is mostly a measure to ensure that
+ * normalization on write doesn't get thwarted; there's no reason anybody should
+ * be encoding low tags with the long form, but the spec doesn't appear to
+ * forbid it.
+ */
+#define LIBDER_NORMALIZE_TAGS 0x0000000000000002ULL
+
+/* Universal types (reserved) */
+#define LIBDER_NORMALIZE_TYPE_MASK 0xffffffff00000000ULL
+#define LIBDER_NORMALIZE_TYPE_FLAG(val) ((1ULL << val) << 32ULL)
+
+/* All valid bits. */
+#define LIBDER_NORMALIZE_ALL \
+ (LIBDER_NORMALIZE_TYPE_MASK | LIBDER_NORMALIZE_CONSTRUCTED | \
+ LIBDER_NORMALIZE_TAGS)
+
+struct libder_ctx * libder_open(void);
+void libder_close(struct libder_ctx *);
+void libder_abort(struct libder_ctx *);
+const char *libder_get_error(struct libder_ctx *);
+bool libder_has_error(struct libder_ctx *);
+uint64_t libder_get_normalize(struct libder_ctx *);
+uint64_t libder_set_normalize(struct libder_ctx *, uint64_t);
+bool libder_get_strict(struct libder_ctx *);
+bool libder_set_strict(struct libder_ctx *, bool);
+int libder_get_verbose(struct libder_ctx *);
+int libder_set_verbose(struct libder_ctx *, int);
+
+struct libder_object *libder_read(struct libder_ctx *, const uint8_t *, size_t *);
+struct libder_object *libder_read_fd(struct libder_ctx *, int, size_t *);
+struct libder_object *libder_read_file(struct libder_ctx *, FILE *, size_t *);
+
+uint8_t *libder_write(struct libder_ctx *, struct libder_object *, uint8_t *,
+ size_t *);
+
+#define DER_CHILDREN(obj) libder_obj_children(obj)
+#define DER_NEXT(obj) libder_obj_next(obj)
+
+#define DER_FOREACH_CHILD(var, obj) \
+ for ((var) = DER_CHILDREN((obj)); \
+ (var); \
+ (var) = DER_NEXT((var)))
+#define DER_FOREACH_CHILD_SAFE(var, obj, tvar) \
+ for ((var) = DER_CHILDREN((obj)); \
+ (var) && ((tvar) = DER_NEXT((var)), 1); \
+ (var) = (tvar))
+
+struct libder_object *libder_obj_alloc(struct libder_ctx *, struct libder_tag *, const uint8_t *, size_t);
+struct libder_object *libder_obj_alloc_simple(struct libder_ctx *, uint8_t, const uint8_t *,
+ size_t);
+void libder_obj_free(struct libder_object *);
+
+bool libder_obj_append(struct libder_object *, struct libder_object *);
+struct libder_object *libder_obj_child(const struct libder_object *, size_t);
+struct libder_object *libder_obj_children(const struct libder_object *);
+struct libder_object *libder_obj_next(const struct libder_object *);
+struct libder_tag *libder_obj_type(const struct libder_object *);
+uint8_t libder_obj_type_simple(const struct libder_object *);
+const uint8_t *libder_obj_data(const struct libder_object *, size_t *);
+
+/* Debugging aide -- probably shouldn't use. */
+void libder_obj_dump(const struct libder_object *, FILE *);
+
+struct libder_tag *libder_type_alloc_simple(struct libder_ctx *, uint8_t);
+struct libder_tag *libder_type_dup(struct libder_ctx *, const struct libder_tag *);
+void libder_type_free(struct libder_tag *);
+#define libder_type_simple libder_type_simple_abi
+uint8_t libder_type_simple(const struct libder_tag *);
diff --git a/libder/libder_error.c b/libder/libder_error.c
new file mode 100644
index 000000000000..6ca0acc83e6d
--- /dev/null
+++ b/libder/libder_error.c
@@ -0,0 +1,76 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <stdio.h>
+
+#include "libder_private.h"
+
+#undef libder_set_error
+
+static const char libder_error_nodesc[] = "[Description not available]";
+
+#define DESCRIBE(err, msg) { LDE_ ## err, msg }
+static const struct libder_error_desc {
+ enum libder_error desc_error;
+ const char *desc_str;
+} libder_error_descr[] = {
+ DESCRIBE(NONE, "No error"),
+ DESCRIBE(NOMEM, "Out of memory"),
+ DESCRIBE(INVAL, "Invalid parameter"),
+ DESCRIBE(SHORTHDR, "Header too short"),
+ DESCRIBE(BADVARLEN, "Bad variable length encoding"),
+ DESCRIBE(LONGLEN, "Encoded length too large (8 byte max)"),
+ DESCRIBE(SHORTDATA, "Payload not available (too short)"),
+ DESCRIBE(GARBAGE, "Garbage after encoded data"),
+ DESCRIBE(STREAMERR, "Stream error"),
+ DESCRIBE(TRUNCVARLEN, "Variable length object truncated"),
+ DESCRIBE(COALESCE_BADCHILD, "Bad child encountered when coalescing"),
+ DESCRIBE(BADOBJECT, "Payload not valid for object type"),
+ DESCRIBE(STRICT_EOC, "Strict: end-of-content violation"),
+ DESCRIBE(STRICT_TAG, "Strict: tag violation"),
+ DESCRIBE(STRICT_PVARLEN, "Strict: primitive using indefinite length"),
+ DESCRIBE(STRICT_BOOLEAN, "Strict: boolean encoded incorrectly"),
+ DESCRIBE(STRICT_NULL, "Strict: null encoded incorrectly"),
+ DESCRIBE(STRICT_PRIMITIVE, "Strict: type must be primitive"),
+ DESCRIBE(STRICT_CONSTRUCTED, "Strict: type must be constructed"),
+ DESCRIBE(STRICT_BITSTRING, "Strict: malformed constructed bitstring"),
+};
+
+const char *
+libder_get_error(struct libder_ctx *ctx)
+{
+ const struct libder_error_desc *desc;
+
+ for (size_t i = 0; i < nitems(libder_error_descr); i++) {
+ desc = &libder_error_descr[i];
+
+ if (desc->desc_error == ctx->error)
+ return (desc->desc_str);
+ }
+
+ return (libder_error_nodesc);
+}
+
+bool
+libder_has_error(struct libder_ctx *ctx)
+{
+
+ return (ctx->error != 0);
+}
+
+LIBDER_PRIVATE void
+libder_set_error(struct libder_ctx *ctx, int error, const char *file, int line)
+{
+ ctx->error = error;
+
+ if (ctx->verbose >= 2) {
+ fprintf(stderr, "%s: [%s:%d]: %s (error %d)\n",
+ __func__, file, line, libder_get_error(ctx), error);
+ } else if (ctx->verbose >= 1) {
+ fprintf(stderr, "%s: %s (error %d)\n", __func__,
+ libder_get_error(ctx), error);
+ }
+}
diff --git a/libder/libder_obj.3 b/libder/libder_obj.3
new file mode 100644
index 000000000000..d7e51da1d2fb
--- /dev/null
+++ b/libder/libder_obj.3
@@ -0,0 +1,138 @@
+.\"
+.\" SPDX-Copyright-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (C) 2024 Kyle Evans <kevans@FreeBSD.org>
+.\"
+.Dd March 2, 2024
+.Dt LIBDER_OBJ 3
+.Os
+.Sh NAME
+.Nm libder_obj ,
+.Nm libder_obj_alloc ,
+.Nm libder_obj_alloc_simple ,
+.Nm libder_obj_free ,
+.Nm libder_obj_append ,
+.Nm libder_obj_child ,
+.Nm libder_obj_next ,
+.Nm libder_obj_type ,
+.Nm libder_obj_type_simple ,
+.Nm libder_obj_data ,
+.Nm libder_obj_dump
+.Nd inspecting and creating libder objects
+.Sh LIBRARY
+.Lb libder
+.Sh SYNOPSIS
+.In libder.h
+.Ft struct libder_object *
+.Fn libder_obj_alloc "struct libder_ctx *ctx" "struct libder_tag *type" "const uint8_t *data" "size_t datasz"
+.Ft struct libder_object *
+.Fn libder_obj_alloc_simple "struct libder_ctx *ctx" "uint8_t type" "const uint8_t *data" "size_t datasz"
+.Ft void
+.Fn libder_obj_free "struct libder_object *ctx"
+.Ft bool
+.Fn libder_obj_append "struct libder_object *parent" "struct libder_object *child"
+.Ft struct libder_object *
+.Fn libder_obj_child "const struct libder_object *obj" "size_t which"
+.Ft struct libder_object *
+.Fn libder_obj_next "const struct libder_object *obj"
+.Fn "DER_FOREACH_CHILD" "struct libder_obj *iter" "struct libder_obj *obj"
+.Fn "DER_FOREACH_CHILD_SAFE" "struct libder_obj *iter" "struct libder_obj *obj" "struct libder_obj *tmp"
+.Ft struct libder_tag *
+.Fn libder_obj_type "const struct libder_object *obj"
+.Ft uint8_t
+.Fn libder_obj_type_simple "const struct libder_object *obj"
+.Ft const uint8_t *
+.Fn libder_obj_data "const struct libder_object *obj" "size_t *sz"
+.Ft void
+.Fn libder_obj_dump "const struct libder_object *obj" "FILE *fp"
+.Sh DESCRIPTION
+The
+.Nm
+family of functions may be used by the application to create its own objects and
+object hierarchy, rather than reading them from an existing stream.
+.Pp
+The
+.Fn libder_obj_alloc
+and
+.Fn libder_obj_alloc_simple
+functions allocate a new object with the specified
+.Fa type
+and
+.Fa data .
+Most applications will likely prefer to use the
+.Dq simple
+variant to avoid having to manage a
+.Xr libder_type 3
+lifecycle and associated boilerplate.
+The base variant remains around for when
+.Xr libder_type 3
+grows the necessary API to create arbitrarily large tags.
+.Pp
+The
+.Fn libder_obj_append
+function is used to append
+.Fa obj
+to the
+.Fa parent
+object's children.
+For example, to add an object to a sequence.
+.Pp
+The
+.Fn libder_obj_child
+and
+.Fn libder_obj_next
+functions are used to iterate through the children of
+.Fa obj .
+The
+.Fa which
+argument to
+.Fn libder_obj_child
+specifies the index of the child requested, starting at
+.Dv 0 .
+The
+.Fn DER_FOREACH_CHILD
+and
+.Fn DER_FOREACH_CHILD_SAFE
+macros are provided for convenience.
+The difference between these two is that it is safe to free the iterator in the
+.Fn DER_FOREACH_CHILD_SAFE
+loop body.
+.Pp
+The
+.Fn libder_obj_type
+and
+.Fn libder_obj_type_simple
+functions are used to get the type information about an
+.Fa obj .
+As usual, the
+.Dq simple
+variant will return the one-byte encoding of a tag between 0 and 30.
+If the tag is actually larger than 30, then all of the lower 5 bits will be set
+to indicate that it's a long tag, and that the application should have used
+.Fn libder_obj_type
+instead.
+.Pp
+The
+.Fn libder_obj_data
+function returns a pointer to the
+.Fa data
+from
+.Fa obj ,
+and updates
+.Fa *sz
+with the data's size.
+Note that the data is not copied out here, the application is responsible for
+making its own copy of the returned buffer.
+.Pp
+The
+.Fn libder_obj_dump
+function is a debugging function that likely shouldn't be used.
+A human readable representation of the provided
+.Fa obj
+will be written to the stream
+.Fa fp .
+.Sh SEE ALSO
+.Xr libder 3 ,
+.Xr libder_read 3 ,
+.Xr libder_type 3 ,
+.Xr libder_write 3
diff --git a/libder/libder_obj.c b/libder/libder_obj.c
new file mode 100644
index 000000000000..21d39e01fc17
--- /dev/null
+++ b/libder/libder_obj.c
@@ -0,0 +1,1192 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "libder_private.h"
+
+#undef DER_CHILDREN
+#undef DER_NEXT
+
+#define DER_CHILDREN(obj) ((obj)->children)
+#define DER_NEXT(obj) ((obj)->next)
+
+static uint8_t *
+libder_obj_alloc_copy_payload(struct libder_ctx *ctx, const uint8_t *payload_in,
+ size_t length)
+{
+ uint8_t *payload;
+
+ if ((length == 0 && payload_in != NULL) ||
+ (length != 0 && payload_in == NULL)) {
+ libder_set_error(ctx, LDE_INVAL);
+ return (NULL);
+ }
+
+ if (length > 0) {
+ payload = malloc(length);
+ if (payload == NULL) {
+ libder_set_error(ctx, LDE_NOMEM);
+ return (NULL);
+ }
+
+ memcpy(payload, payload_in, length);
+ } else {
+ payload = NULL;
+ }
+
+ return (payload);
+}
+
+static bool
+libder_obj_alloc_check(struct libder_ctx *ctx, struct libder_tag *type,
+ const uint8_t *payload_in, size_t length)
+{
+ /*
+ * In addition to our normal constraints, constructed objects coming in
+ * from lib users should not have payloads.
+ */
+ if (!libder_is_valid_obj(ctx, type, payload_in, length, false) ||
+ (type->tag_constructed && length != 0)) {
+ libder_set_error(ctx, LDE_BADOBJECT);
+ return (false);
+ }
+
+ return (true);
+}
+
+struct libder_object *
+libder_obj_alloc(struct libder_ctx *ctx, struct libder_tag *type,
+ const uint8_t *payload_in, size_t length)
+{
+ struct libder_object *obj;
+ uint8_t *payload;
+
+ if (!libder_obj_alloc_check(ctx, type, payload_in, length))
+ return (NULL);
+
+ payload = libder_obj_alloc_copy_payload(ctx, payload_in, length);
+
+ obj = libder_obj_alloc_internal(ctx, type, payload, length, 0);
+ if (obj == NULL) {
+ if (length != 0) {
+ libder_bzero(payload, length);
+ free(payload);
+ }
+
+ libder_set_error(ctx, LDE_NOMEM);
+ }
+
+ return (obj);
+}
+
+struct libder_object *
+libder_obj_alloc_simple(struct libder_ctx *ctx, uint8_t stype,
+ const uint8_t *payload_in, size_t length)
+{
+ struct libder_object *obj;
+ struct libder_tag *type;
+ uint8_t *payload;
+
+ type = libder_type_alloc_simple(ctx, stype);
+ if (type == NULL)
+ return (NULL);
+
+ if (!libder_obj_alloc_check(ctx, type, payload_in, length)) {
+ libder_type_free(type);
+ return (NULL);
+ }
+
+ payload = libder_obj_alloc_copy_payload(ctx, payload_in, length);
+
+ obj = libder_obj_alloc_internal(ctx, type, payload, length, LDO_OWNTAG);
+ if (obj == NULL) {
+ if (length != 0) {
+ libder_bzero(payload, length);
+ free(payload);
+ }
+
+ libder_type_free(type);
+ libder_set_error(ctx, LDE_NOMEM);
+ }
+
+ return (obj);
+}
+
+/*
+ * Returns an obj on success, NULL if out of memory. `obj` takes ownership of
+ * the payload on success.
+ */
+LIBDER_PRIVATE struct libder_object *
+libder_obj_alloc_internal(struct libder_ctx *ctx, struct libder_tag *type,
+ uint8_t *payload, size_t length, uint32_t flags)
+{
+ struct libder_object *obj;
+
+ assert((flags & ~(LDO_OWNTAG)) == 0);
+
+ if (length != 0)
+ assert(payload != NULL);
+ else
+ assert(payload == NULL);
+
+ obj = malloc(sizeof(*obj));
+ if (obj == NULL)
+ return (NULL);
+
+ if ((flags & LDO_OWNTAG) != 0) {
+ obj->type = type;
+ } else {
+ /*
+ * Deep copies the tag data, so that the caller can predict what
+ * it can do with the buffer.
+ */
+ obj->type = libder_type_dup(ctx, type);
+ if (obj->type == NULL) {
+ free(obj);
+ return (NULL);
+ }
+ }
+
+ obj->length = length;
+ obj->payload = payload;
+ obj->children = obj->next = obj->parent = NULL;
+ obj->nchildren = 0;
+
+ return (obj);
+}
+
+LIBDER_PRIVATE size_t
+libder_size_length(size_t sz)
+{
+ size_t nbytes;
+
+ /*
+ * With DER, we use the smallest encoding necessary: less than 0x80
+ * can be encoded in one byte.
+ */
+ if (sz < 0x80)
+ return (1);
+
+ /*
+ * We can support up to 0x7f size bytes, but we don't really have a way
+ * to represent that right now. It's a good thing this function only
+ * takes a size_t, otherwise this would be a bit wrong.
+ */
+ for (nbytes = 1; nbytes < sizeof(size_t); nbytes++) {
+ if ((sz & ~((1ULL << 8 * nbytes) - 1)) == 0)
+ break;
+ }
+
+ /* Add one for the lead byte describing the length of the length. */
+ return (nbytes + 1);
+}
+
+/*
+ * Returns the size on-disk. If an object has children, we encode the size as
+ * the sum of their lengths recursively. Otherwise, we use the object's size.
+ *
+ * Returns 0 if the object size would overflow a size_t... perhaps we could
+ * lift this restriction later.
+ *
+ * Note that the size of the object will be set/updated to simplify later write
+ * calculations.
+ */
+LIBDER_PRIVATE size_t
+libder_obj_disk_size(struct libder_object *obj, bool include_header)
+{
+ struct libder_object *walker;
+ size_t disk_size, header_size;
+
+ disk_size = obj->length;
+ if (obj->children != NULL) {
+ /* We should have rejected these. */
+ assert(obj->length == 0);
+
+ DER_FOREACH_CHILD(walker, obj) {
+ size_t child_size;
+
+ child_size = libder_obj_disk_size(walker, true);
+ if (SIZE_MAX - child_size < disk_size)
+ return (0); /* Overflow */
+ disk_size += child_size;
+ }
+ }
+
+ obj->disk_size = disk_size;
+
+ /*
+ * Children always include the header above, we only include the header
+ * at the root if we're calculating how much space we need in total.
+ */
+ if (include_header) {
+ /* Size of the length + the tag (arbitrary length) */
+ header_size = libder_size_length(disk_size) + obj->type->tag_size;
+ if (obj->type->tag_encoded)
+ header_size++; /* Lead byte */
+ if (SIZE_MAX - header_size < disk_size)
+ return (0);
+
+ disk_size += header_size;
+ }
+
+ return (disk_size);
+}
+
+void
+libder_obj_free(struct libder_object *obj)
+{
+ struct libder_object *child, *tmp;
+
+ if (obj == NULL)
+ return;
+
+ DER_FOREACH_CHILD_SAFE(child, obj, tmp)
+ libder_obj_free(child);
+
+ if (obj->payload != NULL) {
+ libder_bzero(obj->payload, obj->length);
+ free(obj->payload);
+ }
+
+ libder_type_free(obj->type);
+ free(obj);
+}
+
+static void
+libder_obj_unlink(struct libder_object *obj)
+{
+ struct libder_object *child, *parent, *prev;
+
+ parent = obj->parent;
+ if (parent == NULL)
+ return;
+
+ prev = NULL;
+ assert(parent->nchildren > 0);
+ DER_FOREACH_CHILD(child, parent) {
+ if (child == obj) {
+ if (prev == NULL)
+ parent->children = child->next;
+ else
+ prev->next = child->next;
+ parent->nchildren--;
+ child->parent = NULL;
+ return;
+ }
+
+ prev = child;
+ }
+
+ assert(0 && "Internal inconsistency: parent set, but child not found");
+}
+
+bool
+libder_obj_append(struct libder_object *parent, struct libder_object *child)
+{
+ struct libder_object *end, *walker;
+
+ if (!parent->type->tag_constructed)
+ return (false);
+
+ /* XXX Type check */
+
+ if (child->parent != NULL)
+ libder_obj_unlink(child);
+
+ if (parent->nchildren == 0) {
+ parent->children = child;
+ parent->nchildren++;
+ return (true);
+ }
+
+ /* Walk the chain */
+ DER_FOREACH_CHILD(walker, parent) {
+ end = walker;
+ }
+
+ assert(end != NULL);
+ end->next = child;
+ parent->nchildren++;
+ child->parent = parent;
+ return (true);
+}
+
+struct libder_object *
+libder_obj_child(const struct libder_object *obj, size_t idx)
+{
+ struct libder_object *cur;
+
+ DER_FOREACH_CHILD(cur, obj) {
+ if (idx-- == 0)
+ return (cur);
+ }
+
+ return (NULL);
+}
+
+struct libder_object *
+libder_obj_children(const struct libder_object *obj)
+{
+
+ return (obj->children);
+}
+
+struct libder_object *
+libder_obj_next(const struct libder_object *obj)
+{
+
+ return (obj->next);
+}
+
+struct libder_tag *
+libder_obj_type(const struct libder_object *obj)
+{
+
+ return (obj->type);
+}
+
+uint8_t
+libder_obj_type_simple(const struct libder_object *obj)
+{
+ struct libder_tag *type = obj->type;
+ uint8_t simple = type->tag_class << 6;
+
+ if (type->tag_constructed)
+ simple |= BER_TYPE_CONSTRUCTED_MASK;
+
+ if (type->tag_encoded)
+ simple |= 0x1f; /* Encode the "long tag" tag. */
+ else
+ simple |= type->tag_short;
+ return (simple);
+}
+
+const uint8_t *
+libder_obj_data(const struct libder_object *obj, size_t *osz)
+{
+
+ if (obj->type->tag_constructed)
+ return (NULL);
+
+ *osz = obj->length;
+ return (obj->payload);
+}
+
+static const char *
+libder_type_name(const struct libder_tag *type)
+{
+ static char namebuf[128];
+
+ if (type->tag_encoded) {
+ return ("{ ... }");
+ }
+
+ if (type->tag_class != BC_UNIVERSAL)
+ goto fallback;
+
+#define UTYPE(val) case val: return (&(#val)[3])
+ switch (type->tag_short) {
+ UTYPE(BT_RESERVED);
+ UTYPE(BT_BOOLEAN);
+ UTYPE(BT_INTEGER);
+ UTYPE(BT_BITSTRING);
+ UTYPE(BT_OCTETSTRING);
+ UTYPE(BT_NULL);
+ UTYPE(BT_OID);
+ UTYPE(BT_OBJDESC);
+ UTYPE(BT_EXTERNAL);
+ UTYPE(BT_REAL);
+ UTYPE(BT_ENUMERATED);
+ UTYPE(BT_PDV);
+ UTYPE(BT_UTF8STRING);
+ UTYPE(BT_RELOID);
+ UTYPE(BT_NUMERICSTRING);
+ UTYPE(BT_STRING);
+ UTYPE(BT_TELEXSTRING);
+ UTYPE(BT_VIDEOTEXSTRING);
+ UTYPE(BT_IA5STRING);
+ UTYPE(BT_UTCTIME);
+ UTYPE(BT_GENTIME);
+ UTYPE(BT_GFXSTRING);
+ UTYPE(BT_VISSTRING);
+ UTYPE(BT_GENSTRING);
+ UTYPE(BT_UNIVSTRING);
+ UTYPE(BT_CHARSTRING);
+ UTYPE(BT_BMPSTRING);
+ case BT_SEQUENCE & ~BER_TYPE_CONSTRUCTED_MASK:
+ case BT_SEQUENCE: return "SEQUENCE";
+ case BT_SET & ~BER_TYPE_CONSTRUCTED_MASK:
+ case BT_SET: return "SET";
+ }
+
+fallback:
+ snprintf(namebuf, sizeof(namebuf), "%.02x", libder_type_simple(type));
+ return (&namebuf[0]);
+}
+
+static void
+libder_obj_dump_internal(const struct libder_object *obj, FILE *fp, int lvl)
+{
+ static char spacer[4096];
+ const struct libder_object *child;
+
+ /* Primitive, goofy, but functional. */
+ if (spacer[0] == '\0')
+ memset(spacer, '\t', sizeof(spacer));
+
+ if (lvl >= (int)sizeof(spacer)) {
+ /* Too large, truncate the display. */
+ fprintf(fp, "%.*s...\n", (int)sizeof(spacer), spacer);
+ return;
+ }
+
+ if (obj->children == NULL) {
+ size_t col = lvl * 8;
+
+ col += fprintf(fp, "%.*sOBJECT[type=%s, size=%zx]%s",
+ lvl, spacer, libder_type_name(obj->type),
+ obj->length, obj->length != 0 ? ": " : "");
+
+ if (obj->length != 0) {
+ uint8_t printb;
+
+#define LIBDER_CONTENTS_WRAP 80
+ for (size_t i = 0; i < obj->length; i++) {
+ if (col + 3 >= LIBDER_CONTENTS_WRAP) {
+ fprintf(fp, "\n%.*s ", lvl, spacer);
+ col = (lvl * 8) + 4;
+ }
+
+ if (obj->payload == NULL)
+ printb = 0;
+ else
+ printb = obj->payload[i];
+
+ col += fprintf(fp, "%.02x ", printb);
+ }
+ }
+
+ fprintf(fp, "\n");
+
+ return;
+ }
+
+ fprintf(fp, "%.*sOBJECT[type=%s]\n", lvl, spacer,
+ libder_type_name(obj->type));
+ DER_FOREACH_CHILD(child, obj)
+ libder_obj_dump_internal(child, fp, lvl + 1);
+}
+
+void
+libder_obj_dump(const struct libder_object *root, FILE *fp)
+{
+
+ libder_obj_dump_internal(root, fp, 0);
+}
+
+LIBDER_PRIVATE bool
+libder_is_valid_obj(struct libder_ctx *ctx, const struct libder_tag *type,
+ const uint8_t *payload, size_t payloadsz, bool varlen)
+{
+
+ if (payload != NULL) {
+ assert(payloadsz > 0);
+ assert(!varlen);
+ } else {
+ assert(payloadsz == 0);
+ }
+
+ /* No rules for non-universal types. */
+ if (type->tag_class != BC_UNIVERSAL || type->tag_encoded)
+ return (true);
+
+ if (ctx->strict && type->tag_constructed) {
+ /* Types that don't allow constructed */
+ switch (libder_type_simple(type) & ~BER_TYPE_CONSTRUCTED_MASK) {
+ case BT_BOOLEAN:
+ case BT_INTEGER:
+ case BT_REAL:
+ case BT_NULL:
+ libder_set_error(ctx, LDE_STRICT_PRIMITIVE);
+ return (false);
+ default:
+ break;
+ }
+ } else if (ctx->strict) {
+ /* Types that cannot be primitive */
+ switch (libder_type_simple(type) | BER_TYPE_CONSTRUCTED_MASK) {
+ case BT_SEQUENCE:
+ case BT_SET:
+ libder_set_error(ctx, LDE_STRICT_CONSTRUCTED);
+ return (false);
+ default:
+ break;
+ }
+ }
+
+ /* Further validation */
+ switch (libder_type_simple(type)) {
+ case BT_BOOLEAN:
+ if (ctx->strict && payloadsz != 1) {
+ libder_set_error(ctx, LDE_STRICT_BOOLEAN);
+ return (false);
+ }
+ break;
+ case BT_NULL:
+ if (ctx->strict && (payloadsz != 0 || varlen)) {
+ libder_set_error(ctx, LDE_STRICT_NULL);
+ return (false);
+ }
+ break;
+ case BT_BITSTRING: /* Primitive */
+ /*
+ * Bit strings require more invasive parsing later during child
+ * coalescing or normalization, so we alway strictly enforce
+ * their form.
+ */
+ if (payloadsz == 1 && payload[0] != 0)
+ return (false);
+
+ /* We can't have more than seven unused bits. */
+ return (payloadsz < 2 || payload[0] < 8);
+ case BT_RESERVED:
+ if (payloadsz != 0) {
+ libder_set_error(ctx, LDE_STRICT_EOC);
+ return (false);
+ }
+ break;
+ default:
+ break;
+ }
+
+ return (true);
+}
+
+LIBDER_PRIVATE bool
+libder_obj_may_coalesce_children(const struct libder_object *obj)
+{
+
+ /* No clue about non-universal types. */
+ if (obj->type->tag_class != BC_UNIVERSAL || obj->type->tag_encoded)
+ return (false);
+
+ /* Constructed types don't have children. */
+ if (!obj->type->tag_constructed)
+ return (false);
+
+ /* Strip the constructed bit off. */
+ switch (libder_type_simple(obj->type)) {
+ case BT_OCTETSTRING: /* Raw data types */
+ case BT_BITSTRING:
+ return (true);
+ case BT_UTF8STRING: /* String types */
+ case BT_NUMERICSTRING:
+ case BT_STRING:
+ case BT_TELEXSTRING:
+ case BT_VIDEOTEXSTRING:
+ case BT_IA5STRING:
+ case BT_GFXSTRING:
+ case BT_VISSTRING:
+ case BT_GENSTRING:
+ case BT_UNIVSTRING:
+ case BT_CHARSTRING:
+ case BT_BMPSTRING:
+ return (true);
+ case BT_UTCTIME: /* Time types */
+ case BT_GENTIME:
+ return (true);
+ default:
+ return (false);
+ }
+}
+
+static size_t
+libder_merge_bitstrings(uint8_t *buf, size_t offset, size_t bufsz,
+ const struct libder_object *child)
+{
+ const uint8_t *rhs = child->payload;
+ size_t rsz = child->disk_size, startoff = offset;
+ uint8_t rhsunused, unused;
+
+ rhsunused = (rhs != NULL ? rhs[0] : 0);
+
+ /* We have no unused bits if the buffer's empty as of yet. */
+ if (offset == 0)
+ unused = 0;
+ else
+ unused = buf[0];
+
+ /* Shave the lead byte off if we have one. */
+ if (rsz > 1) {
+ if (rhs != NULL)
+ rhs++;
+ rsz--;
+ }
+
+ if (unused == 0) {
+ size_t extra = 0;
+
+ /*
+ * In all cases we'll just write the unused byte separately,
+ * since we're copying way past it in the common case and can't
+ * just overwrite it as part of the memcpy().
+ */
+ if (offset == 0) {
+ offset = 1;
+ extra++;
+ }
+
+ assert(rhsunused < 8);
+ assert(offset + rsz <= bufsz);
+
+ buf[0] = rhsunused;
+ if (rhs == NULL)
+ memset(&buf[offset], 0, rsz);
+ else
+ memcpy(&buf[offset], rhs, rsz);
+
+ return (rsz + extra);
+ }
+
+ for (size_t i = 0; i < rsz; i++) {
+ uint8_t bits, next;
+
+ if (rhs == NULL)
+ next = 0;
+ else
+ next = rhs[i];
+
+ /* Rotate the leading bits into the byte before it. */
+ assert(unused < 8);
+ bits = next >> (8 - unused);
+ buf[offset - 1] |= bits;
+
+ next <<= unused;
+
+ /*
+ * Copy the new valid bits in; we shift over the old unused
+ * amount up until the very last bit, then we have to recalculate
+ * because we may be dropping it entirely.
+ */
+ if (i == rsz - 1) {
+ assert(rhsunused < 8);
+
+ /*
+ * Figure out how many unused bits we have between the two
+ * buffers, sum % 8 is the new # unused bits. It will be
+ * somewhere in the range of [0, 14], and if it's at or
+ * higher than a single byte then that's a clear indicator
+ * that we shifted some unused bits into the previous byte and
+ * can just halt here.
+ */
+ unused += rhsunused;
+ buf[0] = unused % 8;
+ if (unused >= 8)
+ break;
+ }
+
+ assert(offset < bufsz);
+ buf[offset++] = next;
+ }
+
+ return (offset - startoff);
+}
+
+LIBDER_PRIVATE bool
+libder_obj_coalesce_children(struct libder_object *obj, struct libder_ctx *ctx)
+{
+ struct libder_object *child, *last_child, *tmp;
+ size_t new_size = 0, offset = 0;
+ uint8_t *coalesced_data;
+ uint8_t type;
+ bool need_payload = false, strict_violation = false;
+
+ if (obj->nchildren == 0 || !libder_obj_may_coalesce_children(obj))
+ return (true);
+
+ assert(obj->type->tag_class == BC_UNIVERSAL);
+ assert(obj->type->tag_constructed);
+ assert(!obj->type->tag_encoded);
+ type = obj->type->tag_short;
+
+ last_child = NULL;
+ DER_FOREACH_CHILD(child, obj) {
+ /* Sanity check and coalesce our children. */
+ if (child->type->tag_class != BC_UNIVERSAL ||
+ child->type->tag_short != obj->type->tag_short) {
+ libder_set_error(ctx, LDE_COALESCE_BADCHILD);
+ return (false);
+ }
+
+ /* Recursively coalesce everything. */
+ if (!libder_obj_coalesce_children(child, ctx))
+ return (false);
+
+ /*
+ * The child node will be disappearing anyways, so we stash the
+ * disk size sans header in its disk_size to reuse in the later
+ * loop.
+ */
+ child->disk_size = libder_obj_disk_size(child, false);
+
+ /*
+ * We strip the lead byte off of every element, and add it back
+ * in pre-allocation.
+ */
+ if (type == BT_BITSTRING && child->disk_size > 1)
+ child->disk_size--;
+ if (child->disk_size > 0)
+ last_child = child;
+
+ new_size += child->disk_size;
+
+ if (child->payload != NULL)
+ need_payload = true;
+ }
+
+ if (new_size != 0 && need_payload) {
+ if (type == BT_BITSTRING)
+ new_size++;
+ coalesced_data = malloc(new_size);
+ if (coalesced_data == NULL) {
+ libder_set_error(ctx, LDE_NOMEM);
+ return (false);
+ }
+ } else {
+ /*
+ * This would perhaps be a bit weird, but that's normalization
+ * for you. We shouldn't really have a UTF-8 string that's
+ * composed of a series of zero-length UTF-8 strings, but
+ * weirder things have happened.
+ */
+ coalesced_data = NULL;
+ }
+
+ /* Avoid leaking any children as we coalesce. */
+ DER_FOREACH_CHILD_SAFE(child, obj, tmp) {
+ if (child->disk_size != 0)
+ assert(coalesced_data != NULL || !need_payload);
+
+ /*
+ * Just free everything when we violate strict rules.
+ */
+ if (strict_violation)
+ goto violated;
+
+ if (child->disk_size != 0 && need_payload) {
+ assert(coalesced_data != NULL);
+ assert(offset + child->disk_size <= new_size);
+
+ /*
+ * Bit strings are special, in that the first byte
+ * contains the number of unused bits at the end. We
+ * need to trim that off when concatenating bit strings
+ */
+ if (type == BT_BITSTRING) {
+ if (ctx->strict && child != last_child &&
+ child->disk_size > 1 && child->payload != NULL) {
+ /*
+ * Each child must have a multiple of 8,
+ * up until the final one.
+ */
+ if (child->payload[0] != 0) {
+ libder_set_error(ctx, LDE_STRICT_BITSTRING);
+ strict_violation = true;
+ goto violated;
+ }
+ }
+
+ offset += libder_merge_bitstrings(coalesced_data,
+ offset, new_size, child);
+ } else {
+ /*
+ * Write zeroes out if we don't have a payload.
+ */
+ if (child->payload == NULL) {
+ memset(&coalesced_data[offset], 0, child->disk_size);
+ offset += child->disk_size;
+ } else {
+ memcpy(&coalesced_data[offset], child->payload,
+ child->disk_size);
+ offset += child->disk_size;
+ }
+ }
+ }
+
+violated:
+ libder_obj_free(child);
+ }
+
+ assert(offset <= new_size);
+
+ /* Zap the children, we've absorbed their bodies. */
+ obj->children = NULL;
+
+ if (strict_violation) {
+ if (coalesced_data != NULL) {
+ libder_bzero(coalesced_data, offset);
+ free(coalesced_data);
+ }
+
+ return (false);
+ }
+
+ /* Finally, swap out the payload. */
+ if (obj->payload != NULL) {
+ libder_bzero(obj->payload, obj->length);
+ free(obj->payload);
+ }
+
+ obj->length = offset;
+ obj->payload = coalesced_data;
+ obj->type->tag_constructed = false;
+
+ return (true);
+}
+
+static bool
+libder_obj_normalize_bitstring(struct libder_object *obj)
+{
+ uint8_t *payload = obj->payload;
+ size_t length = obj->length;
+ uint8_t unused;
+
+ if (payload == NULL || length < 2)
+ return (true);
+
+ unused = payload[0];
+ if (unused == 0)
+ return (true);
+
+ /* Clear the unused bits completely. */
+ payload[length - 1] &= ~((1 << unused) - 1);
+ return (true);
+}
+
+static bool
+libder_obj_normalize_boolean(struct libder_object *obj)
+{
+ uint8_t *payload = obj->payload;
+ size_t length = obj->length;
+ int sense = 0;
+
+ assert(length > 0);
+
+ /*
+ * Booleans must be collapsed down to a single byte, 0x00 or 0xff,
+ * indicating false or true respectively.
+ */
+ if (length == 1 && (payload[0] == 0x00 || payload[0] == 0xff))
+ return (true);
+
+ for (size_t bpos = 0; bpos < length; bpos++) {
+ sense |= payload[bpos];
+ if (sense != 0)
+ break;
+ }
+
+ payload[0] = sense != 0 ? 0xff : 0x00;
+ obj->length = 1;
+ return (true);
+}
+
+static bool
+libder_obj_normalize_integer(struct libder_object *obj)
+{
+ uint8_t *payload = obj->payload;
+ size_t length = obj->length;
+ size_t strip = 0;
+
+ /*
+ * Strip any leading sign-extended looking bytes, but note that
+ * we can't strip a leading byte unless it matches the sign bit
+ * on the next byte.
+ */
+ for (size_t bpos = 0; bpos < length - 1; bpos++) {
+ if (payload[bpos] != 0 && payload[bpos] != 0xff)
+ break;
+
+ if (payload[bpos] == 0xff) {
+ /* Only if next byte indicates signed. */
+ if ((payload[bpos + 1] & 0x80) == 0)
+ break;
+ } else {
+ /* Only if next byte indicates unsigned. */
+ if ((payload[bpos + 1] & 0x80) != 0)
+ break;
+ }
+
+ strip++;
+ }
+
+ if (strip != 0) {
+ payload += strip;
+ length -= strip;
+
+ memmove(&obj->payload[0], payload, length);
+ obj->length = length;
+ }
+
+ return (true);
+}
+
+static int
+libder_obj_tag_compare(const struct libder_tag *lhs, const struct libder_tag *rhs)
+{
+ const uint8_t *lbits, *rbits;
+ size_t delta, end, lsz, rsz;
+ uint8_t lbyte, rbyte;
+
+ /* Highest bits: tag class, libder_ber_class has the same bit ordering. */
+ if (lhs->tag_class < rhs->tag_class)
+ return (-1);
+ if (lhs->tag_class > rhs->tag_class)
+ return (1);
+
+ /* Next bit: constructed vs. primitive */
+ if (!lhs->tag_constructed && rhs->tag_constructed)
+ return (-1);
+ if (lhs->tag_constructed && rhs->tag_constructed)
+ return (1);
+
+ /*
+ * Finally: tag data; we can use the size as a first-order heuristic
+ * because we store tags in the shortest possible representation.
+ */
+ if (lhs->tag_size < rhs->tag_size)
+ return (-1);
+ else if (lhs->tag_size > rhs->tag_size)
+ return (1);
+
+ if (!lhs->tag_encoded) {
+ lbits = (const void *)&lhs->tag_short;
+ lsz = sizeof(uint64_t);
+ } else {
+ lbits = lhs->tag_long;
+ lsz = lhs->tag_size;
+ }
+
+ if (!rhs->tag_encoded) {
+ rbits = (const void *)&rhs->tag_short;
+ rsz = sizeof(uint64_t);
+ } else {
+ rbits = rhs->tag_long;
+ rsz = rhs->tag_size;
+ }
+
+ delta = 0;
+ end = MAX(lsz, rsz);
+ if (lsz > rsz)
+ delta = lsz - rsz;
+ else if (lsz < rsz)
+ delta = rsz - lsz;
+ for (size_t i = 0; i < end; i++) {
+ /* Zero-extend the short one the difference. */
+ if (lsz < rsz && i < delta)
+ lbyte = 0;
+ else
+ lbyte = lbits[i - delta];
+
+ if (lsz > rsz && i < delta)
+ rbyte = 0;
+ else
+ rbyte = rbits[i - delta];
+
+ if (lbyte < rbyte)
+ return (-1);
+ else if (lbyte > rbyte)
+ return (-1);
+ }
+
+ return (0);
+}
+
+/*
+ * Similar to strcmp(), returns -1, 0, or 1.
+ */
+static int
+libder_obj_compare(const struct libder_object *lhs, const struct libder_object *rhs)
+{
+ size_t end;
+ int cmp;
+ uint8_t lbyte, rbyte;
+
+ cmp = libder_obj_tag_compare(lhs->type, rhs->type);
+ if (cmp != 0)
+ return (cmp);
+
+ /*
+ * We'll compare up to the longer of the two; the shorter payload is
+ * zero-extended at the end for comparison purposes.
+ */
+ end = MAX(lhs->length, rhs->length);
+ for (size_t pos = 0; pos < end; pos++) {
+ if (lhs->payload != NULL && pos < lhs->length)
+ lbyte = lhs->payload[pos];
+ else
+ lbyte = 0;
+ if (rhs->payload != NULL && pos < rhs->length)
+ rbyte = rhs->payload[pos];
+ else
+ rbyte = 0;
+
+ if (lbyte < rbyte)
+ return (-1);
+ else if (lbyte > rbyte)
+ return (1);
+ }
+
+ return (0);
+}
+
+static int
+libder_obj_normalize_set_cmp(const void *lhs_entry, const void *rhs_entry)
+{
+ const struct libder_object *lhs =
+ *__DECONST(const struct libder_object **, lhs_entry);
+ const struct libder_object *rhs =
+ *__DECONST(const struct libder_object **, rhs_entry);
+
+ return (libder_obj_compare(lhs, rhs));
+}
+
+static bool
+libder_obj_normalize_set(struct libder_object *obj, struct libder_ctx *ctx)
+{
+ struct libder_object **sorting;
+ struct libder_object *child;
+ size_t offset = 0;
+
+ if (obj->nchildren < 2)
+ return (true);
+
+ /*
+ * Kind of goofy, but we'll just take advantage of a standardized
+ * qsort() rather than rolling our own sort -- we have no idea how large
+ * of a dataset we're working with.
+ */
+ sorting = calloc(obj->nchildren, sizeof(*sorting));
+ if (sorting == NULL) {
+ libder_set_error(ctx, LDE_NOMEM);
+ return (false);
+ }
+
+ DER_FOREACH_CHILD(child, obj) {
+ sorting[offset++] = child;
+ }
+
+ assert(offset == obj->nchildren);
+ qsort(sorting, offset, sizeof(*sorting), libder_obj_normalize_set_cmp);
+
+ obj->children = sorting[0];
+ sorting[offset - 1]->next = NULL;
+ for (size_t i = 0; i < offset - 1; i++) {
+ sorting[i]->next = sorting[i + 1];
+ }
+
+ free(sorting);
+
+ return (true);
+}
+
+LIBDER_PRIVATE bool
+libder_obj_normalize(struct libder_object *obj, struct libder_ctx *ctx)
+{
+ uint8_t *payload = obj->payload;
+ size_t length = obj->length;
+
+ if (obj->type->tag_constructed) {
+ /*
+ * For constructed types, we'll see if we can coalesce their
+ * children into them, then we'll proceed with whatever normalization
+ * rules we can apply to the children.
+ */
+ if (DER_NORMALIZING(ctx, CONSTRUCTED) && !libder_obj_coalesce_children(obj, ctx))
+ return (false);
+
+ /*
+ * We may not be a constructed object anymore after the above coalescing
+ * happened, so we check it again here. Constructed objects need not go
+ * any further, but the now-primitive coalesced types still need to be
+ * normalized.
+ */
+ if (obj->type->tag_constructed) {
+ struct libder_object *child;
+
+ DER_FOREACH_CHILD(child, obj) {
+ if (!libder_obj_normalize(child, ctx))
+ return (false);
+ }
+
+ /* Sets must be sorted. */
+ if (obj->type->tag_short != BT_SET)
+ return (true);
+
+ return (libder_obj_normalize_set(obj, ctx));
+ }
+ }
+
+ /* We only have normalization rules for universal types. */
+ if (obj->type->tag_class != BC_UNIVERSAL || obj->type->tag_encoded)
+ return (true);
+
+ if (!libder_normalizing_type(ctx, obj->type))
+ return (true);
+
+ /*
+ * We are clear to normalize this object, check for some easy cases that
+ * don't need normalization.
+ */
+ switch (libder_type_simple(obj->type)) {
+ case BT_BITSTRING:
+ case BT_BOOLEAN:
+ case BT_INTEGER:
+ /*
+ * If we have a zero payload, then we need to encode them as a
+ * single zero byte.
+ */
+ if (payload == NULL) {
+ if (length != 1)
+ obj->length = 1;
+
+ return (true);
+ }
+
+ break;
+ case BT_NULL:
+ if (payload != NULL) {
+ free(payload);
+
+ obj->payload = NULL;
+ obj->length = 0;
+ }
+
+ return (true);
+ default:
+ /*
+ * If we don't have a payload, we'll just leave it alone.
+ */
+ if (payload == NULL)
+ return (true);
+ break;
+ }
+
+ switch (libder_type_simple(obj->type)) {
+ case BT_BITSTRING:
+ return (libder_obj_normalize_bitstring(obj));
+ case BT_BOOLEAN:
+ return (libder_obj_normalize_boolean(obj));
+ case BT_INTEGER:
+ return (libder_obj_normalize_integer(obj));
+ default:
+ break;
+ }
+
+ return (true);
+}
diff --git a/libder/libder_private.h b/libder/libder_private.h
new file mode 100644
index 000000000000..3324420ef6d8
--- /dev/null
+++ b/libder/libder_private.h
@@ -0,0 +1,178 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <sys/param.h>
+
+#include <assert.h>
+#include <signal.h>
+#include <stdbool.h>
+#ifdef __APPLE__
+#define __STDC_WANT_LIB_EXT1__ 1 /* memset_s */
+#endif
+/* explicit_bzero is in one of these... */
+#include <string.h>
+#include <strings.h>
+#include "libder.h"
+
+/* FreeBSD's sys/cdefs.h */
+#ifndef __DECONST
+#define __DECONST(type, var) ((type)(uintptr_t)(const void *)(var))
+#endif
+#ifndef __unused
+#define __unused __attribute__((__unused__))
+#endif
+
+/* FreeBSD's sys/params.h */
+#ifndef nitems
+#define nitems(x) (sizeof((x)) / sizeof((x)[0]))
+#endif
+#ifndef MIN
+#define MIN(a,b) (((a)<(b))?(a):(b))
+#endif
+#ifndef MAX
+#define MAX(a,b) (((a)>(b))?(a):(b))
+#endif
+
+struct libder_ctx;
+struct libder_object;
+
+struct libder_ctx {
+ uint64_t normalize;
+ size_t buffer_size;
+ enum libder_error error;
+ int verbose;
+ bool strict;
+ volatile sig_atomic_t abort;
+};
+
+struct libder_tag {
+ union {
+ uint8_t tag_short;
+ uint8_t *tag_long;
+ };
+ size_t tag_size;
+ enum libder_ber_class tag_class;
+ bool tag_constructed;
+ bool tag_encoded;
+};
+
+struct libder_object {
+ struct libder_tag *type;
+ size_t length;
+ size_t nchildren;
+ size_t disk_size;
+ uint8_t *payload; /* NULL for sequences */
+ struct libder_object *children;
+ struct libder_object *parent;
+ struct libder_object *next;
+};
+
+static inline sig_atomic_t
+libder_check_abort(struct libder_ctx *ctx)
+{
+
+ return (ctx->abort);
+}
+
+static inline void
+libder_clear_abort(struct libder_ctx *ctx)
+{
+
+ ctx->abort = 1;
+}
+
+#define LIBDER_PRIVATE __attribute__((__visibility__("hidden")))
+
+#define DER_NORMALIZING(ctx, bit) \
+ (((ctx)->normalize & (LIBDER_NORMALIZE_ ## bit)) != 0)
+
+static inline bool
+libder_normalizing_type(const struct libder_ctx *ctx, const struct libder_tag *type)
+{
+ uint8_t tagval;
+
+ assert(!type->tag_constructed);
+ assert(!type->tag_encoded);
+ assert(type->tag_class == BC_UNIVERSAL);
+ assert(type->tag_short < 0x1f);
+
+ tagval = type->tag_short;
+ return ((ctx->normalize & LIBDER_NORMALIZE_TYPE_FLAG(tagval)) != 0);
+}
+
+/* All of the lower bits set. */
+#define BER_TYPE_LONG_MASK 0x1f
+
+/*
+ * Check if the type matches one of our universal types.
+ */
+static inline bool
+libder_type_is(const struct libder_tag *type, uint8_t utype)
+{
+
+ if (type->tag_class != BC_UNIVERSAL || type->tag_encoded)
+ return (false);
+ if ((utype & BER_TYPE_CONSTRUCTED_MASK) != type->tag_constructed)
+ return (false);
+
+ utype &= ~BER_TYPE_CONSTRUCTED_MASK;
+ return (utype == type->tag_short);
+}
+
+/*
+ * We'll use this one a decent amount, so we'll keep it inline. There's also
+ * an _abi version that we expose as public interface via a 'libder_type_simple'
+ * macro.
+ */
+#undef libder_type_simple
+
+static inline uint8_t
+libder_type_simple(const struct libder_tag *type)
+{
+ uint8_t encoded = type->tag_class << 6;
+
+ assert(!type->tag_encoded);
+ if (type->tag_constructed)
+ encoded |= BER_TYPE_CONSTRUCTED_MASK;
+
+ encoded |= type->tag_short;
+ return (encoded);
+}
+
+static inline void
+libder_bzero(uint8_t *buf, size_t bufsz)
+{
+
+#ifdef __APPLE__
+ memset_s(buf, bufsz, 0, bufsz);
+#else
+ explicit_bzero(buf, bufsz);
+#endif
+}
+
+size_t libder_get_buffer_size(struct libder_ctx *);
+void libder_set_error(struct libder_ctx *, int, const char *, int);
+
+#define libder_set_error(ctx, error) \
+ libder_set_error((ctx), (error), __FILE__, __LINE__)
+
+struct libder_object *libder_obj_alloc_internal(struct libder_ctx *,
+ struct libder_tag *, uint8_t *, size_t, uint32_t);
+#define LDO_OWNTAG 0x0001 /* Object owns passed in tag */
+
+size_t libder_size_length(size_t);
+bool libder_is_valid_obj(struct libder_ctx *,
+ const struct libder_tag *, const uint8_t *, size_t, bool);
+size_t libder_obj_disk_size(struct libder_object *, bool);
+bool libder_obj_may_coalesce_children(const struct libder_object *);
+bool libder_obj_coalesce_children(struct libder_object *, struct libder_ctx *);
+bool libder_obj_normalize(struct libder_object *, struct libder_ctx *);
+
+struct libder_tag *libder_type_alloc(void);
+void libder_type_release(struct libder_tag *);
+void libder_normalize_type(struct libder_ctx *, struct libder_tag *);
diff --git a/libder/libder_read.3 b/libder/libder_read.3
new file mode 100644
index 000000000000..69c9ba8d0d2c
--- /dev/null
+++ b/libder/libder_read.3
@@ -0,0 +1,101 @@
+.\"
+.\" SPDX-Copyright-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (C) 2024 Kyle Evans <kevans@FreeBSD.org>
+.\"
+.Dd March 2, 2024
+.Dt LIBDER_READ 3
+.Os
+.Sh NAME
+.Nm libder_read ,
+.Nm libder_read_fd ,
+.Nm libder_read_file
+.Nd reading DER encoded streams
+.Sh LIBRARY
+.Lb libder
+.Sh SYNOPSIS
+.In libder.h
+.Ft struct libder_object *
+.Fn libder_read "struct libder_ctx *ctx" "const uint8_t *buf" "size_t *bufsz"
+.Ft struct libder_object *
+.Fn libder_read_fd "struct libder_ctx *ctx" "int fd" "size_t *readsz"
+.Ft struct libder_object *
+.Fn libder_read_file "struct libder_ctx *ctx" "FILE *fp" "size_t *readsz"
+.Sh DESCRIPTION
+The
+.Nm
+family of functions are used to parse BER/DER encoded data into an object tree
+that
+.Xr libder 3
+can work with.
+All of these functions will return an object on success and update
+.Fa *readsz
+with the number of bytes consumed, or
+.Dv NULL
+on failure.
+.Pp
+The
+.Fn libder_read
+function will read from a buffer
+.Fa buf
+of known size
+.Fa bufsz .
+It is not considered an error for
+.Fa buf
+to have contents past the first valid object encountered.
+The application is
+expected to check
+.Fa *bufsz
+upon success and determine if any residual buffer exists, and if that residual
+is OK.
+.Pp
+.Xr libder 3
+can also stream a BER encoded object with either of the
+.Fn libder_read_fd
+or
+.Fn libder_read_file
+functions from a file descriptor or
+.Xr stdio 3
+stream respectively.
+Both functions will try very hard not to over-read from the stream to avoid
+putting it in a precarious state, but bogus looking data may still cause them
+to consume more of the stream than intended.
+.Pp
+Note that
+.Fn libder_read_fd
+will ignore an
+.Ev EINTR
+return value from
+.Xr read 2
+by default and continue reading from the
+.Fa fd .
+If the application is signalled, it can abort the
+.Xr read 2
+operation instead with
+.Xr libder_abort 3 .
+Note that
+.Nm libder
+does not currently have other points that an abort can be signalled from, so if
+.Fn libder_read_fd
+is not specifically waiting for data from the
+.Va fd
+when a signal hits, then the operation will continue until successful with
+one exception.
+If
+.Xr libder_abort 3
+is called at any other point in the middle of
+.Fn libder_read_fd ,
+then the abort flag will not be cleared until it does receive an interrupted
+.Xr read 2
+call, or until the next call to one of the
+.Nm
+family of functions.
+In the future,
+.Nm
+may support resuming an aborted operation and allow cancellation at other
+specific points within the operation.
+.Sh SEE ALSO
+.Xr libder 3 ,
+.Xr libder_obj 3 ,
+.Xr libder_type 3 ,
+.Xr libder_write 3
diff --git a/libder/libder_read.c b/libder/libder_read.c
new file mode 100644
index 000000000000..dba56746be21
--- /dev/null
+++ b/libder/libder_read.c
@@ -0,0 +1,864 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/types.h>
+
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "libder_private.h"
+
+enum libder_stream_type {
+ LDST_NONE,
+ LDST_FD,
+ LDST_FILE,
+};
+
+struct libder_payload {
+ bool payload_heap;
+ uint8_t *payload_data;
+ size_t payload_size;
+};
+
+struct libder_stream {
+ enum libder_stream_type stream_type;
+ struct libder_ctx *stream_ctx;
+ uint8_t *stream_buf;
+ size_t stream_bufsz;
+
+ size_t stream_offset;
+ size_t stream_resid;
+ size_t stream_consumed;
+ size_t stream_last_commit;
+
+ union {
+ const uint8_t *stream_src_buf;
+ FILE *stream_src_file;
+ int stream_src_fd;
+ };
+
+ int stream_error;
+ bool stream_eof;
+};
+
+static uint8_t *
+payload_move(struct libder_payload *payload, size_t *sz)
+{
+ uint8_t *data;
+ size_t datasz;
+
+ data = NULL;
+ datasz = payload->payload_size;
+ if (payload->payload_heap) {
+ data = payload->payload_data;
+ } else if (datasz > 0) {
+ data = malloc(datasz);
+ if (data == NULL)
+ return (NULL);
+
+ memcpy(data, payload->payload_data, datasz);
+ }
+
+ payload->payload_heap = false;
+ payload->payload_data = NULL;
+ payload->payload_size = 0;
+
+ *sz = datasz;
+ return (data);
+}
+
+static void
+payload_free(struct libder_payload *payload)
+{
+
+ if (!payload->payload_heap)
+ return;
+
+ if (payload->payload_data != NULL) {
+ libder_bzero(payload->payload_data, payload->payload_size);
+ free(payload->payload_data);
+ }
+
+ payload->payload_heap = false;
+ payload->payload_data = NULL;
+ payload->payload_size = 0;
+}
+
+static bool
+libder_stream_init(struct libder_ctx *ctx, struct libder_stream *stream)
+{
+ size_t buffer_size;
+
+ stream->stream_ctx = ctx;
+ stream->stream_error = 0;
+ stream->stream_eof = false;
+ stream->stream_offset = 0;
+ stream->stream_consumed = 0;
+ stream->stream_last_commit = 0;
+ if (stream->stream_type == LDST_NONE) {
+ assert(stream->stream_src_buf != NULL);
+ assert(stream->stream_bufsz != 0);
+ assert(stream->stream_resid != 0);
+
+ return (true);
+ }
+
+ buffer_size = libder_get_buffer_size(ctx);
+ assert(buffer_size != 0);
+
+ stream->stream_buf = malloc(buffer_size);
+ if (stream->stream_buf == NULL) {
+ libder_set_error(ctx, LDE_NOMEM);
+ } else {
+ stream->stream_bufsz = buffer_size;
+ stream->stream_resid = 0; /* Nothing read yet */
+ }
+
+ return (stream->stream_buf != NULL);
+}
+
+static void
+libder_stream_free(struct libder_stream *stream)
+{
+ if (stream->stream_buf != NULL) {
+ libder_bzero(stream->stream_buf, stream->stream_bufsz);
+ free(stream->stream_buf);
+ }
+}
+
+static void
+libder_stream_commit(struct libder_stream *stream)
+{
+
+ if (stream->stream_offset <= stream->stream_last_commit)
+ return;
+
+ stream->stream_consumed += stream->stream_offset - stream->stream_last_commit;
+ stream->stream_last_commit = stream->stream_offset;
+}
+
+static bool
+libder_stream_dynamic(const struct libder_stream *stream)
+{
+
+ return (stream->stream_type != LDST_NONE);
+}
+
+static bool
+libder_stream_eof(const struct libder_stream *stream)
+{
+
+ /*
+ * We're not EOF until we're both EOF and have processed all of the data
+ * remaining in the buffer.
+ */
+ return (stream->stream_eof && stream->stream_resid == 0);
+}
+
+static void
+libder_stream_repack(struct libder_stream *stream)
+{
+
+ /*
+ * Nothing to do, data's already at the beginning.
+ */
+ if (stream->stream_offset == 0)
+ return;
+
+ /*
+ * If there's data in-flight, we'll repack it back to the beginning so
+ * that we can store more with fewer calls to refill. If there's no
+ * data in-flight, we naturally just reset the offset.
+ */
+ if (stream->stream_resid != 0) {
+ uint8_t *dst = &stream->stream_buf[0];
+ uint8_t *src = &stream->stream_buf[stream->stream_offset];
+
+ memmove(dst, src, stream->stream_resid);
+ }
+
+ stream->stream_last_commit -= stream->stream_offset;
+ stream->stream_offset = 0;
+}
+
+static const uint8_t *
+libder_stream_refill(struct libder_stream *stream, size_t req)
+{
+ size_t offset = stream->stream_offset;
+ const uint8_t *src;
+#ifndef NDEBUG
+ const uint8_t *bufend;
+#endif
+ uint8_t *refill_buf;
+ size_t bufleft, freadsz, needed, totalsz;
+ ssize_t readsz;
+
+ /*
+ * For non-streaming, we just fulfill requests straight out of
+ * the source buffer.
+ */
+ if (stream->stream_type == LDST_NONE)
+ src = stream->stream_src_buf;
+ else
+ src = stream->stream_buf;
+
+ if (stream->stream_resid >= req) {
+ stream->stream_offset += req;
+ stream->stream_resid -= req;
+ return (&src[offset]);
+ }
+
+ /* Cannot refill the non-streaming type. */
+ if (stream->stream_type == LDST_NONE) {
+ stream->stream_eof = true;
+ return (NULL);
+ }
+
+ bufleft = stream->stream_bufsz - (stream->stream_offset + stream->stream_resid);
+
+ /*
+ * If we can't fit all of our data in the remainder of the buffer, we'll
+ * try to repack it to just fit as much as we can in.
+ */
+ if (req > bufleft && stream->stream_offset != 0) {
+ libder_stream_repack(stream);
+
+ bufleft = stream->stream_bufsz - stream->stream_resid;
+ offset = stream->stream_offset;
+ }
+
+ refill_buf = &stream->stream_buf[offset + stream->stream_resid];
+ needed = req - stream->stream_resid;
+
+ assert(needed <= bufleft);
+
+#ifndef NDEBUG
+ bufend = &stream->stream_buf[stream->stream_bufsz];
+#endif
+ totalsz = 0;
+
+ switch (stream->stream_type) {
+ case LDST_FILE:
+ assert(stream->stream_src_file != NULL);
+
+ while (needed != 0) {
+ assert(refill_buf + needed <= bufend);
+
+ freadsz = fread(refill_buf, 1, needed, stream->stream_src_file);
+ if (freadsz == 0) {
+ /*
+ * Error always put us into EOF state.
+ */
+ stream->stream_eof = true;
+ if (ferror(stream->stream_src_file))
+ stream->stream_error = 1;
+ break;
+ }
+
+ stream->stream_resid += freadsz;
+ refill_buf += freadsz;
+ needed -= freadsz;
+ totalsz += freadsz;
+ }
+ break;
+ case LDST_FD:
+ assert(stream->stream_src_fd >= 0);
+
+ while (needed != 0) {
+ assert(refill_buf + needed <= bufend);
+
+ readsz = read(stream->stream_src_fd, refill_buf, needed);
+ if (readsz <= 0) {
+ /*
+ * In the future, we should likely make this
+ * configurable in some sense, but for now this
+ * seems fine. If, e.g., we caught a SIGINT,
+ * the application could always just close the
+ * fd on us if we should bail out. The problem
+ * right now is that we have no way to resume a
+ * partial transfer.
+ */
+ if (readsz < 0 && errno == EINTR &&
+ !libder_check_abort(stream->stream_ctx))
+ continue;
+ stream->stream_eof = true;
+ if (readsz < 0) {
+ stream->stream_ctx->abort = false;
+ stream->stream_error = errno;
+ if (stream->stream_ctx->verbose > 0)
+ warn("libder_read");
+ }
+ break;
+ }
+
+ stream->stream_resid += readsz;
+ refill_buf += readsz;
+ needed -= readsz;
+ totalsz += readsz;
+ }
+
+ break;
+ case LDST_NONE:
+ assert(0 && "Unrecognized stream type");
+ break;
+ }
+
+ /*
+ * For streaming types, we commit as soon as we refill the buffer because
+ * we can't just rewind.
+ */
+ stream->stream_consumed += totalsz;
+ stream->stream_last_commit += totalsz;
+
+ if (needed != 0) {
+ if (stream->stream_error != 0)
+ libder_set_error(stream->stream_ctx, LDE_STREAMERR);
+ return (NULL);
+ } else {
+ stream->stream_offset += req;
+ stream->stream_resid -= req;
+ }
+
+ return (&stream->stream_buf[offset]);
+}
+
+/*
+ * We can't just use realloc() because it won't provide any guarantees about
+ * the previous region if it can't just resize in-place, so we'll always just
+ * allocate a new one and copy ourselves.
+ */
+static uint8_t *
+libder_read_realloc(uint8_t *ptr, size_t oldsz, size_t newsz)
+{
+ uint8_t *newbuf;
+
+ if (oldsz == 0)
+ assert(ptr == NULL);
+ else
+ assert(ptr != NULL);
+ assert(newsz > oldsz);
+
+ newbuf = malloc(newsz);
+ if (newbuf == NULL)
+ return (NULL);
+
+ if (oldsz != 0) {
+ memcpy(newbuf, ptr, oldsz);
+
+ libder_bzero(ptr, oldsz);
+ free(ptr);
+ }
+
+ return (newbuf);
+}
+
+#define BER_TYPE_LONG_BATCH 0x04
+
+static bool
+der_read_structure_tag(struct libder_ctx *ctx, struct libder_stream *stream,
+ struct libder_tag *type)
+{
+ const uint8_t *buf;
+ uint8_t *longbuf = NULL, val;
+ size_t longbufsz = 0, offset = 0, received = 0;
+
+ for (;;) {
+ /*
+ * We have to refill one byte at a time to avoid overreading
+ * into the structure size.
+ */
+ if ((buf = libder_stream_refill(stream, 1)) == NULL) {
+ free(longbuf);
+ if (!libder_stream_eof(stream))
+ libder_set_error(ctx, LDE_SHORTHDR);
+ return (false);
+ }
+
+ received++;
+ val = buf[0];
+ if (received == 1) {
+ /* Deconstruct the class and p/c */
+ type->tag_class = BER_TYPE_CLASS(val);
+ type->tag_constructed = BER_TYPE_CONSTRUCTED(val);
+
+ /* Long form, or short form? */
+ if (BER_TYPE(val) != BER_TYPE_LONG_MASK) {
+ type->tag_short = BER_TYPE(val);
+ type->tag_size = sizeof(uint8_t);
+ type->tag_encoded = false;
+
+ return (true);
+ }
+
+ /*
+ * No content from this one, grab another byte.
+ */
+ type->tag_encoded = true;
+ continue;
+ }
+
+ /* We might normalize it later, depending on flags. */
+ if (offset == 0 && (val & 0x7f) == 0 && ctx->strict) {
+ libder_set_error(ctx, LDE_STRICT_TAG);
+ return (false);
+ }
+
+ /* XXX Impose a max size? Perhaps configurable. */
+ if (offset == longbufsz) {
+ uint8_t *next;
+ size_t nextsz;
+
+ nextsz = longbufsz + BER_TYPE_LONG_BATCH;
+ next = realloc(longbuf, nextsz * sizeof(*longbuf));
+ if (next == NULL) {
+ free(longbuf);
+ libder_set_error(ctx, LDE_NOMEM);
+ return (false);
+ }
+
+ longbuf = next;
+ longbufsz = nextsz;
+ }
+
+ longbuf[offset++] = val;
+
+ if ((val & 0x80) == 0)
+ break;
+ }
+
+ type->tag_long = longbuf;
+ type->tag_size = offset;
+
+ libder_normalize_type(ctx, type);
+
+ return (true);
+}
+
+static int
+der_read_structure(struct libder_ctx *ctx, struct libder_stream *stream,
+ struct libder_tag *type, struct libder_payload *payload, bool *varlen)
+{
+ const uint8_t *buf;
+ size_t rsz, offset, resid;
+ uint8_t bsz;
+
+ rsz = 0;
+ if (!der_read_structure_tag(ctx, stream, type)) {
+ return (-1);
+ }
+
+ if ((buf = libder_stream_refill(stream, 1)) == NULL) {
+ if (!libder_stream_eof(stream))
+ libder_set_error(ctx, LDE_SHORTHDR);
+ goto failed;
+ }
+
+ bsz = *buf++;
+
+#define LENBIT_LONG 0x80
+ *varlen = false;
+ if ((bsz & LENBIT_LONG) != 0) {
+ /* Long or long form, bsz describes how many bytes we have. */
+ bsz &= ~LENBIT_LONG;
+ if (bsz != 0) {
+ /* Long */
+ if (bsz > sizeof(rsz)) {
+ libder_set_error(ctx, LDE_LONGLEN);
+ goto failed; /* Only support up to long bytes. */
+ } else if ((buf = libder_stream_refill(stream, bsz)) == NULL) {
+ libder_set_error(ctx, LDE_SHORTHDR);
+ goto failed;
+ }
+
+ rsz = 0;
+ for (int i = 0; i < bsz; i++) {
+ if (i != 0)
+ rsz <<= 8;
+ rsz |= *buf++;
+ }
+ } else {
+ if (ctx->strict && !type->tag_constructed) {
+ libder_set_error(ctx, LDE_STRICT_PVARLEN);
+ goto failed;
+ }
+
+ *varlen = true;
+ }
+ } else {
+ /* Short form */
+ rsz = bsz;
+ }
+
+ if (rsz != 0) {
+ assert(!*varlen);
+
+ /*
+ * If we're not running a dynamic stream, we can just use a
+ * pointer into the buffer. The caller may copy the payload out
+ * anyways, but there's no sense in doing it up-front in case we
+ * hit an error in between then and now.
+ */
+ if (!libder_stream_dynamic(stream)) {
+ /*
+ * This is a little dirty, but the caller won't mutate
+ * the data -- it'll either strictly read it, or it will
+ * copy it out to a known-mutable region.
+ */
+ payload->payload_data =
+ __DECONST(void *, libder_stream_refill(stream, rsz));
+ payload->payload_heap = false;
+ if (payload->payload_data == NULL) {
+ libder_set_error(ctx, LDE_SHORTDATA);
+ goto failed;
+ }
+ } else {
+ uint8_t *payload_data;
+
+ /*
+ * We play it conservative here: we could allocate the
+ * buffer up-front, but we have no idea how much data we
+ * actually have to receive! The length is a potentially
+ * attacker-controlled aspect, so we're cautiously optimistic
+ * that it's accurate.
+ */
+ payload_data = NULL;
+
+ offset = 0;
+ resid = rsz;
+ while (resid != 0) {
+ uint8_t *next_data;
+ size_t req;
+
+ req = MIN(stream->stream_bufsz, resid);
+ if ((buf = libder_stream_refill(stream, req)) == NULL) {
+ libder_bzero(payload_data, offset);
+ free(payload_data);
+
+ libder_set_error(ctx, LDE_SHORTDATA);
+ goto failed;
+ }
+
+ next_data = libder_read_realloc(payload_data,
+ offset, offset + req);
+ if (next_data == NULL) {
+ libder_bzero(payload_data, offset);
+ free(payload_data);
+
+ libder_set_error(ctx, LDE_NOMEM);
+ goto failed;
+ }
+
+ payload_data = next_data;
+ next_data = NULL;
+
+ memcpy(&payload_data[offset], buf, req);
+ offset += req;
+ resid -= req;
+ }
+
+ payload->payload_heap = true;
+ payload->payload_data = payload_data;
+ }
+
+ payload->payload_size = rsz;
+ }
+
+ libder_stream_commit(stream);
+ return (0);
+
+failed:
+ libder_type_release(type);
+ return (-1);
+}
+
+static struct libder_object *
+libder_read_object(struct libder_ctx *ctx, struct libder_stream *stream)
+{
+ struct libder_payload payload = { 0 };
+ struct libder_object *child, **next, *obj;
+ struct libder_stream memstream, *childstream;
+ struct libder_tag type;
+ int error;
+ bool varlen;
+
+ /* Peel off one structure. */
+ obj = NULL;
+ error = der_read_structure(ctx, stream, &type, &payload, &varlen);
+ if (error != 0) {
+ assert(payload.payload_data == NULL);
+ return (NULL); /* Error already set, if needed. */
+ }
+
+ if (!libder_is_valid_obj(ctx, &type, payload.payload_data,
+ payload.payload_size, varlen)) {
+ /*
+ * libder_is_valid_obj may set a more specific error, e.g., a
+ * strict mode violation.
+ */
+ if (ctx->error == LDE_NONE)
+ libder_set_error(ctx, LDE_BADOBJECT);
+ goto out;
+ }
+
+ if (!type.tag_constructed) {
+ uint8_t *payload_data;
+ size_t payloadsz;
+
+ /*
+ * Primitive types cannot use the indefinite form, they must
+ * have an encoded size.
+ */
+ if (varlen) {
+ libder_set_error(ctx, LDE_BADVARLEN);
+ goto out;
+ }
+
+ /*
+ * Copy the payload out now if it's not heap-allocated.
+ */
+ payload_data = payload_move(&payload, &payloadsz);
+ if (payload_data == NULL) {
+ libder_set_error(ctx, LDE_NOMEM);
+ goto out;
+ }
+
+ obj = libder_obj_alloc_internal(ctx, &type, payload_data,
+ payloadsz, 0);
+ if (obj == NULL) {
+ free(payload_data);
+ libder_set_error(ctx, LDE_NOMEM);
+ goto out;
+ }
+
+ libder_type_release(&type);
+ return (obj);
+ }
+
+ obj = libder_obj_alloc_internal(ctx, &type, NULL, 0, 0);
+ if (obj == NULL) {
+ libder_set_error(ctx, LDE_NOMEM);
+ goto out;
+ }
+
+ if (varlen) {
+ childstream = stream;
+ } else {
+ memstream = (struct libder_stream){
+ .stream_type = LDST_NONE,
+ .stream_bufsz = payload.payload_size,
+ .stream_resid = payload.payload_size,
+ .stream_src_buf = payload.payload_data,
+ };
+
+ childstream = &memstream;
+ }
+
+ /* Enumerate children */
+ next = &obj->children;
+ for (;;) {
+ child = libder_read_object(ctx, childstream);
+ if (child == NULL) {
+ /*
+ * We may not know how much data we have, so this is our
+ * normal terminal condition.
+ */
+ if (ctx->error != LDE_NONE) {
+ /* Free everything and bubble the error up. */
+ libder_obj_free(obj);
+ obj = NULL;
+ }
+ break;
+ }
+
+ if (libder_type_is(child->type, BT_RESERVED) &&
+ child->length == 0) {
+ /*
+ * This child is just a marker; free it, don't leak it,
+ * and stop here.
+ */
+ libder_obj_free(child);
+
+ /* Malformed: shall not be present */
+ if (!varlen) {
+ if (ctx->strict) {
+ libder_set_error(ctx, LDE_STRICT_EOC);
+ libder_obj_free(obj);
+ obj = NULL;
+ break;
+ }
+
+ continue;
+ }
+
+ /* Error detection */
+ varlen = false;
+ break;
+ }
+
+ obj->nchildren++;
+ child->parent = obj;
+ *next = child;
+ next = &child->next;
+ }
+
+ if (varlen) {
+ libder_set_error(ctx, LDE_TRUNCVARLEN);
+ libder_obj_free(obj);
+ obj = NULL;
+ }
+
+out:
+ libder_type_release(&type);
+ payload_free(&payload);
+ return (obj);
+}
+
+static struct libder_object *
+libder_read_stream(struct libder_ctx *ctx, struct libder_stream *stream)
+{
+ struct libder_object *root;
+
+ ctx->error = LDE_NONE;
+ root = libder_read_object(ctx, stream);
+
+ if (root != NULL && libder_type_is(root->type, BT_RESERVED) &&
+ root->length == 0) {
+ /* Strict violation: must not appear. */
+ if (ctx->strict)
+ libder_set_error(ctx, LDE_STRICT_EOC);
+ libder_obj_free(root);
+ root = NULL;
+ }
+ if (root != NULL)
+ assert(stream->stream_consumed != 0);
+ return (root);
+}
+
+/*
+ * Read the DER-encoded `data` into `ctx`.
+ *
+ * Returns an object on success, or NULL on failure. *datasz is updated to
+ * indicate the number of bytes consumed either way -- it will only be updated
+ * in the failure case if at least one object was valid.
+ */
+struct libder_object *
+libder_read(struct libder_ctx *ctx, const uint8_t *data, size_t *datasz)
+{
+ struct libder_stream *stream;
+ struct libder_object *root;
+
+ stream = malloc(sizeof(*stream));
+ if (stream == NULL) {
+ libder_set_error(ctx, LDE_NOMEM);
+ return (NULL);
+ }
+
+ *stream = (struct libder_stream){
+ .stream_type = LDST_NONE,
+ .stream_bufsz = *datasz,
+ .stream_resid = *datasz,
+ .stream_src_buf = data,
+ };
+
+ libder_clear_abort(ctx);
+ ctx->error = LDE_NONE;
+ if (!libder_stream_init(ctx, stream)) {
+ free(stream);
+ return (NULL);
+ }
+
+ root = libder_read_stream(ctx, stream);
+ if (stream->stream_consumed != 0)
+ *datasz = stream->stream_consumed;
+
+ libder_stream_free(stream);
+ free(stream);
+
+ return (root);
+}
+
+/*
+ * Ditto above, but with an fd. *consumed is not ignored on entry, and returned
+ * with the number of bytes read from fd if consumed is not NULL. libder(3)
+ * tries to not over-read if an invalid structure is detected.
+ */
+struct libder_object *
+libder_read_fd(struct libder_ctx *ctx, int fd, size_t *consumed)
+{
+ struct libder_stream *stream;
+ struct libder_object *root;
+
+ stream = malloc(sizeof(*stream));
+ if (stream == NULL) {
+ libder_set_error(ctx, LDE_NOMEM);
+ return (NULL);
+ }
+
+ *stream = (struct libder_stream){
+ .stream_type = LDST_FD,
+ .stream_src_fd = fd,
+ };
+
+ root = NULL;
+ libder_clear_abort(ctx);
+ ctx->error = LDE_NONE;
+ if (!libder_stream_init(ctx, stream)) {
+ free(stream);
+ return (NULL);
+ }
+
+ root = libder_read_stream(ctx, stream);
+ if (consumed != NULL && stream->stream_consumed != 0)
+ *consumed = stream->stream_consumed;
+
+ libder_stream_free(stream);
+ free(stream);
+ return (root);
+}
+
+/*
+ * Ditto above, but with a FILE instead of an fd.
+ */
+struct libder_object *
+libder_read_file(struct libder_ctx *ctx, FILE *fp, size_t *consumed)
+{
+ struct libder_stream *stream;
+ struct libder_object *root;
+
+ stream = malloc(sizeof(*stream));
+ if (stream == NULL) {
+ libder_set_error(ctx, LDE_NOMEM);
+ return (NULL);
+ }
+
+ *stream = (struct libder_stream){
+ .stream_type = LDST_FILE,
+ .stream_src_file = fp,
+ };
+
+ root = NULL;
+ libder_clear_abort(ctx);
+ ctx->error = LDE_NONE;
+ if (!libder_stream_init(ctx, stream)) {
+ free(stream);
+ return (NULL);
+ }
+
+ root = libder_read_stream(ctx, stream);
+ if (consumed != NULL && stream->stream_consumed != 0)
+ *consumed = stream->stream_consumed;
+
+ libder_stream_free(stream);
+ free(stream);
+
+ return (root);
+}
diff --git a/libder/libder_type.3 b/libder/libder_type.3
new file mode 100644
index 000000000000..df577a70f406
--- /dev/null
+++ b/libder/libder_type.3
@@ -0,0 +1,71 @@
+.\"
+.\" SPDX-Copyright-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (C) 2024 Kyle Evans <kevans@FreeBSD.org>
+.\"
+.Dd March 2, 2024
+.Dt LIBDER_TYPE 3
+.Os
+.Sh NAME
+.Nm libder_type ,
+.Nm libder_type_alloc_simple ,
+.Nm libder_type_dup ,
+.Nm libder_type_free ,
+.Nm libder_type_simple
+.Nd creating DER types
+.Sh LIBRARY
+.Lb libder
+.Sh SYNOPSIS
+.In libder.h
+.Ft struct libder_tag *
+.Fn libder_type_alloc_simple "struct libder_ctx *ctx" "uint8_t type"
+.Ft struct libder_tag *
+.Fn libder_type_dup "struct libder_ctx *ctx" "const struct libder_tag *type"
+.Ft void
+.Fn libder_type_free "struct libder_tag *type"
+.Ft uint8_t
+.Fn libder_type_simple "const struct libder_tag *type"
+.Sh DESCRIPTION
+The
+.Nm
+family of functions operate on the
+.Xr libder 3
+type primitive.
+These functions are largely useless as currently implemented, as
+.Xr libder_obj 3
+has a method for allocating an object using a simple tag directly.
+In the future,
+.Nm
+will have an API for importing encoded tags that need more than the
+.Dq simple
+one byte form (tags 0-30).
+.Pp
+The
+.Fn libder_type_alloc_simple
+function allocates a new type from the
+.Dq simple
+one byte form.
+This type may be subsequently passed to
+.Xr libder_obj_alloc 3 .
+.Pp
+The
+.Fn libder_type_dup
+function duplicates an existing type, and the
+.Fn libder_type_free
+function frees the type.
+.Pp
+The
+.Ft libder_type_simple
+function encodes the given
+.Fa type
+in the
+.Dq simple
+one byte buffer form.
+In this form, the class bits and the primitive and constructed bits are encoded
+in the three most significant bits, and the lower five bits are used to encode
+a tag number between 0 and 30.
+.Sh SEE ALSO
+.Xr libder 3 ,
+.Xr libder_obj 3 ,
+.Xr libder_read 3 ,
+.Xr libder_write 3
diff --git a/libder/libder_type.c b/libder/libder_type.c
new file mode 100644
index 000000000000..dec942ce68f9
--- /dev/null
+++ b/libder/libder_type.c
@@ -0,0 +1,150 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "libder_private.h"
+
+uint8_t
+libder_type_simple_abi(const struct libder_tag *type)
+{
+
+ return (libder_type_simple(type));
+}
+
+/*
+ * We'll likely expose this in the form of libder_type_import(), which validates
+ * and allocates a tag.
+ */
+LIBDER_PRIVATE struct libder_tag *
+libder_type_alloc(void)
+{
+
+ return (calloc(1, sizeof(struct libder_tag)));
+}
+
+struct libder_tag *
+libder_type_dup(struct libder_ctx *ctx, const struct libder_tag *dtype)
+{
+ struct libder_tag *type;
+
+ type = libder_type_alloc();
+ if (type == NULL) {
+ libder_set_error(ctx, LDE_NOMEM);
+ return (NULL);
+ }
+
+ memcpy(type, dtype, sizeof(*dtype));
+
+ if (type->tag_encoded) {
+ uint8_t *tdata;
+
+ /* Deep copy the tag data. */
+ tdata = malloc(type->tag_size);
+ if (tdata == NULL) {
+ libder_set_error(ctx, LDE_NOMEM);
+
+ /*
+ * Don't accidentally free the caller's buffer; it may
+ * be an external user of the API.
+ */
+ type->tag_long = NULL;
+ type->tag_size = 0;
+ libder_type_free(type);
+ return (NULL);
+ }
+
+ memcpy(tdata, dtype->tag_long, dtype->tag_size);
+ type->tag_long = tdata;
+ }
+
+ return (type);
+}
+
+struct libder_tag *
+libder_type_alloc_simple(struct libder_ctx *ctx, uint8_t typeval)
+{
+ struct libder_tag *type;
+
+ type = libder_type_alloc();
+ if (type == NULL) {
+ libder_set_error(ctx, LDE_NOMEM);
+ return (NULL);
+ }
+
+ type->tag_size = sizeof(typeval);
+ type->tag_class = BER_TYPE_CLASS(typeval);
+ type->tag_constructed = BER_TYPE_CONSTRUCTED(typeval);
+ type->tag_short = BER_TYPE(typeval);
+ return (type);
+}
+
+LIBDER_PRIVATE void
+libder_type_release(struct libder_tag *type)
+{
+
+ if (type->tag_encoded) {
+ free(type->tag_long);
+ type->tag_long = NULL;
+
+ /*
+ * Leaving type->tag_encoded set in case it helps us catch some
+ * bogus re-use of the type; we'd surface that as a null ptr
+ * deref as they think they should be using tag_long.
+ */
+ }
+}
+
+void
+libder_type_free(struct libder_tag *type)
+{
+
+ if (type == NULL)
+ return;
+
+ libder_type_release(type);
+ free(type);
+}
+
+LIBDER_PRIVATE void
+libder_normalize_type(struct libder_ctx *ctx, struct libder_tag *type)
+{
+ uint8_t tagval;
+ size_t offset;
+
+ if (!type->tag_encoded || !DER_NORMALIZING(ctx, TAGS))
+ return;
+
+ /*
+ * Strip any leading 0's off -- not possible in strict mode.
+ */
+ for (offset = 0; offset < type->tag_size - 1; offset++) {
+ if ((type->tag_long[offset] & 0x7f) != 0)
+ break;
+ }
+
+ assert(offset == 0 || !ctx->strict);
+ if (offset != 0) {
+ type->tag_size -= offset;
+ memmove(&type->tag_long[0], &type->tag_long[offset],
+ type->tag_size);
+ }
+
+ /*
+ * We might be able to strip it down to a unencoded tag_short, if only
+ * the lower 5 bits are in use.
+ */
+ if (type->tag_size != 1 || (type->tag_long[0] & ~0x1e) != 0)
+ return;
+
+ tagval = type->tag_long[0];
+
+ free(type->tag_long);
+ type->tag_short = tagval;
+ type->tag_encoded = false;
+}
diff --git a/libder/libder_write.3 b/libder/libder_write.3
new file mode 100644
index 000000000000..8b1a5aa2bbff
--- /dev/null
+++ b/libder/libder_write.3
@@ -0,0 +1,54 @@
+.\"
+.\" SPDX-Copyright-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (C) 2024 Kyle Evans <kevans@FreeBSD.org>
+.\"
+.Dd March 2, 2024
+.Dt LIBDER_WRITE 3
+.Os
+.Sh NAME
+.Nm libder_write
+.Nd writing DER encoded buffers
+.Sh LIBRARY
+.Lb libder
+.Sh SYNOPSIS
+.In libder.h
+.Ft uint8_t *
+.Fn libder_write "struct libder_ctx *ctx" "struct libder_object *root" "uint8_t *buf" "size_t *bufsize"
+.Sh DESCRIPTION
+The
+.Fn libder_write
+writes the specified
+.Fa root
+into the given
+.Fa buf
+of size
+.Fa bufsize .
+If a
+.Dv NULL
+and
+.Dv 0
+are passed in, then
+.Fn libder_write
+will alllocate a buffer just large enough to fit the encoded
+.Fa root .
+Upon successful write,
+.Fn libder_write
+will return a pointer to the buffer used, and
+.Fa *bufsize
+is updated to indicate how many bytes were written.
+On failure,
+.Dv NULL
+is returned and
+.Fa *bufsize
+will remain unmodified.
+.Pp
+Normalization rules are applied at write time, if specified via
+.Xr libder_set_normalize 3 .
+Note that applications do not typically need to enable normalization, as they
+are all enabled by default.
+.Sh SEE ALSO
+.Xr libder 3 ,
+.Xr libder_obj 3 ,
+.Xr libder_read 3 ,
+.Xr libder_type 3
diff --git a/libder/libder_write.c b/libder/libder_write.c
new file mode 100644
index 000000000000..66ccbcfbf21d
--- /dev/null
+++ b/libder/libder_write.c
@@ -0,0 +1,229 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "libder.h"
+#include "libder_private.h"
+
+struct memory_write_data {
+ uint8_t *buf;
+ size_t bufsz;
+ size_t offset;
+};
+
+typedef bool (write_buffer_t)(void *, const uint8_t *, size_t);
+
+static bool
+libder_write_object_tag(struct libder_ctx *ctx __unused,
+ const struct libder_object *obj, write_buffer_t *write_buffer, void *cookie)
+{
+ const struct libder_tag *type = obj->type;
+ uint8_t value;
+
+ if (!type->tag_encoded) {
+ value = libder_type_simple(type);
+ return (write_buffer(cookie, &value, sizeof(value)));
+ }
+
+ /* Write out the tag info first. */
+ value = BER_TYPE_LONG_MASK;
+ value |= type->tag_class << 6;
+ if (type->tag_constructed)
+ value |= BER_TYPE_CONSTRUCTED_MASK;
+
+ if (!write_buffer(cookie, &value, sizeof(value)))
+ return (false);
+
+ /* Write out the encoded tag next. */
+ return (write_buffer(cookie, type->tag_long, type->tag_size));
+}
+
+static bool
+libder_write_object_header(struct libder_ctx *ctx, struct libder_object *obj,
+ write_buffer_t *write_buffer, void *cookie)
+{
+ size_t size;
+ uint8_t sizelen, value;
+
+ if (!libder_write_object_tag(ctx, obj, write_buffer, cookie))
+ return (false);
+
+ size = obj->disk_size;
+ sizelen = libder_size_length(size);
+
+ if (sizelen == 1) {
+ assert((size & ~0x7f) == 0);
+
+ value = size;
+ if (!write_buffer(cookie, &value, sizeof(value)))
+ return (false);
+ } else {
+ /*
+ * Protocol supports at most 0x7f size bytes, but we can only
+ * do up to a size_t.
+ */
+ uint8_t sizebuf[sizeof(size_t)], *sizep;
+
+ sizelen--; /* Remove the lead byte. */
+
+ value = 0x80 | sizelen;
+ if (!write_buffer(cookie, &value, sizeof(value)))
+ return (false);
+
+ sizep = &sizebuf[0];
+ for (uint8_t i = 0; i < sizelen; i++)
+ *sizep++ = (size >> ((sizelen - i - 1) * 8)) & 0xff;
+
+ if (!write_buffer(cookie, &sizebuf[0], sizelen))
+ return (false);
+ }
+
+ return (true);
+}
+
+static bool
+libder_write_object_payload(struct libder_ctx *ctx __unused,
+ struct libder_object *obj, write_buffer_t *write_buffer, void *cookie)
+{
+ uint8_t *payload = obj->payload;
+ size_t length = obj->length;
+
+ /* We don't expect `obj->payload` to be valid for a zero-size value. */
+ if (length == 0)
+ return (true);
+
+ /*
+ * We allow a NULL payload with a non-zero length to indicate that an
+ * object should write zeroes out, we just didn't waste the memory on
+ * these small allocations. Ideally if it's more than just one or two
+ * zeroes we're instead allocating a buffer for it and doing some more
+ * efficient copying from there.
+ */
+ if (payload == NULL) {
+ uint8_t zero = 0;
+
+ for (size_t i = 0; i < length; i++) {
+ if (!write_buffer(cookie, &zero, 1))
+ return (false);
+ }
+
+ return (true);
+ }
+
+ return (write_buffer(cookie, payload, length));
+}
+
+static bool
+libder_write_object(struct libder_ctx *ctx, struct libder_object *obj,
+ write_buffer_t *write_buffer, void *cookie)
+{
+ struct libder_object *child;
+
+ if (DER_NORMALIZING(ctx, CONSTRUCTED) && !libder_obj_coalesce_children(obj, ctx))
+ return (false);
+
+ /* Write out this object's header first */
+ if (!libder_write_object_header(ctx, obj, write_buffer, cookie))
+ return (false);
+
+ /* Write out the payload. */
+ if (obj->children == NULL)
+ return (libder_write_object_payload(ctx, obj, write_buffer, cookie));
+
+ assert(obj->type->tag_constructed);
+
+ /* Recurse on each child. */
+ DER_FOREACH_CHILD(child, obj) {
+ if (!libder_write_object(ctx, child, write_buffer, cookie))
+ return (false);
+ }
+
+ return (true);
+}
+
+static bool
+memory_write(void *cookie, const uint8_t *data, size_t datasz)
+{
+ struct memory_write_data *mwrite = cookie;
+ uint8_t *dst = &mwrite->buf[mwrite->offset];
+ size_t left;
+
+ /* Small buffers should have been rejected long before now. */
+ left = mwrite->bufsz - mwrite->offset;
+ assert(datasz <= left);
+
+ memcpy(dst, data, datasz);
+ mwrite->offset += datasz;
+ return (true);
+}
+
+/*
+ * Writes the object rooted at `root` to the buffer. If `buf` == NULL and
+ * `*bufsz` == 0, we'll allocate a buffer just large enough to hold the result
+ * and pass the size back via `*bufsz`. If a pre-allocated buffer is passed,
+ * we may still update `*bufsz` if normalization made the buffer smaller.
+ *
+ * If the buffer is too small, *bufsz will be set to the size of buffer needed.
+ */
+uint8_t *
+libder_write(struct libder_ctx *ctx, struct libder_object *root, uint8_t *buf,
+ size_t *bufsz)
+{
+ struct memory_write_data mwrite = { 0 };
+ size_t needed;
+
+ /*
+ * We shouldn't really see buf == NULL with *bufsz != 0 or vice-versa.
+ * Combined, they mean that we should allocate whatever buffer size we
+ * need.
+ */
+ if ((buf == NULL && *bufsz != 0) || (buf != NULL && *bufsz == 0))
+ return (NULL); /* XXX Surface error? */
+
+ /*
+ * If we're doing any normalization beyond our standard size
+ * normalization, we apply those rules up front since they may alter our
+ * disk size every so slightly.
+ */
+ if (ctx->normalize != 0 && !libder_obj_normalize(root, ctx))
+ return (NULL);
+
+ needed = libder_obj_disk_size(root, true);
+ if (needed == 0)
+ return (NULL); /* Overflow */
+
+ /* Allocate if we weren't passed a buffer. */
+ if (*bufsz == 0) {
+ *bufsz = needed;
+ buf = malloc(needed);
+ if (buf == NULL)
+ return (NULL);
+ } else if (needed > *bufsz) {
+ *bufsz = needed;
+ return (NULL); /* Insufficient space */
+ }
+
+ /* Buffer large enough, write into it. */
+ mwrite.buf = buf;
+ mwrite.bufsz = *bufsz;
+ if (!libder_write_object(ctx, root, &memory_write, &mwrite)) {
+ libder_bzero(mwrite.buf, mwrite.offset);
+ free(buf);
+ return (NULL); /* XXX Error */
+ }
+
+ /*
+ * We don't normalize the in-memory representation of the tree, we do
+ * that as we're writing into the buffer. It could be the case that we
+ * didn't need the full buffer as a result of normalization.
+ */
+ *bufsz = mwrite.offset;
+
+ return (buf);
+}
diff --git a/tests/.gitignore b/tests/.gitignore
new file mode 100644
index 000000000000..075588a81e75
--- /dev/null
+++ b/tests/.gitignore
@@ -0,0 +1,12 @@
+CORPUS*
+crash-*
+leak-*
+oom-*
+*.log
+
+fuzz_*
+test_*
+!*.c
+!*.h
+
+make_corpus
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
new file mode 100644
index 000000000000..fc366ab88ed7
--- /dev/null
+++ b/tests/CMakeLists.txt
@@ -0,0 +1,41 @@
+set(FUZZERS fuzz_parallel fuzz_stream fuzz_write)
+set(UTILS )
+set(TESTS test_privkey test_pubkey)
+
+set(ALL_TESTS ${UTILS} ${TESTS})
+
+if(BUILD_FUZZERS)
+ set(UTILS ${UTILS} make_corpus)
+ set(ALL_TESTS ${ALL_TESTS} ${FUZZERS} make_corpus)
+
+ foreach(fuzzer IN LISTS FUZZERS)
+ add_executable(${fuzzer} ${fuzzer}.c)
+
+ target_compile_options(${fuzzer} PUBLIC -fsanitize=fuzzer)
+ target_link_options(${fuzzer} PUBLIC -fsanitize=fuzzer)
+ endforeach()
+
+ target_link_options(fuzz_parallel PUBLIC -pthread)
+endif()
+
+foreach(prog IN LISTS UTILS TESTS)
+ add_executable(${prog} ${prog}.c)
+endforeach()
+
+foreach(prog IN LISTS ALL_TESTS)
+ target_include_directories(${prog} PRIVATE ${CMAKE_SOURCE_DIR}/libder)
+ target_link_libraries(${prog} der_static)
+endforeach()
+
+add_custom_command(TARGET test_privkey POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/repo.priv ${CMAKE_CURRENT_BINARY_DIR}/repo.priv)
+add_custom_command(TARGET test_pubkey POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/repo.pub ${CMAKE_CURRENT_BINARY_DIR}/repo.pub)
+
+add_custom_target(check
+ DEPENDS test_pubkey test_privkey
+ COMMAND "${CMAKE_CURRENT_BINARY_DIR}/test_pubkey"
+ COMMAND "${CMAKE_CURRENT_BINARY_DIR}/test_privkey"
+)
diff --git a/tests/fuzz_parallel.c b/tests/fuzz_parallel.c
new file mode 100644
index 000000000000..afd4425970a2
--- /dev/null
+++ b/tests/fuzz_parallel.c
@@ -0,0 +1,111 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/param.h>
+#include <sys/socket.h>
+
+#include <assert.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <libder.h>
+
+#include "fuzzers.h"
+
+struct fuzz_frame {
+ uint8_t frame_threads;
+};
+
+struct thread_input {
+ const uint8_t *data;
+ size_t datasz;
+};
+
+static void *
+thread_main(void *cookie)
+{
+ const struct thread_input *input = cookie;
+ struct libder_ctx *ctx;
+ struct libder_object *obj;
+ const uint8_t *data = input->data;
+ size_t readsz, sz = input->datasz;
+
+ ctx = libder_open();
+ readsz = sz;
+ obj = libder_read(ctx, data, &readsz);
+ if (obj == NULL || readsz != sz)
+ goto out;
+
+ if (obj != NULL) {
+ uint8_t *buf = NULL;
+ size_t bufsz = 0;
+
+ /*
+ * If we successfully read it, then it shouldn't
+ * overflow. We're letting libder allocate the buffer,
+ * so we shouldn't be able to hit the 'too small' bit.
+ *
+ * I can't imagine what other errors might happen, so
+ * we'll just assert on it.
+ */
+ buf = libder_write(ctx, obj, buf, &bufsz);
+ if (buf == NULL)
+ goto out;
+
+ assert(bufsz != 0);
+
+ free(buf);
+ }
+
+out:
+ libder_obj_free(obj);
+ libder_close(ctx);
+ return (NULL);
+}
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t sz)
+{
+ const struct fuzz_frame *frame;
+ pthread_t *threads;
+ struct thread_input inp;
+ size_t nthreads;
+
+ if (sz <= sizeof(*frame))
+ return (-1);
+
+ frame = (const void *)data;
+ data += sizeof(*frame);
+ sz -= sizeof(*frame);
+
+ if (frame->frame_threads < 2)
+ return (-1);
+
+ threads = malloc(sizeof(*threads) * frame->frame_threads);
+ if (threads == NULL)
+ return (-1);
+
+ inp.data = data;
+ inp.datasz = sz;
+
+ for (nthreads = 0; nthreads < frame->frame_threads; nthreads++) {
+ if (pthread_create(&threads[nthreads], NULL, thread_main,
+ &inp) != 0)
+ break;
+ }
+
+ for (uint8_t i = 0; i < nthreads; i++)
+ pthread_join(threads[i], NULL);
+
+ free(threads);
+
+ return (0);
+}
diff --git a/tests/fuzz_stream.c b/tests/fuzz_stream.c
new file mode 100644
index 000000000000..0f7cc6167e7e
--- /dev/null
+++ b/tests/fuzz_stream.c
@@ -0,0 +1,246 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/param.h>
+#include <sys/socket.h>
+
+#include <assert.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <libder.h>
+
+#include "fuzzers.h"
+
+struct supply_data {
+ const uint8_t *data;
+ volatile size_t datasz;
+ int socket;
+};
+
+static void *
+supply_thread(void *data)
+{
+ struct supply_data *sdata = data;
+ size_t sz = sdata->datasz;
+ ssize_t writesz;
+
+ do {
+ writesz = write(sdata->socket, sdata->data, sz);
+
+ data += writesz;
+ sz -= writesz;
+ } while (sz != 0 && writesz > 0);
+
+ sdata->datasz = sz;
+ shutdown(sdata->socket, SHUT_RDWR);
+ close(sdata->socket);
+
+ return (NULL);
+}
+
+static int
+fuzz_fd(const struct fuzz_params *fparams, const uint8_t *data, size_t sz)
+{
+ struct supply_data sdata;
+ struct libder_ctx *ctx;
+ struct libder_object *obj;
+ size_t totalsz;
+ int sockets[2];
+ pid_t pid;
+ int ret;
+
+ ret = socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0,
+ &sockets[0]);
+ if (ret == -1)
+ return (-1);
+
+ sdata.data = data;
+ sdata.datasz = sz;
+ sdata.socket = sockets[1];
+ signal(SIGCHLD, SIG_IGN);
+ pid = fork();
+ if (pid == -1) {
+ close(sockets[0]);
+ close(sockets[1]);
+ return (-1);
+ }
+
+ if (pid == 0) {
+ close(sockets[0]);
+ supply_thread(&sdata);
+ _exit(0);
+ } else {
+ close(sockets[1]);
+ }
+
+ totalsz = 0;
+ ret = 0;
+ ctx = libder_open();
+ libder_set_strict(ctx, !!fparams->strict);
+ while (totalsz < sz) {
+ size_t readsz = 0;
+
+ obj = libder_read_fd(ctx, sockets[0], &readsz);
+ libder_obj_free(obj);
+
+ /*
+ * Even invalid reads should consume at least one byte.
+ */
+ assert(readsz != 0);
+
+ totalsz += readsz;
+ if (readsz == 0)
+ break;
+ }
+
+ assert(totalsz == sz);
+ libder_close(ctx);
+ close(sockets[0]);
+
+ return (ret);
+}
+
+static int
+fuzz_file(const struct fuzz_params *fparams, const uint8_t *data, size_t sz)
+{
+ FILE *fp;
+ struct libder_ctx *ctx;
+ struct libder_object *obj;
+ size_t totalsz;
+ int ret;
+
+ if (fparams->buftype >= BUFFER_END)
+ return (-1);
+
+ fp = fmemopen(__DECONST(void *, data), sz, "rb");
+ assert(fp != NULL);
+
+ switch (fparams->buftype) {
+ case BUFFER_NONE:
+ setvbuf(fp, NULL, 0, _IONBF);
+ break;
+ case BUFFER_FULL:
+ setvbuf(fp, NULL, 0, _IOFBF);
+ break;
+ case BUFFER_END:
+ assert(0);
+ }
+
+ totalsz = 0;
+ ret = 0;
+ ctx = libder_open();
+ libder_set_strict(ctx, !!fparams->strict);
+ while (!feof(fp)) {
+ size_t readsz = 0;
+
+ obj = libder_read_file(ctx, fp, &readsz);
+ libder_obj_free(obj);
+
+ if (obj == NULL)
+ assert(readsz != 0 || feof(fp));
+ else
+ assert(readsz != 0);
+
+ totalsz += readsz;
+ }
+
+ assert(totalsz == sz);
+ libder_close(ctx);
+ fclose(fp);
+
+ return (ret);
+}
+
+static int
+fuzz_plain(const struct fuzz_params *fparams, const uint8_t *data, size_t sz)
+{
+ struct libder_ctx *ctx;
+ struct libder_object *obj;
+ int ret;
+
+ if (sz == 0)
+ return (-1);
+
+ ret = 0;
+ ctx = libder_open();
+ libder_set_strict(ctx, !!fparams->strict);
+ do {
+ size_t readsz;
+
+ readsz = sz;
+ obj = libder_read(ctx, data, &readsz);
+ libder_obj_free(obj);
+
+ if (obj == NULL)
+ assert(readsz != 0 || readsz == sz);
+ else
+ assert(readsz != 0);
+
+ /*
+ * If we hit an entirely invalid segment of the buffer, we'll
+ * just skip a byte and try again.
+ */
+ data += MAX(1, readsz);
+ sz -= MAX(1, readsz);
+ } while (sz != 0);
+
+ libder_close(ctx);
+
+ return (ret);
+};
+
+static bool
+validate_padding(const struct fuzz_params *fparams)
+{
+ const uint8_t *end = (const void *)(fparams + 1);
+ const uint8_t *pad = (const uint8_t *)&fparams->PARAM_PAD_START;
+
+ while (pad < end) {
+ if (*pad++ != 0)
+ return (false);
+ }
+
+ return (true);
+}
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t sz)
+{
+ const struct fuzz_params *fparams;
+
+ if (sz <= sizeof(*fparams))
+ return (-1);
+
+ fparams = (const void *)data;
+ if (fparams->type >= STREAM_END)
+ return (-1);
+
+ if (!validate_padding(fparams))
+ return (-1);
+
+ data += sizeof(*fparams);
+ sz -= sizeof(*fparams);
+
+ if (fparams->type != STREAM_FILE && fparams->buftype != BUFFER_NONE)
+ return (-1);
+
+ switch (fparams->type) {
+ case STREAM_FD:
+ return (fuzz_fd(fparams, data, sz));
+ case STREAM_FILE:
+ return (fuzz_file(fparams, data, sz));
+ case STREAM_PLAIN:
+ return (fuzz_plain(fparams, data, sz));
+ case STREAM_END:
+ assert(0);
+ }
+
+ __builtin_trap();
+}
diff --git a/tests/fuzz_write.c b/tests/fuzz_write.c
new file mode 100644
index 000000000000..2ad5b5eb1764
--- /dev/null
+++ b/tests/fuzz_write.c
@@ -0,0 +1,79 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/param.h>
+#include <sys/socket.h>
+
+#include <assert.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <libder.h>
+
+#include "fuzzers.h"
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t sz)
+{
+ struct libder_ctx *ctx;
+ struct libder_object *obj;
+ size_t readsz;
+ int ret;
+ bool strict;
+
+ if (sz < 2)
+ return (-1);
+
+ /*
+ * I worked this in originally by just using the high bit of the first
+ * byte, but then I realized that encoding it that way would make it
+ * impossible to get strict validation of universal and application
+ * tags. The former is a bit more important than the latter.
+ */
+ strict = !!data[0];
+ data++;
+ sz--;
+
+ ctx = libder_open();
+ libder_set_strict(ctx, strict);
+ ret = -1;
+ readsz = sz;
+ obj = libder_read(ctx, data, &readsz);
+ if (obj == NULL || readsz != sz)
+ goto out;
+
+ if (obj != NULL) {
+ uint8_t *buf = NULL;
+ size_t bufsz = 0;
+
+ /*
+ * If we successfully read it, then it shouldn't
+ * overflow. We're letting libder allocate the buffer,
+ * so we shouldn't be able to hit the 'too small' bit.
+ *
+ * I can't imagine what other errors might happen, so
+ * we'll just assert on it.
+ */
+ buf = libder_write(ctx, obj, buf, &bufsz);
+ if (buf == NULL)
+ goto out;
+
+ assert(bufsz != 0);
+
+ free(buf);
+ }
+
+ ret = 0;
+
+out:
+ libder_obj_free(obj);
+ libder_close(ctx);
+
+ return (ret);
+}
diff --git a/tests/fuzzers.h b/tests/fuzzers.h
new file mode 100644
index 000000000000..0f94bc7f25fb
--- /dev/null
+++ b/tests/fuzzers.h
@@ -0,0 +1,40 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+enum stream_type {
+ STREAM_FD = 0, /* read_fd() type */
+ STREAM_FILE = 1, /* read_file() type */
+ STREAM_PLAIN = 2,
+
+ STREAM_END
+} __attribute__((packed));
+
+enum stream_buffer {
+ BUFFER_NONE = 0,
+ BUFFER_FULL = 1,
+
+ BUFFER_END,
+} __attribute__((packed));
+
+struct fuzz_params {
+ enum stream_type type;
+ enum stream_buffer buftype;
+
+#define PARAM_PAD_START _pad0
+ uint8_t strict;
+ uint8_t _pad0[5];
+
+ /* Give me plenty of padding. */
+ uint64_t padding[3];
+};
+
+_Static_assert(sizeof(struct fuzz_params) == 32,
+ "fuzz_params ABI broken, will invalidate CORPUS");
+
diff --git a/tests/make_corpus.c b/tests/make_corpus.c
new file mode 100644
index 000000000000..68554d7c17de
--- /dev/null
+++ b/tests/make_corpus.c
@@ -0,0 +1,137 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/param.h>
+
+#undef NDEBUG
+#include <assert.h>
+#include <err.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <libder.h>
+
+#include "fuzzers.h"
+
+#define LARGE_SIZE (1024 * 64)
+
+static const uint8_t empty_seq[] = { BT_SEQUENCE, 0x00 };
+static const uint8_t long_size[21] = { BT_OCTETSTRING, 0x83, 0x00, 0x00, 0x10 };
+
+/* 64k */
+#define LARGE_SIZE_ENCODING 0x83, 0x01, 0x00, 0x00
+static const uint8_t large_octet[LARGE_SIZE + 5] = { BT_OCTETSTRING, LARGE_SIZE_ENCODING };
+
+#define VARLEN_SEQ BT_OCTETSTRING, 0x04, 0x01, 0x02, 0x03, 0x04
+#define VARLEN_CHILDREN VARLEN_SEQ, VARLEN_SEQ, VARLEN_SEQ
+static const uint8_t varlen[] = { BT_SEQUENCE, 0x80,
+ VARLEN_CHILDREN, 0x00, 0x00 };
+
+#define BITSTRING1 BT_BITSTRING, 0x04, 0x03, 0xc0, 0xc0, 0xcc
+#define BITSTRING2 BT_BITSTRING, 0x04, 0x05, 0xdd, 0xdd, 0xff
+static const uint8_t constructed_bitstring[] = { 0x20 | BT_BITSTRING,
+ 2 * 6, BITSTRING1, BITSTRING2 };
+
+#define FUZZER_SEED(seq) { #seq, sizeof(seq), seq }
+static const struct seed {
+ const char *seed_name;
+ size_t seed_seqsz;
+ const uint8_t *seed_seq;
+} seeds[] = {
+ FUZZER_SEED(empty_seq),
+ FUZZER_SEED(long_size),
+ FUZZER_SEED(large_octet),
+ FUZZER_SEED(varlen),
+ FUZZER_SEED(constructed_bitstring),
+};
+
+static void
+usage(void)
+{
+ fprintf(stderr, "usage: %s [-H] <corpus-dir>\n", getprogname());
+ exit(1);
+}
+
+static void
+write_one(const struct fuzz_params *params, const struct seed *seed, int dirfd,
+ bool striphdr)
+{
+ char *name;
+ int fd = -1;
+
+ assert(asprintf(&name, "base_%d_%d_%d_%s", params->type,
+ params->buftype, params->strict, seed->seed_name) != -1);
+
+ fd = openat(dirfd, name, O_RDWR | O_TRUNC | O_CREAT, 0644);
+ assert(fd != -1);
+
+ /*
+ * Write our params + seed; if we're stripping the header we won't have
+ * the full params, but we'll still have our signal byte for strict
+ * mode.
+ */
+ if (!striphdr)
+ assert(write(fd, &params, sizeof(params)) == sizeof(params));
+ else
+ assert(write(fd, &params->strict, sizeof(params->strict)) == sizeof(params->strict));
+
+ assert(write(fd, seed->seed_seq, seed->seed_seqsz) == seed->seed_seqsz);
+
+ free(name);
+ close(fd);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct fuzz_params params;
+ const struct seed *seed;
+ const char *seed_dir;
+ int dirfd = -1;
+ bool striphdr = false;
+
+ if (argc < 2 || argc > 3)
+ usage();
+
+ if (argc == 3 && strcmp(argv[1], "-H") != 0)
+ usage();
+
+ striphdr = argc == 3;
+ seed_dir = argv[argc - 1];
+
+ dirfd = open(seed_dir, O_SEARCH);
+ if (dirfd == -1)
+ err(1, "%s: open", seed_dir);
+
+ memset(&params, 0, sizeof(params));
+
+ for (int type = 0; type < STREAM_END; type++) {
+ params.type = type;
+
+ for (int buffered = 0; buffered < BUFFER_END; buffered++) {
+ params.buftype = buffered;
+
+ for (uint8_t strict = 0; strict < 2; strict++) {
+ params.strict = strict;
+
+ for (size_t i = 0; i < nitems(seeds); i++) {
+ seed = &seeds[i];
+
+ write_one(&params, seed, dirfd, striphdr);
+ }
+ }
+
+ if (type != STREAM_FILE)
+ break;
+ }
+ }
+
+ close(dirfd);
+}
diff --git a/tests/repo.priv b/tests/repo.priv
new file mode 100644
index 000000000000..74a030b6802c
--- /dev/null
+++ b/tests/repo.priv
Binary files differ
diff --git a/tests/repo.pub b/tests/repo.pub
new file mode 100644
index 000000000000..bdcb1a20a1c7
--- /dev/null
+++ b/tests/repo.pub
Binary files differ
diff --git a/tests/test_common.h b/tests/test_common.h
new file mode 100644
index 000000000000..76e850f19128
--- /dev/null
+++ b/tests/test_common.h
@@ -0,0 +1,29 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <assert.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <limits.h>
+#include <stdlib.h>
+
+static inline int
+open_progdir(const char *prog)
+{
+ char pdir[PATH_MAX], *resolved;
+ int dfd;
+
+ resolved = realpath(prog, &pdir[0]);
+ assert(resolved != NULL);
+
+ resolved = dirname(&pdir[0]);
+ assert(resolved != NULL);
+
+ dfd = open(resolved, O_DIRECTORY);
+ assert(dfd != -1);
+
+ return (dfd);
+}
diff --git a/tests/test_privkey.c b/tests/test_privkey.c
new file mode 100644
index 000000000000..5e7519f5a715
--- /dev/null
+++ b/tests/test_privkey.c
@@ -0,0 +1,175 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/stat.h>
+
+#include <assert.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <libder.h>
+
+#include "test_common.h"
+
+/*
+ * Note that the choice of secp112r1 is completely arbitrary. I was mainly
+ * shooting for something pretty weak to avoid people trying to "catch me"
+ * checking in private key material, even though it's very incredibly clearly
+ * just for a test case.
+ */
+static const uint8_t oid_secp112r1[] =
+ { 0x2b, 0x81, 0x04, 0x00, 0x06 };
+
+static const uint8_t privdata[] = { 0xa5, 0xf5, 0x2a, 0x56, 0x61, 0xe3, 0x58,
+ 0x76, 0x5c, 0x4f, 0xd6, 0x8d, 0x60, 0x54 };
+
+static const uint8_t pubdata[] = { 0x00, 0x04, 0xae, 0x69, 0x41, 0x0d, 0x9c,
+ 0x9b, 0xf2, 0x34, 0xf6, 0x2d, 0x7c, 0x91, 0xe1, 0xc7, 0x7f, 0x23, 0xa0,
+ 0x84, 0x34, 0x5c, 0x38, 0x26, 0xd8, 0xcf, 0xbe, 0xf7, 0xdc, 0x8a };
+
+static void
+test_interface(struct libder_object *root)
+{
+ const uint8_t *data;
+ size_t datasz;
+ struct libder_object *keystring, *oid;
+
+ /* Grab the oid first. */
+ oid = libder_obj_child(root, 2);
+ assert(oid != NULL); /* Actually just the container... */
+ assert(libder_obj_type_simple(oid) == 0xa0);
+
+ oid = libder_obj_child(oid, 0);
+ assert(oid != NULL); /* Now *that*'s an OID. */
+ assert(libder_obj_type_simple(oid) == BT_OID);
+ data = libder_obj_data(oid, &datasz);
+ assert(datasz == sizeof(oid_secp112r1));
+ assert(memcmp(oid_secp112r1, data, datasz) == 0);
+
+ keystring = libder_obj_child(root, 1);
+ assert(keystring != NULL);
+ assert(libder_obj_type_simple(keystring) == BT_OCTETSTRING);
+
+ data = libder_obj_data(keystring, &datasz);
+ assert(datasz == sizeof(privdata));
+ assert(memcmp(privdata, data, datasz) == 0);
+}
+
+/* buf and bufszs are just our reference */
+static void
+test_construction(struct libder_ctx *ctx, const uint8_t *buf, size_t bufsz)
+{
+ uint8_t *out;
+ struct libder_object *obj, *oidp, *pubp, *root;
+ struct libder_object *keystring;
+ size_t outsz;
+ uint8_t data;
+
+ root = libder_obj_alloc_simple(ctx, BT_SEQUENCE, NULL, 0);
+ assert(root != NULL);
+
+ data = 1;
+ obj = libder_obj_alloc_simple(ctx, BT_INTEGER, &data, 1);
+ assert(obj != NULL);
+ assert(libder_obj_append(root, obj));
+
+ /* Private key material */
+ obj = libder_obj_alloc_simple(ctx, BT_OCTETSTRING, privdata, sizeof(privdata));
+ assert(obj != NULL);
+ assert(libder_obj_append(root, obj));
+
+ /* Now throw in the OID and pubkey containers */
+ oidp = libder_obj_alloc_simple(ctx,
+ (BC_CONTEXT << 6) | BER_TYPE_CONSTRUCTED_MASK | 0, NULL, 0);
+ assert(oidp != NULL);
+ assert(libder_obj_append(root, oidp));
+
+ pubp = libder_obj_alloc_simple(ctx,
+ (BC_CONTEXT << 6) | BER_TYPE_CONSTRUCTED_MASK | 1, NULL, 0);
+ assert(pubp != NULL);
+ assert(libder_obj_append(root, pubp));
+
+ /* Actually add the OID */
+ obj = libder_obj_alloc_simple(ctx, BT_OID, oid_secp112r1, sizeof(oid_secp112r1));
+ assert(obj != NULL);
+ assert(libder_obj_append(oidp, obj));
+
+ /* Finally, add the pubkey */
+ obj = libder_obj_alloc_simple(ctx, BT_BITSTRING, pubdata, sizeof(pubdata));
+ assert(obj != NULL);
+ assert(libder_obj_append(pubp, obj));
+
+ out = NULL;
+ outsz = 0;
+ out = libder_write(ctx, root, out, &outsz);
+ assert(out != NULL);
+ assert(outsz == bufsz);
+
+ assert(memcmp(out, buf, bufsz) == 0);
+
+ libder_obj_free(root);
+ free(out);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct stat sb;
+ struct libder_ctx *ctx;
+ struct libder_object *root;
+ uint8_t *buf, *out;
+ size_t bufsz, outsz, rootsz;
+ ssize_t readsz;
+ int dfd, error, fd;
+
+ dfd = open_progdir(argv[0]);
+
+ fd = openat(dfd, "repo.priv", O_RDONLY);
+ assert(fd >= 0);
+
+ close(dfd);
+ dfd = -1;
+
+ error = fstat(fd, &sb);
+ assert(error == 0);
+
+ bufsz = sb.st_size;
+ buf = malloc(bufsz);
+ assert(buf != NULL);
+
+ readsz = read(fd, buf, bufsz);
+ close(fd);
+
+ assert(readsz == bufsz);
+
+ ctx = libder_open();
+ rootsz = bufsz;
+ libder_set_verbose(ctx, 2);
+ root = libder_read(ctx, buf, &rootsz);
+
+ assert(root != NULL);
+ assert(rootsz == bufsz);
+
+ test_interface(root);
+ test_construction(ctx, buf, bufsz);
+
+ outsz = 0;
+ out = NULL;
+ out = libder_write(ctx, root, out, &outsz);
+ assert(out != NULL);
+ assert(outsz == bufsz);
+
+ assert(memcmp(buf, out, outsz) == 0);
+
+ free(out);
+ free(buf);
+ libder_obj_free(root);
+ libder_close(ctx);
+}
diff --git a/tests/test_pubkey.c b/tests/test_pubkey.c
new file mode 100644
index 000000000000..9fbd070f0e87
--- /dev/null
+++ b/tests/test_pubkey.c
@@ -0,0 +1,143 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/stat.h>
+
+#include <assert.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <libder.h>
+
+#include "test_common.h"
+
+static const uint8_t oid_ecpubkey[] =
+ { 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01 };
+static const uint8_t oid_secp256k1[] =
+ { 0x2b, 0x81, 0x04, 0x00, 0x0a };
+
+static const uint8_t pubdata[] = { 0x00, 0x04, 0xd1, 0x76, 0x20, 0x39, 0xe5, 0x3e,
+ 0x67, 0x7d, 0x8d, 0xfd, 0xc4, 0x21, 0x20, 0xcd, 0xb0, 0xbf, 0x47, 0x87, 0x6a,
+ 0xf8, 0x07, 0x73, 0xbe, 0xbe, 0xd5, 0xbb, 0x3c, 0xbc, 0x32, 0x93, 0xd9, 0xdf,
+ 0x96, 0x25, 0xb7, 0x0e, 0x3c, 0x55, 0x12, 0xee, 0x7a, 0x02, 0x39, 0x0f, 0xee,
+ 0x7b, 0xfe, 0x1a, 0x93, 0x76, 0xf7, 0xc2, 0xac, 0x05, 0xba, 0x9a, 0x83, 0x37,
+ 0xf5, 0xcd, 0x55, 0x57, 0x39, 0x6f };
+
+static void
+test_interface(struct libder_object *root)
+{
+ const uint8_t *data;
+ size_t datasz;
+ struct libder_object *keystring;
+
+ keystring = libder_obj_child(root, 1);
+ assert(keystring != NULL);
+ assert(libder_obj_type_simple(keystring) == BT_BITSTRING);
+
+ data = libder_obj_data(keystring, &datasz);
+ assert(datasz == sizeof(pubdata));
+ assert(memcmp(pubdata, data, datasz) == 0);
+}
+
+/* buf and bufszs are just our reference */
+static void
+test_construction(struct libder_ctx*ctx, const uint8_t *buf, size_t bufsz)
+{
+ uint8_t *out;
+ struct libder_object *obj, *params, *root;
+ struct libder_object *keystring;
+ size_t outsz;
+
+ root = libder_obj_alloc_simple(ctx, BT_SEQUENCE, NULL, 0);
+ assert(root != NULL);
+
+ params = libder_obj_alloc_simple(ctx, BT_SEQUENCE, NULL, 0);
+ assert(params != NULL);
+ assert(libder_obj_append(root, params));
+
+ keystring = libder_obj_alloc_simple(ctx, BT_BITSTRING, pubdata, sizeof(pubdata));
+ assert(keystring != NULL);
+ assert(libder_obj_append(root, keystring));
+
+ /* Now go back and build the two params, id and curve */
+ obj = libder_obj_alloc_simple(ctx, BT_OID, oid_ecpubkey, sizeof(oid_ecpubkey));
+ assert(obj != NULL);
+ assert(libder_obj_append(params, obj));
+
+ obj = libder_obj_alloc_simple(ctx, BT_OID, oid_secp256k1, sizeof(oid_secp256k1));
+ assert(obj != NULL);
+ assert(libder_obj_append(params, obj));
+
+ out = NULL;
+ outsz = 0;
+ out = libder_write(ctx, root, out, &outsz);
+ assert(out != NULL);
+ assert(outsz == bufsz);
+ assert(memcmp(out, buf, bufsz) == 0);
+
+ libder_obj_free(root);
+ free(out);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct stat sb;
+ struct libder_ctx *ctx;
+ struct libder_object *root;
+ uint8_t *buf, *out;
+ size_t bufsz, outsz, rootsz;
+ ssize_t readsz;
+ int dfd, error, fd;
+
+ dfd = open_progdir(argv[0]);
+
+ fd = openat(dfd, "repo.pub", O_RDONLY);
+ assert(fd >= 0);
+
+ close(dfd);
+ dfd = -1;
+
+ error = fstat(fd, &sb);
+ assert(error == 0);
+
+ bufsz = sb.st_size;
+ buf = malloc(bufsz);
+ assert(buf != NULL);
+
+ readsz = read(fd, buf, bufsz);
+ close(fd);
+
+ assert(readsz == bufsz);
+
+ ctx = libder_open();
+ rootsz = bufsz;
+ libder_set_verbose(ctx, 2);
+ root = libder_read(ctx, buf, &rootsz);
+
+ assert(root != NULL);
+ assert(rootsz == bufsz);
+
+ test_interface(root);
+ test_construction(ctx, buf, bufsz);
+
+ outsz = 0;
+ out = NULL;
+ out = libder_write(ctx, root, out, &outsz);
+ assert(out != NULL);
+ assert(outsz == bufsz);
+
+ assert(memcmp(buf, out, outsz) == 0);
+
+ free(out);
+ free(buf);
+ libder_obj_free(root);
+ libder_close(ctx);
+}