[stdlib] Add PEM Encoding/Decoding Module (#2858)

* [stdlib] Add PEM Encoding/Decoding Module
* release notes
* Removed some unnecessary macro usages. Fixed memory handling with headers.
* Make end of line a parameter. Internal encode method -> function. Use more tmem. Remove t-functions.
* Update API

---------

Co-authored-by: Christoffer Lerno <christoffer@aegik.com>
Co-authored-by: Christoffer Lerno <christoffer.lerno@gmail.com>
This commit is contained in:
Zack Puhl
2026-02-05 18:01:07 -05:00
committed by GitHub
parent 1933d47ba1
commit 4521a25284
4 changed files with 648 additions and 1 deletions

View File

@@ -160,7 +160,12 @@ macro bool char_in_set(char c, String set)
}
<*
@return "a String which is safe to convert to a ZString"
Join together an array of strings via a "joiner" sequence, which is inserted between each element.
@param [&inout] allocator : "The allocator to use."
@param [in] s : "An array of strings to join in sequence."
@param [in] joiner : "The string used to join each element of `s`."
@return "A single string containing the result, allocated via `allocator`, safe to convert to a ZString."
*>
fn String join(Allocator allocator, String[] s, String joiner)
{
@@ -189,6 +194,9 @@ fn String join(Allocator allocator, String[] s, String joiner)
return (String)data[:offset];
}
<* Alias for `string::join` using the temp allocator. *>
macro String tjoin(String[] s, String joiner) => join(tmem, s, joiner);
<*
Replace all instances of one substring with a different string.

355
lib/std/encoding/pem.c3 Normal file
View File

@@ -0,0 +1,355 @@
// Copyright (c) 2026 Zack Puhl <github@xmit.xyz>. All rights reserved.
// Use of this source code is governed by the MIT license
// a copy of which can be found in the LICENSE_STDLIB file.
//
// A module for encoding or decoding PEM blobs [mostly] in accordance with RFCs 1421-1424.
// This implementation retains a lot of flexibility in parsing input PEM blobs.
//
module std::encoding::pem;
import std::collections, std::encoding::base64;
<* A safe, default tag to use per RFC 1421's rules. *>
const String DEFAULT_TAG = "PRIVACY-ENHANCED MESSAGE";
<* The set of characters which are considered valid for PEM tags (which appear inside of Encapsulation Boundaries). *>
const AsciiCharset TAG_SET @local = ascii::@combine_sets(ascii::ALPHA_UPPER_SET, ascii::NUMBER_SET, ascii::@create_set(" _-/+()"));
<* The set of characters which are considered valid for optional PEM headers used. *>
const AsciiCharset HEADER_KEY_SET @local = ascii::@combine_sets(ascii::ALPHANUMERIC_SET, ascii::@create_set("!#$%&'*+-.^_`|~"));
<* All PEM Encapsulation Boundaries must use this delimiter to demarcate the PEM from its surrounding content, if any. *>
const String EB_DELIMITER @local = "-----";
<* All PEM blobs will start with this Encapsulation Boundary prefix. *>
const String PRE_EB_PREFIX @local = EB_DELIMITER +++ "BEGIN ";
<* All PEM blobs will terminate with this Encapsulation Boundary prefix. *>
const String POST_EB_PREFIX @local = EB_DELIMITER +++ "END ";
alias PemHeader = String[2];
<* Specify a set of possible PEM en/decoding faults. *>
faultdef
BODY_REQUIRED, // encoding: no body given (or too few of them)
HEADERS_REQUIRED, // encoding: no headers given (or too few of them)
HEADER_KEY_REQUIRED, // encoding: blank header keys are not allowed
HEADER_VALUE_REQUIRED, // encoding: blank header values are not allowed
INVALID_BODY, // decoding: invalid body, likely bad base64
INVALID_FORMAT, // decoding: invalid input formatting - no pre-EB or just plain wrong
INVALID_HEADER, // decoding: invalid headers
INVALID_HEADER_KEY, // decoding: invalid or empty header key
INVALID_PRE_EB, // decoding: invalid pre-EncapsBoundary BEFORE the PEM body
INVALID_POST_EB, // decoding: invalid post-EncapsBoundary AFTER the PEM body
INVALID_TAG, // decoding: invalid tag within an EB
MISMATCHED_TAG, // decoding: the tag from the pre-EB doesn't match that of the post-EB
MISSING_BODY, // decoding: missing PEM body base64
MISSING_HEADER_KEY, // decoding: the header is missing its key
MISSING_HEADER_VALUE, // decoding: the header is missing its value
MISSING_POST_EB, // decoding: no post-EB was found to close off the PEM
MISSING_TAG, // decoding: no tag was defined or parsed from the EB
TAG_REQUIRED, // encoding: no/empty tag given (or too few of them)
;
<* Represents a PEM object in memory, with a reference to the body data, tag value, and optional headers. *>
struct Pem
{
<* The allocator associated with the PEM's creation and destruction. *>
Allocator allocator;
<* A flexible 'tag' value used within the Encapsulation Boundary to denote the type of the PEM. *>
String tag;
<* A set of optional headers used to provide more context or information about the body of the PEM object. *>
LinkedHashMap{String, String} headers;
<* The core boy data of the PEM itself - the main values to be transmitted in this format. *>
char[] data;
}
<*
Create a new PEM object from a few inputs. Each input (i.e., tag, data, and headers) is copied to a new memory location.
The PEM object itself is not allocated in-memory, but is a simple container that points to each value that _is_.
Key-Value pairs for headers are provided in sequence as variadic arguments: `"key", "value", "key2", "value2", ...`
Created PEMs that are not temporary should be destroyed with `Pem.free`.
@param [&inout] allocator : "The allocator to use when copying the provided PEM object's fields."
@param [in] data : "The body data of the PEM."
@param [in] tag : "The tag value to use within the PEM's Encapsulation Boundary."
@return "A new PEM object."
*>
fn Pem create(Allocator allocator, char[] data, String tag, PemHeader... args)
{
Pem result = {
.allocator = allocator,
.tag = tag.copy(allocator),
.data = allocator::clone_slice(allocator, data),
};
result.headers.init(allocator, capacity: max(args.len, 16));
foreach (arg : args)
{
result.add_header(arg[0], arg[1]);
}
return result;
}
<*
Duplicate a `Pem` container and allocate copies of its members using the given allocator.
@param [&inout] allocator : "The allocator to use when copying the `Pem` members."
*>
fn Pem Pem.copy(&self, Allocator allocator)
{
Pem result = create(allocator, self.data, self.tag);
self.headers.@each(;String key, String value)
{
result.add_header(key, value);
};
return result;
}
<*
Safely destroys a `Pem` and deallocate all of its members. This should always be explicitly called when not using `tmem`.
*>
fn void Pem.free(&self)
{
mem::zero_volatile(self.data);
if (self.allocator != tmem)
{
self.headers.@each(;String key, String value)
{
allocator::free(self.allocator, value);
};
self.headers.free();
self.tag.free(self.allocator);
allocator::free(self.allocator, self.data);
}
mem::zero_volatile(@as_char_view(*self));
}
fn void Pem.add_header(&self, String key, String value)
{
(void)self.headers[key].free(self.allocator);
self.headers[key] = value.copy(self.allocator);
}
<*
Attempt to decode an input string into one or more `Pem` objects. If the input contains any non-PEM or otherwise
invalid data, then this will throw an error. Ideally, this function is used to decode PEM files explicitly, lest
the caller need to be sure they're only providing PEM data +/- some intermediate whitespace.
@param [&inout] allocator : "The allocator to use when creating the `Pem` outputs and members."
@param [in] input : "The string to parse one or more PEM blobs from."
@return "An array of decoded `Pem` objects, depending on how many were present in the input (separated optionally by whitespace)."
*>
fn Pem[]? decode(Allocator allocator, String input) => @pool()
{
List{Pem} pem_list;
pem_list.tinit();
String[] lines = input.treplace("\r\n", "\n").tsplit("\n");
foreach (&line : lines) *line = (*line).trim_right(); // remove any trailing whitespace as this can disrupt parsing (but shouldn't)
while (lines.len > 0)
{
pem_list.push(_decode_single(allocator, &lines)!);
while (lines.len > 0 && lines[0].trim().len == 0) lines = lines[1..]; // skip all empty lines in between or after PEM boundaries
}
return pem_list.to_array(allocator);
}
<*
INTERNAL ONLY: Decode one PEM at a time, from pre-EB to its discovered post-EB.
@param [&inout] allocator : "The allocator to use during decoding to return the result."
@param [&inout] lines_io : "A pointer to an input slice to modify as the single PEM is parsed from it."
*>
fn Pem? _decode_single(Allocator allocator, String[]* lines_io) @local
{
String[] lines = *lines_io; // copy to local var
Pem result = { .allocator = allocator };
result.headers.init(allocator);
defer catch result.free();
// Remove any preceding whitespace-only lines.
while (lines[0].trim().len == 0) lines = lines[1..];
if (lines.len < 3) return INVALID_FORMAT~; // at least 3 lines (pre-EB, body, post-EB) are always required
// The Pre-Encapsulation-Boundary must be of the format: -----BEGIN TAG-----, where "TAG" can be any upper-case identifier [A-Z_ -/]
String pre_eb = lines[0];
if (pre_eb[0:11] != PRE_EB_PREFIX || pre_eb[^5..] != EB_DELIMITER) return INVALID_PRE_EB~;
String tag = pre_eb[PRE_EB_PREFIX.len..^6];
if (!tag.len || !tag.trim().len) return MISSING_TAG~;
foreach (c : tag) if (!TAG_SET.contains(c)) return INVALID_TAG~;
result.tag = tag.copy(allocator);
// The Post-Encapsulation-Boundary is the same, but uses "END", and the extracted tag must match.
// Since the input might contain more than one PEM unit, we need to search for the ending encapsulation boundary dynamically.
String post_eb;
usz endl;
for SEARCH_EB: (endl = 1; endl < lines.len; endl++)
{
if (lines[endl].len > POST_EB_PREFIX.len && lines[endl][0:EB_DELIMITER.len] == EB_DELIMITER)
{
post_eb = lines[endl];
break SEARCH_EB;
}
}
if (!post_eb.len) return MISSING_POST_EB~;
if (post_eb[0:9] != POST_EB_PREFIX || post_eb[^5..] != EB_DELIMITER) return INVALID_POST_EB~;
String post_tag = post_eb[POST_EB_PREFIX.len..^6];
if (post_tag.len != tag.len || post_tag != tag) return MISMATCHED_TAG~;
// Now that the inner portion is decapsulated, tag is, strip off the boundaries.
*lines_io = lines[endl+1..]; // update the iterated slice of lines from the calling context - see: `decode`
lines = lines[1:endl-1];
// while there's a colon+space in the current line, we should assume that this is a key-value header pair
while (lines[0].contains(": "))
{
if (!HEADER_KEY_SET.contains(lines[0][0])) return INVALID_HEADER~; // not a multiline header? error out if the first char is not appropriate
String[] marker = lines; // temporary marker
usz span = 1; // how many lines this header spans
// Search for multi-line key-value pairs, indicated by a whitespace character beginning the current line.
for (lines = lines[1..]; lines[0].len > 0 && ascii::WHITESPACE_SET.contains(lines[0][0]); lines = lines[1..], span++);
foreach (&line : marker[:span]) *line = (*line).trim(); // always trim on both sides
String full_header = string::tjoin(marker[:span], " "); // join the lines with a single space
if (!full_header.contains(": ")) return INVALID_HEADER~; // reassert the presence of this
// Extract the key and value from the message, then validate.
// The header name should match a valid set of characters, but the value doesn't need to conform to anything other than existing
String[] kv = full_header.tsplit(": ", max: 2);
if (!kv[0].len) return MISSING_HEADER_KEY~;
if (!kv[1].len) return MISSING_HEADER_VALUE~;
foreach (c : kv[0]) if (!HEADER_KEY_SET.contains(c)) return INVALID_HEADER_KEY~;
result.add_header(kv[0], kv[1]); // finally, push the values
}
// if any headers were present, the line after the headers MUST BE EMPTY
if (result.headers.len() > 0)
{
if (lines[0].trim().len > 0) return INVALID_FORMAT~; // but we are forgiving about whitespace here
lines = lines[1..];
}
// Here, we assume lines[0] is the start of base64 data. This means there must be at least 1 line, of course.
if (lines.len < 1) return MISSING_BODY~;
// ... While the PEM format specifies a 64-character width on all but the last line of the base64 body,
// this parser doesn't need to be particular about that as long as the base64 is ok
// In this case, the rest of the lines in the set should be base64 and should decode accordingly
String to_decode = string::tjoin(lines, "");
if (!to_decode.len) return MISSING_BODY~; // paranoia
result.data = (base64::decode(allocator, to_decode) ?? INVALID_BODY~)!;
return result;
}
<*
Encodes a single `Pem` object into a new PEM-formatted string.
@param pem : "The pem object to encode"
@param [&inout] allocator : "The allocator to use for allocating the final encoded string."
*>
fn String? encode_pem(Pem pem, Allocator allocator, bool use_crlf = false)
{
if (!pem.data.len) return BODY_REQUIRED~;
if (!pem.tag.len) return TAG_REQUIRED~;
DString out;
out.tinit();
String line_ending = use_crlf ? "\r\n" : "\n";
@pool()
{
out.appendf(PRE_EB_PREFIX +++ "%s" +++ EB_DELIMITER +++ "%s", pem.tag, line_ending);
foreach KEY_ITER: (key : pem.headers.tkeys())
{
if (!key.len) return HEADER_KEY_REQUIRED~;
String value = pem.headers[key]!!;
if (!value.len) return HEADER_VALUE_REQUIRED~;
usz first_line_length = 64 - 2 - key.len;
if (value.len <= first_line_length)
{
out.appendf("%s: %s%s", key, value, line_ending);
continue KEY_ITER;
}
out.appendf("%s: %s%s", key, value[:first_line_length].trim(), line_ending);
value = value[first_line_length..];
while (value.len > 0)
{
out.appendf(" %s%s", (value.len >= 63 ? value[:63] : value[..]).trim(), line_ending);
value = value.len >= 63 ? value[63..] : {};
}
}
if (pem.headers.len() > 0) out.append(line_ending);
String body = base64::tencode(pem.data);
while (body.len > 0)
{
out.appendf("%s%s", body.len >= 64 ? body[:64] : body[..], line_ending);
body = body.len >= 64 ? body[64..] : {};
}
out.appendf(POST_EB_PREFIX +++ "%s" +++ EB_DELIMITER +++ "%s", pem.tag, line_ending);
};
return allocator == tmem ? out.str_view() : out.copy_str(allocator);
}
<*
Encodes a set of input data into a `String` containing the PEM-encoded data.
@param [&inout] allocator : "The allocator to use when creating the final output string."
@param [in] data : "The body data for the output PEM."
@param [in] tag : "The tag "
*>
fn String? encode(Allocator allocator, char[] data, String tag, PemHeader... headers, bool use_crlf = false) => @pool()
{
if (!data.len) return BODY_REQUIRED~;
return encode_pem(create(tmem, data, tag, ...headers), allocator, use_crlf);
}
<*
Encode many inputs to a single output string that represents chained/sequential PEM objects in the order they were provided.
The length of the `bodies` and `tags` array must be equal.
If headers are provided, they must be arrays of String objects, matching both the number of tags and the number of bodies.
@param [&inout] allocator : "The allocator to use when creating the final output string."
@param [in] bodies : "An ordered array of binary arrays, each representing the body of a single PEM."
@param [in] tags : "An ordered array of tag strings, each representing the tag of a single PEM."
@return "A new `String`, allocated with `allocator`, that contains all PEM objects in the order they were given."
*>
fn String? encode_many(Allocator allocator, char[][] bodies, String[] tags, PemHeader[]... pem_headers, bool use_crlf = false)
{
usz entries = max(bodies.len, tags.len, pem_headers.len);
switch
{
case bodies.len < entries: return BODY_REQUIRED~;
case tags.len < entries: return TAG_REQUIRED~;
case pem_headers.len > 0 && pem_headers.len < entries: return HEADERS_REQUIRED~;
}
DString out;
out.tinit();
if (!pem_headers.len)
{
foreach (x, body : bodies) @pool()
{
out.append(encode(tmem, body, tags[x], use_crlf: use_crlf)!);
};
}
else
{
foreach (i, headers : pem_headers) @pool()
{
out.append(encode(tmem, bodies[i], tags[i], ...headers, use_crlf: use_crlf)!);
};
}
return allocator == tmem ? out.str_view() : out.copy_str(allocator);
}

View File

@@ -11,6 +11,7 @@
- Summarize sort macros as generic function wrappers to reduce the amount of generated code. #2831
- Remove dependency on temp allocator in String.join.
- Remove dependency on temp allocator in File.open.
- Added PEM encoding/decoding. #2858
### Fixes
- Add error message if directory with output file name already exists

View File

@@ -0,0 +1,283 @@
// Copyright (c) 2026 Zack Puhl <github@xmit.xyz>. All rights reserved.
// Use of this source code is governed by the MIT license
// a copy of which can be found in the LICENSE_STDLIB file.
module pem_test @test;
import std::collections::pair, std::encoding::pem, std::io;
fn void decode_single()
{
Pem[] res = pem::decode(tmem, SINGLE)!!;
test::@check(res.len == 1);
test::@check(res[0].tag == "CERTIFICATE");
test::@check(res[0].headers.len() == 0);
test::@check(res[0].data == SINGLE_DATA);
}
fn void encode_single()
{
test::@check(pem::encode(tmem, SINGLE_DATA, "CERTIFICATE")!![..^2] == SINGLE);
test::@check(pem::encode(tmem, SINGLE_DATA, "CERTIFICATE", use_crlf: true)!![..^3] == SINGLE.treplace("\n", "\r\n"));
}
fn void decode_multi()
{
const Pair{String, String}[] EXPECTED = {
{ "Something", "Here" },
{ "A-Multiline", "Header Goes in this spot And another line to back it up" },
{ "Non-Multi", "line header!" },
{ "I'm-trying", "to confuse things: when parsing the content of this file" },
{ "done", "now" },
};
Pem[] res = pem::decode(tmem, MULTI)!!;
test::@check(res.len == 2);
test::@check(res[0].tag == "THIS IS NOT A REAL PRIVATE KEY");
test::@check(res[0].data == MULTI_DATA);
test::@check(res[0].headers.len() == 5);
foreach (pair : EXPECTED)
{
String val = res[0].headers[pair.first]!!;
test::@check(res[0].headers.has_key(pair.first), "Expected header key '%s' but it wasn't found.", pair.first);
test::@check(val == pair.second, "Expected value '%s' for key '%s' but did not get a match (got: '%s').", pair.second, pair.first, val);
}
test::@check(res[1].tag == "CERTIFICATE");
test::@check(res[1].headers.len() == 0);
test::@check(res[1].data == SINGLE_DATA);
}
fn void encode_multi()
{
String result = pem::encode_many(tmem, { SINGLE_DATA, MULTI_DATA }, { "CERTIFICATE", "THING" })!!;
test::@check(result == SIMPLE_MULTI, "Got:\n%s\n\nExpected:\n%s\n\n", result, SIMPLE_MULTI);
}
fn void encode_single_with_headers()
{
String result = pem::encode(tmem, SINGLE_DATA, "CERTIFICATE", {"My-Header", "Example"}, {"This-is-a-longer-header", "with some extra content that will bleed over to the next line."}, {"Last", "Header is single-line."})!!;
test::@check(result == SINGLE_WITH_HEADERS, "Got:\n%s\n\nExpected:\n%s\n\n", result, SINGLE_WITH_HEADERS);
}
fn void leak_check_decode()
{
Pem[] res = pem::decode(mem, MULTI)!!;
test::@check(res.len == 2);
// don't optimize it away >:I
io::printfn(res[0].tag);
io::printfn(res[1].tag);
foreach (r : res) r.free();
mem::free(res);
// ==========================================
res = pem::decode(mem, SINGLE)!!;
test::@check(res.len == 1);
io::printfn(res[0].tag);
foreach (r : res) r.free();
mem::free(res);
}
fn void leak_check_encode()
{
String encoded = pem::encode(mem, SINGLE_DATA, "SOMETHING", { "Key", "Value" }, {"KeyX", "ValueX" })!!;
test::@check(encoded.len > 0);
io::printfn("ENCODED_SINGLE:\n%s\n", encoded);
encoded.free(mem);
encoded = pem::encode_many(mem, { SINGLE_DATA, MULTI_DATA }, { "CERTIFICATE", "THING" }, (String[2][]) { {"Key1", "Value1"}, {"KeyX", "ValueX"} }, (String[2][]){ {"more", "more"} })!!;
test::@check(encoded.len > 0);
io::printfn("ENCODED_MULTI:\n%s\n", encoded);
encoded.free(mem);
}
fn void decode_errors()
{
foreach (i, pair : DECODE_ERRORS) test::@error(pem::decode(tmem, pair.first), pair.second);
}
// =============================================================================================
// These PEM blobs aren't 'real' in the sense that they're completely public. Tbh I kinda swiped
// them from another public repository, and the underlying CN is Superfish, Inc. If these were
// real exfiltrations at some point in time, then they're widely public as is... No harm done.
const String SINGLE = `-----BEGIN CERTIFICATE-----
MIIC9TCCAl6gAwIBAgIJANL8E4epRNznMA0GCSqGSIb3DQEBBQUAMFsxGDAWBgNV
BAoTD1N1cGVyZmlzaCwgSW5jLjELMAkGA1UEBxMCU0YxCzAJBgNVBAgTAkNBMQsw
CQYDVQQGEwJVUzEYMBYGA1UEAxMPU3VwZXJmaXNoLCBJbmMuMB4XDTE0MDUxMjE2
MjUyNloXDTM0MDUwNzE2MjUyNlowWzEYMBYGA1UEChMPU3VwZXJmaXNoLCBJbmMu
MQswCQYDVQQHEwJTRjELMAkGA1UECBMCQ0ExCzAJBgNVBAYTAlVTMRgwFgYDVQQD
Ew9TdXBlcmZpc2gsIEluYy4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOjz
Shh2Xxk/sc9Y6X9DBwmVgDXFD/5xMSeBmRImIKXfj2r8QlU57gk4idngNsSsAYJb
1Tnm+Y8HiN/+7vahFM6pdEXY/fAXVyqC4XouEpNarIrXFWPRt5tVgA9YvBxJ7SBi
3bZMpTrrHD2g/3pxptMQeDOuS8Ic/ZJKocPnQaQtAgMBAAGjgcAwgb0wDAYDVR0T
BAUwAwEB/zAdBgNVHQ4EFgQU+5izU38URC7o7tUJml4OVoaoNYgwgY0GA1UdIwSB
hTCBgoAU+5izU38URC7o7tUJml4OVoaoNYihX6RdMFsxGDAWBgNVBAoTD1N1cGVy
ZmlzaCwgSW5jLjELMAkGA1UEBxMCU0YxCzAJBgNVBAgTAkNBMQswCQYDVQQGEwJV
UzEYMBYGA1UEAxMPU3VwZXJmaXNoLCBJbmMuggkA0vwTh6lE3OcwDQYJKoZIhvcN
AQEFBQADgYEApHyg7ApKx3DEcWjzOyLi3JyN0JL+c35yK1VEmxu0Qusfr76645Oj
1IsYwpTws6a9ZTRMzST4GQvFFQra81eLqYbPbMPuhC+FCxkUF5i0DNSWi+kczJXJ
TtCqSwGl9t9JEoFqvtW+znZ9TqyLiOMw7TGEUI+88VAqW0qmXnwPcfo=
-----END CERTIFICATE-----`;
const String MULTI =
`-----BEGIN THIS IS NOT A REAL PRIVATE KEY-----
Something: Here
A-Multiline: Header
Goes in this spot
And another line to back it up
Non-Multi: line header!
I'm-trying: to confuse things: when parsing
the content of this file
done: now
MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIDHHhyAEZQoICAggA
MBQGCCqGSIb3DQMHBAiHEg+MCYQ30ASCAoDEvGvFRHvtWOb5Rc0f3lbVKqeUvWSz
xQn+rZELHnwb6baolmbFcsi6XkacVzL/EF7Ll4de/CSQ6pZZCCvfDzov0mPOuGve
SAe7hbAcol7+JWVfzbnVTblPf0i7mwSvK61cKq7YfcKJ2os/uJGpeX9zraywWyFx
f+EdTr348dOez8uHkURyY1cvSHsIdITALkChOonAYT68SVighTeB6xOCwfmsHx+X
3Qbhom2YCIxfJiaAoz2/LndCpDaEfOrVrxXFOKXrIbmeDEyjDQj16AVni9uuaj7l
NiO3zrrqxsfdVINPaAYRKQnS102jXqkH01z72c/MpMMC6dwZswF5V3R7RSXngyBn
1GLxVFHKR753Gt0IDag13Bd8Jt890/v0tE0Kx66jCkRGn+VCq6+bsnh7VpTH/cG5
dlFnv56lv2leknu5ghdJHX8YQ6HjnioaaheLA+ORAxqAlD8Itt1/pRBOOMSkutdz
d1px9dB2ZBpSoRAOcBwU5aFaw9uu+tXyzrPM3tZomu8ryQYMNlmVgPNDJOz6jPJi
jaZHWTS7U6j370oH/B0KTUG/ybrJGFnOmPP4h2u/ugG75EkfotURsvbrWuetQhOi
TCH+9nbIcT3pxnTXqI2IRHZXMturQ+6fqlJF3bb9bWarMBuC3KgprqyqXxeM0Sqg
VlyKLWwAuMf2Ec7t7ujqaNmVgv6bpwHEbR6njIi7lC7j4w6D2YQ8vacgvS3MB/K0
SX54HNVBVuXhAixPtYJ6tOBGm7QFAKaXju0PJ+AljnMEsHRekOs2u42OHBXEWDE8
VHw7/lTXWsJkBcQM+g/svyqV4xKHDAixPms2SUwJyKjvEgV+CQok4F/T
-----END THIS IS NOT A REAL PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIC9TCCAl6gAwIBAgIJANL8E4epRNznMA0GCSqGSIb3DQEBBQUAMFsxGDAWBgNV
BAoTD1N1cGVyZmlzaCwgSW5jLjELMAkGA1UEBxMCU0YxCzAJBgNVBAgTAkNBMQsw
CQYDVQQGEwJVUzEYMBYGA1UEAxMPU3VwZXJmaXNoLCBJbmMuMB4XDTE0MDUxMjE2
MjUyNloXDTM0MDUwNzE2MjUyNlowWzEYMBYGA1UEChMPU3VwZXJmaXNoLCBJbmMu
MQswCQYDVQQHEwJTRjELMAkGA1UECBMCQ0ExCzAJBgNVBAYTAlVTMRgwFgYDVQQD
Ew9TdXBlcmZpc2gsIEluYy4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOjz
Shh2Xxk/sc9Y6X9DBwmVgDXFD/5xMSeBmRImIKXfj2r8QlU57gk4idngNsSsAYJb
1Tnm+Y8HiN/+7vahFM6pdEXY/fAXVyqC4XouEpNarIrXFWPRt5tVgA9YvBxJ7SBi
3bZMpTrrHD2g/3pxptMQeDOuS8Ic/ZJKocPnQaQtAgMBAAGjgcAwgb0wDAYDVR0T
BAUwAwEB/zAdBgNVHQ4EFgQU+5izU38URC7o7tUJml4OVoaoNYgwgY0GA1UdIwSB
hTCBgoAU+5izU38URC7o7tUJml4OVoaoNYihX6RdMFsxGDAWBgNVBAoTD1N1cGVy
ZmlzaCwgSW5jLjELMAkGA1UEBxMCU0YxCzAJBgNVBAgTAkNBMQswCQYDVQQGEwJV
UzEYMBYGA1UEAxMPU3VwZXJmaXNoLCBJbmMuggkA0vwTh6lE3OcwDQYJKoZIhvcN
AQEFBQADgYEApHyg7ApKx3DEcWjzOyLi3JyN0JL+c35yK1VEmxu0Qusfr76645Oj
1IsYwpTws6a9ZTRMzST4GQvFFQra81eLqYbPbMPuhC+FCxkUF5i0DNSWi+kczJXJ
TtCqSwGl9t9JEoFqvtW+znZ9TqyLiOMw7TGEUI+88VAqW0qmXnwPcfo=
-----END CERTIFICATE-----
`;
const String SIMPLE_MULTI =
`-----BEGIN CERTIFICATE-----
MIIC9TCCAl6gAwIBAgIJANL8E4epRNznMA0GCSqGSIb3DQEBBQUAMFsxGDAWBgNV
BAoTD1N1cGVyZmlzaCwgSW5jLjELMAkGA1UEBxMCU0YxCzAJBgNVBAgTAkNBMQsw
CQYDVQQGEwJVUzEYMBYGA1UEAxMPU3VwZXJmaXNoLCBJbmMuMB4XDTE0MDUxMjE2
MjUyNloXDTM0MDUwNzE2MjUyNlowWzEYMBYGA1UEChMPU3VwZXJmaXNoLCBJbmMu
MQswCQYDVQQHEwJTRjELMAkGA1UECBMCQ0ExCzAJBgNVBAYTAlVTMRgwFgYDVQQD
Ew9TdXBlcmZpc2gsIEluYy4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOjz
Shh2Xxk/sc9Y6X9DBwmVgDXFD/5xMSeBmRImIKXfj2r8QlU57gk4idngNsSsAYJb
1Tnm+Y8HiN/+7vahFM6pdEXY/fAXVyqC4XouEpNarIrXFWPRt5tVgA9YvBxJ7SBi
3bZMpTrrHD2g/3pxptMQeDOuS8Ic/ZJKocPnQaQtAgMBAAGjgcAwgb0wDAYDVR0T
BAUwAwEB/zAdBgNVHQ4EFgQU+5izU38URC7o7tUJml4OVoaoNYgwgY0GA1UdIwSB
hTCBgoAU+5izU38URC7o7tUJml4OVoaoNYihX6RdMFsxGDAWBgNVBAoTD1N1cGVy
ZmlzaCwgSW5jLjELMAkGA1UEBxMCU0YxCzAJBgNVBAgTAkNBMQswCQYDVQQGEwJV
UzEYMBYGA1UEAxMPU3VwZXJmaXNoLCBJbmMuggkA0vwTh6lE3OcwDQYJKoZIhvcN
AQEFBQADgYEApHyg7ApKx3DEcWjzOyLi3JyN0JL+c35yK1VEmxu0Qusfr76645Oj
1IsYwpTws6a9ZTRMzST4GQvFFQra81eLqYbPbMPuhC+FCxkUF5i0DNSWi+kczJXJ
TtCqSwGl9t9JEoFqvtW+znZ9TqyLiOMw7TGEUI+88VAqW0qmXnwPcfo=
-----END CERTIFICATE-----
-----BEGIN THING-----
MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIDHHhyAEZQoICAggA
MBQGCCqGSIb3DQMHBAiHEg+MCYQ30ASCAoDEvGvFRHvtWOb5Rc0f3lbVKqeUvWSz
xQn+rZELHnwb6baolmbFcsi6XkacVzL/EF7Ll4de/CSQ6pZZCCvfDzov0mPOuGve
SAe7hbAcol7+JWVfzbnVTblPf0i7mwSvK61cKq7YfcKJ2os/uJGpeX9zraywWyFx
f+EdTr348dOez8uHkURyY1cvSHsIdITALkChOonAYT68SVighTeB6xOCwfmsHx+X
3Qbhom2YCIxfJiaAoz2/LndCpDaEfOrVrxXFOKXrIbmeDEyjDQj16AVni9uuaj7l
NiO3zrrqxsfdVINPaAYRKQnS102jXqkH01z72c/MpMMC6dwZswF5V3R7RSXngyBn
1GLxVFHKR753Gt0IDag13Bd8Jt890/v0tE0Kx66jCkRGn+VCq6+bsnh7VpTH/cG5
dlFnv56lv2leknu5ghdJHX8YQ6HjnioaaheLA+ORAxqAlD8Itt1/pRBOOMSkutdz
d1px9dB2ZBpSoRAOcBwU5aFaw9uu+tXyzrPM3tZomu8ryQYMNlmVgPNDJOz6jPJi
jaZHWTS7U6j370oH/B0KTUG/ybrJGFnOmPP4h2u/ugG75EkfotURsvbrWuetQhOi
TCH+9nbIcT3pxnTXqI2IRHZXMturQ+6fqlJF3bb9bWarMBuC3KgprqyqXxeM0Sqg
VlyKLWwAuMf2Ec7t7ujqaNmVgv6bpwHEbR6njIi7lC7j4w6D2YQ8vacgvS3MB/K0
SX54HNVBVuXhAixPtYJ6tOBGm7QFAKaXju0PJ+AljnMEsHRekOs2u42OHBXEWDE8
VHw7/lTXWsJkBcQM+g/svyqV4xKHDAixPms2SUwJyKjvEgV+CQok4F/T
-----END THING-----
`;
const String SINGLE_WITH_HEADERS =
`-----BEGIN CERTIFICATE-----
My-Header: Example
This-is-a-longer-header: with some extra content that will bleed
over to the next line.
Last: Header is single-line.
MIIC9TCCAl6gAwIBAgIJANL8E4epRNznMA0GCSqGSIb3DQEBBQUAMFsxGDAWBgNV
BAoTD1N1cGVyZmlzaCwgSW5jLjELMAkGA1UEBxMCU0YxCzAJBgNVBAgTAkNBMQsw
CQYDVQQGEwJVUzEYMBYGA1UEAxMPU3VwZXJmaXNoLCBJbmMuMB4XDTE0MDUxMjE2
MjUyNloXDTM0MDUwNzE2MjUyNlowWzEYMBYGA1UEChMPU3VwZXJmaXNoLCBJbmMu
MQswCQYDVQQHEwJTRjELMAkGA1UECBMCQ0ExCzAJBgNVBAYTAlVTMRgwFgYDVQQD
Ew9TdXBlcmZpc2gsIEluYy4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOjz
Shh2Xxk/sc9Y6X9DBwmVgDXFD/5xMSeBmRImIKXfj2r8QlU57gk4idngNsSsAYJb
1Tnm+Y8HiN/+7vahFM6pdEXY/fAXVyqC4XouEpNarIrXFWPRt5tVgA9YvBxJ7SBi
3bZMpTrrHD2g/3pxptMQeDOuS8Ic/ZJKocPnQaQtAgMBAAGjgcAwgb0wDAYDVR0T
BAUwAwEB/zAdBgNVHQ4EFgQU+5izU38URC7o7tUJml4OVoaoNYgwgY0GA1UdIwSB
hTCBgoAU+5izU38URC7o7tUJml4OVoaoNYihX6RdMFsxGDAWBgNVBAoTD1N1cGVy
ZmlzaCwgSW5jLjELMAkGA1UEBxMCU0YxCzAJBgNVBAgTAkNBMQswCQYDVQQGEwJV
UzEYMBYGA1UEAxMPU3VwZXJmaXNoLCBJbmMuggkA0vwTh6lE3OcwDQYJKoZIhvcN
AQEFBQADgYEApHyg7ApKx3DEcWjzOyLi3JyN0JL+c35yK1VEmxu0Qusfr76645Oj
1IsYwpTws6a9ZTRMzST4GQvFFQra81eLqYbPbMPuhC+FCxkUF5i0DNSWi+kczJXJ
TtCqSwGl9t9JEoFqvtW+znZ9TqyLiOMw7TGEUI+88VAqW0qmXnwPcfo=
-----END CERTIFICATE-----
`;
const char[] SINGLE_DATA =
x'308202f53082025ea003020102020900d2fc1387a944dce7300d06092a864886f70d0101050500305b31183016060355040a130f5375706572666973682c2049'
x'6e632e310b3009060355040713025346310b3009060355040813024341310b3009060355040613025553311830160603550403130f5375706572666973682c20'
x'496e632e301e170d3134303531323136323532365a170d3334303530373136323532365a305b31183016060355040a130f5375706572666973682c20496e632e'
x'310b3009060355040713025346310b3009060355040813024341310b3009060355040613025553311830160603550403130f5375706572666973682c20496e63'
x'2e30819f300d06092a864886f70d010101050003818d0030818902818100e8f34a18765f193fb1cf58e97f430709958035c50ffe7131278199122620a5df8f6a'
x'fc425539ee093889d9e036c4ac01825bd539e6f98f0788dffeeef6a114cea97445d8fdf017572a82e17a2e12935aac8ad71563d1b79b55800f58bc1c49ed2062'
x'ddb64ca53aeb1c3da0ff7a71a6d3107833ae4bc21cfd924aa1c3e741a42d0203010001a381c03081bd300c0603551d13040530030101ff301d0603551d0e0416'
x'0414fb98b3537f14442ee8eed5099a5e0e5686a8358830818d0603551d230481853081828014fb98b3537f14442ee8eed5099a5e0e5686a83588a15fa45d305b'
x'31183016060355040a130f5375706572666973682c20496e632e310b3009060355040713025346310b3009060355040813024341310b30090603550406130255'
x'53311830160603550403130f5375706572666973682c20496e632e820900d2fc1387a944dce7300d06092a864886f70d010105050003818100a47ca0ec0a4ac7'
x'70c47168f33b22e2dc9c8dd092fe737e722b55449b1bb442eb1fafbebae393a3d48b18c294f0b3a6bd65344ccd24f8190bc5150adaf3578ba986cf6cc3ee842f'
x'850b19141798b40cd4968be91ccc95c94ed0aa4b01a5f6df4912816abed5bece767d4eac8b88e330ed3184508fbcf1502a5b4aa65e7c0f71fa';
const char[] MULTI_DATA =
x'308202c6304006092a864886f70d01050d3033301b06092a864886f70d01050c300e04080c71e1c80119428202020800301406082a864886f70d030704088712'
x'0f8c098437d004820280c4bc6bc5447bed58e6f945cd1fde56d52aa794bd64b3c509fead910b1e7c1be9b6a89666c572c8ba5e469c5732ff105ecb97875efc24'
x'90ea9659082bdf0f3a2fd263ceb86bde4807bb85b01ca25efe25655fcdb9d54db94f7f48bb9b04af2bad5c2aaed87dc289da8b3fb891a9797f73adacb05b2171'
x'7fe11d4ebdf8f1d39ecfcb8791447263572f487b087484c02e40a13a89c0613ebc4958a0853781eb1382c1f9ac1f1f97dd06e1a26d98088c5f262680a33dbf2e'
x'7742a436847cead5af15c538a5eb21b99e0c4ca30d08f5e805678bdbae6a3ee53623b7cebaeac6c7dd54834f6806112909d2d74da35ea907d35cfbd9cfcca4c3'
x'02e9dc19b3017957747b4525e7832067d462f15451ca47be771add080da835dc177c26df3dd3fbf4b44d0ac7aea30a44469fe542abaf9bb2787b5694c7fdc1b9'
x'765167bf9ea5bf695e927bb98217491d7f1843a1e39e2a1a6a178b03e391031a80943f08b6dd7fa5104e38c4a4bad773775a71f5d076641a52a1100e701c14e5'
x'a15ac3dbaefad5f2ceb3ccded6689aef2bc9060c36599580f34324ecfa8cf2628da6475934bb53a8f7ef4a07fc1d0a4d41bfc9bac91859ce98f3f8876bbfba01'
x'bbe4491fa2d511b2f6eb5ae7ad4213a24c21fef676c8713de9c674d7a88d8844765732dbab43ee9faa5245ddb6fd6d66ab301b82dca829aeacaa5f178cd12aa0'
x'565c8a2d6c00b8c7f611ceedeee8ea68d99582fe9ba701c46d1ea78c88bb942ee3e30e83d9843cbda720bd2dcc07f2b4497e781cd54156e5e1022c4fb5827ab4'
x'e0469bb40500a6978eed0f27e0258e7304b0745e90eb36bb8d8e1c15c458313c547c3bfe54d75ac26405c40cfa0fecbf2a95e312870c08b13e6b36494c09c8a8'
x'ef12057e090a24e05fd3';
const Pair{ String, fault }[] DECODE_ERRORS = {
{ "heh", pem::INVALID_FORMAT },
{ "---BEGIN CERT\nsomething\nsomething\nsomething", pem::INVALID_PRE_EB },
{ "\n\n-----BEGIN SAMPLE-----\ntest: test\ntest1: test1\n\nQQ==\n", pem::MISSING_POST_EB },
{ "\n\n-----BEGIN SAMPLE-----\ntest: test\ntest1: test1\n\nQQ==\n-----BEGIN TAG-----", pem::INVALID_POST_EB },
{ "-----BEGIN SAMPLE-----\ntest: test\ntest1: test1\n\nQQ==\n-----END TAG-----", pem::MISMATCHED_TAG },
{ "-----BEGIN -----\ntest: test\ntest1: test1\n\nQQ==\n-----END SAMPLE-----", pem::MISSING_TAG },
{ "-----BEGIN SAMPLE-----\n a key STARTING with spaces!: test\ntest1: test1\n\nQQ==\n-----END SAMPLE-----", pem::INVALID_HEADER },
{ "-----BEGIN SAMPLE-----\nthis-line-has-no-colon-character\ntest1: test1\n\nQQ==\n-----END SAMPLE-----", pem::INVALID_BODY },
{ "-----BEGIN SAMPLE-----\nthis-line-has-a-colon-character:butnospace\ntest1: test1\n\nQQ==\n-----END SAMPLE-----", pem::INVALID_BODY },
{ "-----BEGIN SAMPLE-----\na key with spaces!: test\ntest1: test1\n\nQQ==\n-----END SAMPLE-----", pem::INVALID_HEADER_KEY },
{ "-----BEGIN SAMPLE-----\n: nokey\ntest1: test1\n\nQQ==\n-----END SAMPLE-----", pem::INVALID_HEADER }, // because ':' isn't in the valid charset, which is checked first
{ "-----BEGIN SAMPLE-----\na key with spaces!: \ntest1: test1\n\nQQ==\n-----END SAMPLE-----", pem::INVALID_BODY }, // because the whitespace is trimmed
{ "-----BEGIN SAMPLE-----\ntest1: test1\nQQ==\n-----END SAMPLE-----", pem::INVALID_FORMAT }, // missing a blank line between headers/body
{ "-----BEGIN SAMPLE-----\ntest1: test1\n\n-----END SAMPLE-----", pem::MISSING_BODY },
{ "-----BEGIN SAMPLE-----\ntest1: test1\n\nthis is not base64 data\n-----END SAMPLE-----", pem::INVALID_BODY },
};