// tests for the pure half of the TLS identity: src/tls_pem.h // // This suite includes tls_pem.h and nothing that drags in a crypto library. If it ever // includes src/tls.h the host build breaks -- which is the point: that is the rule that // keeps the identical code running here and on the board (AGENTS.md "Editing rules"). // // What is under test is the part a handshake cannot diagnose. A certificate that fails // to parse says "malformed"; it does not say "your PEM had 63 characters on line two", // "your fingerprint was truncated to 63 hex digits" or "your SAN wrote the IP address as // text". Each check below is named after the failure it would catch. #include #include #include #include "sha1.h" // base64Encode: a second, independent base64 to cross-check against #include "test.h" #include "tls_pem.h" using namespace kr; using namespace kr::tls; // the module under test; kr alone does not reach into it namespace { // A pattern that cannot appear in any output, so "writes nothing" is an observation // rather than an assumption. constexpr char kSentinel = char(0x5A); void prime(char *buf, size_t cap) { memset(buf, kSentinel, cap); buf[cap - 1] = '\0'; } bool untouched(const char *buf, size_t len) { for (size_t i = 0; i < len; i++) { if (buf[i] != kSentinel) return false; } return true; } std::string hexOf(const uint8_t *p, size_t len) { static const char kHex[] = "0123456789abcdef"; std::string s; for (size_t i = 0; i < len; i++) { s += kHex[p[i] >> 4]; s += kHex[p[i] & 0xF]; } return s; } // The lines strictly between the BEGIN line and the END line. An armour block with no // body has none, and a helper that reported the END marker as a body line would quietly // turn the empty-input case into a passing test of nothing. std::vector bodyLines(const char *pem) { std::vector all; const std::string s(pem); size_t start = 0; while (true) { const size_t q = s.find('\n', start); if (q == std::string::npos) { all.push_back(s.substr(start)); break; } all.push_back(s.substr(start, q - start)); start = q + 1; } if (!all.empty() && all.back().empty()) all.pop_back(); // the final terminator if (all.size() <= 2) return std::vector(); return std::vector(all.begin() + 1, all.end() - 1); } // A DER-shaped blob: every byte value walks somewhere and it is not all zeros, so a // base64 bug that only shows up on particular bit patterns cannot hide here. void fillBlob(uint8_t *buf, size_t len) { for (size_t i = 0; i < len; i++) { buf[i] = static_cast((i * 37 + 11) & 0xFF); } } // The sizes that straddle the 64-character armour line: 48 DER bytes are exactly 64 // base64 characters, so 45/46/47 land on the last partial quantum of a full line and // 49/50/51 spill onto the next one. 704 is the worst case the generator can hand over. const size_t kLineBoundarySizes[] = {0, 1, 2, 3, 4, 45, 46, 47, 48, 49, 50, 51, 94, 95, 96, 97, 142, 143, 144, 145, 146, 299, 300, 301, 400, 704}; std::string toCrlf(const char *src) { std::string out; for (const char *p = src; *p; p++) { if (*p == '\n') out += "\r\n"; out += *p; } return out; } } // namespace int main() { char pem[2048]; uint8_t der[1024]; uint8_t back[1024]; CASE("wrapPem: one known vector, byte for byte"); { // RFC 4648's own example, "foobar" -> "Zm9vYmFy". If the alphabet table is off by // one character anywhere, this is the check that says so. const uint8_t in[] = {'f', 'o', 'o', 'b', 'a', 'r'}; prime(pem, sizeof(pem)); const size_t n = wrapPem("CERTIFICATE", in, sizeof(in), pem, sizeof(pem)); CHECK_STR(pem, "-----BEGIN CERTIFICATE-----\nZm9vYmFy\n-----END CERTIFICATE-----\n"); CHECK_EQ(n, strlen(pem)); CHECK_EQ(n, 63); // 28 head + 9 body (8 characters and their newline) + 26 tail // An empty input is the two armour lines and nothing between them. prime(pem, sizeof(pem)); const size_t z = wrapPem("CERTIFICATE", nullptr, 0, pem, sizeof(pem)); CHECK_STR(pem, "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n"); CHECK_EQ(z, strlen(pem)); // The label a private key is armoured under is a wire format, not decoration. prime(pem, sizeof(pem)); wrapPem(kPemKeyLabel, in, sizeof(in), pem, sizeof(pem)); CHECK_STR(pem, "-----BEGIN EC PRIVATE KEY-----\nZm9vYmFy\n-----END EC PRIVATE KEY-----\n"); } CASE("wrapPem: 64 characters a line, boundary by boundary"); { for (size_t s = 0; s < sizeof(kLineBoundarySizes) / sizeof(kLineBoundarySizes[0]); s++) { const size_t len = kLineBoundarySizes[s]; fillBlob(der, len); prime(pem, sizeof(pem)); const size_t n = wrapPem("CERTIFICATE", der, len, pem, sizeof(pem)); CHECK(n > 0); CHECK_EQ(n, strlen(pem)); // no NUL in the middle, nothing after the end // Every body line is exactly 64 characters except possibly the last, and a body // exists exactly when there are bytes to carry. const std::vector lines = bodyLines(pem); const size_t b64Len = ((len + 2) / 3) * 4; const size_t wantLines = b64Len == 0 ? 0 : (b64Len + 63) / 64; CHECK_EQ(lines.size(), wantLines); for (size_t i = 0; i < lines.size(); i++) { const bool lastLine = i + 1 == lines.size(); CHECK(lines[i].size() <= 64); CHECK(lines[i].size() >= 1); if (!lastLine) CHECK_EQ(lines[i].size(), 64); } // Cross-checked against sha1.h's base64, which the WebSocket handshake already // trusts: two implementations written for different purposes that agree at every // length cannot both be wrong about the alphabet. char ref[1400]; const size_t refLen = base64Encode(der, len, ref, sizeof(ref)); CHECK_EQ(refLen, b64Len); std::string stripped; for (const std::string &l : lines) stripped += l; CHECK(stripped == std::string(ref, refLen)); // And back again, unchanged. size_t got = 0; CHECK(pemFind("CERTIFICATE", pem, back, sizeof(back), &got)); CHECK_EQ(got, len); CHECK(len == 0 || memcmp(der, back, len) == 0); } } CASE("wrapPem: a padded final group on a line of its own still gets its newline"); { // 1, 2 and everything 1 or 2 mod 48: the base64 body ends with a padded group that // starts a fresh line, because 48 DER bytes fill exactly 64 characters. A writer that // counts only whole groups leaves that line unterminated, and the END marker glues // itself onto the body -- pemFind still decodes it, so the round trip passes and the // block is nonetheless not what any other tool on earth writes. This was a real bug // here; the independent-base64 cross-check above is what caught it. const size_t paddedGroupSizes[] = {1, 2, 49, 50, 97, 98, 145, 146}; for (size_t s = 0; s < sizeof(paddedGroupSizes) / sizeof(paddedGroupSizes[0]); s++) { const size_t len = paddedGroupSizes[s]; fillBlob(der, len); prime(pem, sizeof(pem)); const size_t n = wrapPem("CERTIFICATE", der, len, pem, sizeof(pem)); CHECK(n > 0); const char *endMark = strstr(pem, "-----END"); CHECK(endMark != nullptr); CHECK(endMark > pem + 1); CHECK_EQ(endMark[-1], '\n'); // the body's last line is terminated CHECK_EQ(endMark[-2], '='); // ...and it really was the padded group // The body is exactly ceil(b64/64) lines, the last one 4 characters long. const std::vector lines = bodyLines(pem); const size_t b64Len = ((len + 2) / 3) * 4; CHECK_EQ(lines.size(), (b64Len + 63) / 64); CHECK_EQ(lines.back().size(), 4); CHECK_EQ(lines.back()[3], '='); } } CASE("wrapPem: refuses a buffer one byte short, and writes nothing at all"); { fillBlob(der, 100); prime(pem, sizeof(pem)); const size_t exact = wrapPem("CERTIFICATE", der, 100, pem, sizeof(pem)); CHECK_EQ(exact, strlen(pem)); // A buffer the size of the text: the NUL is the byte that does not fit, and a // caller who counted characters and not the terminator is the bug this catches. prime(pem, sizeof(pem)); CHECK_EQ(wrapPem("CERTIFICATE", der, 100, pem, exact), 0); CHECK(untouched(pem, exact)); CHECK_EQ(wrapPem("CERTIFICATE", der, 100, pem, exact + 1), exact); // Same for the empty-body case, which has a size of its own. prime(pem, sizeof(pem)); const size_t emptyNeed = wrapPem("CERTIFICATE", nullptr, 0, pem, sizeof(pem)); prime(pem, sizeof(pem)); CHECK_EQ(wrapPem("CERTIFICATE", nullptr, 0, pem, emptyNeed), 0); CHECK(untouched(pem, emptyNeed)); CHECK_EQ(wrapPem("CERTIFICATE", nullptr, 0, pem, emptyNeed + 1), emptyNeed); } CASE("wrapPem: refuses input it cannot name"); { fillBlob(der, 8); prime(pem, sizeof(pem)); CHECK_EQ(wrapPem(nullptr, der, 8, pem, sizeof(pem)), 0); CHECK_EQ(wrapPem("", der, 8, pem, sizeof(pem)), 0); CHECK_EQ(wrapPem("CERTIFICATE", nullptr, 8, pem, sizeof(pem)), 0); // bytes, no bytes CHECK_EQ(wrapPem("CERTIFICATE", der, 8, nullptr, sizeof(pem)), 0); // A label over the bound is a caller bug; exactly at the bound still works, so the // refusal is about the bound and not about caution. char longLabel[kPemLabelMax + 2]; memset(longLabel, 'X', sizeof(longLabel) - 1); longLabel[sizeof(longLabel) - 1] = '\0'; CHECK_EQ(wrapPem(longLabel, der, 8, pem, sizeof(pem)), 0); longLabel[kPemLabelMax - 1] = '\0'; CHECK(wrapPem(longLabel, der, 8, pem, sizeof(pem)) > 0); } CASE("pemFind: reads back what a real tool wrote"); { fillBlob(der, 47); prime(pem, sizeof(pem)); CHECK(wrapPem("CERTIFICATE", der, 47, pem, sizeof(pem)) > 0); size_t got = 0; // LF, as wrapPem writes it. CHECK(pemFind("CERTIFICATE", pem, back, sizeof(back), &got)); CHECK_EQ(got, 47); CHECK(memcmp(der, back, 47) == 0); // CRLF: whatever passed through a Windows editor, and the shape a copy-pasted // certificate usually arrives in. const std::string crlf = toCrlf(pem); CHECK(pemFind("CERTIFICATE", crlf.c_str(), back, sizeof(back), &got)); CHECK_EQ(got, 47); CHECK(memcmp(der, back, 47) == 0); // No trailing newline after the END marker. std::string trimmed(pem, strlen(pem) - 1); CHECK(pemFind("CERTIFICATE", trimmed.c_str(), back, sizeof(back), &got)); CHECK_EQ(got, 47); // Spaces and tabs inside the body: "AAAA" is three zero bytes, "BBBB" is 04 10 41. const std::string spaced = "-----BEGIN CERTIFICATE-----\nAAAA \t\nBBBB\n-----END CERTIFICATE-----\n"; CHECK(pemFind("CERTIFICATE", spaced.c_str(), back, sizeof(back), &got)); CHECK_EQ(got, 6); CHECK(memcmp(back, "\x00\x00\x00\x04\x10\x41", 6) == 0); // An empty body is zero bytes -- not a failure, and not garbage. CHECK(pemFind("CERTIFICATE", "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n", back, sizeof(back), &got)); CHECK_EQ(got, 0); // Two blocks in one haystack: the first of that label wins. fillBlob(der, 3); char second[256]; wrapPem("CERTIFICATE", der, 3, second, sizeof(second)); const std::string both = std::string(pem) + second; CHECK(pemFind("CERTIFICATE", both.c_str(), back, sizeof(back), &got)); CHECK_EQ(got, 47); // ...and a block stored under another label is not this one. CHECK(!pemFind("EC PRIVATE KEY", both.c_str(), back, sizeof(back), &got)); // The label matches exactly: a request for CERTIFICATE must not pick up a // CERTIFICATE REQUEST, or a key would be found where a certificate belongs. CHECK(!pemFind("CERTIFICATE", "-----BEGIN CERTIFICATE REQUEST-----\nAAAA\n-----END CERTIFICATE REQUEST-----\n", back, sizeof(back), &got)); } CASE("pemFind: rejects a malformed block instead of decoding almost the right thing"); { size_t got = 99; const char *good = "-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----\n"; CHECK(!pemFind("CERTIFICATE", "", back, sizeof(back), &got)); CHECK(!pemFind("CERTIFICATE", "not a pem at all", back, sizeof(back), &got)); // BEGIN without END: a record torn by power loss in the middle of the write. CHECK(!pemFind("CERTIFICATE", "-----BEGIN CERTIFICATE-----\nAAAA\n", back, sizeof(back), &got)); CHECK(!pemFind("CERTIFICATE", "-----END CERTIFICATE-----\n", back, sizeof(back), &got)); // One character torn out of the body, so the length is no longer a multiple of four. CHECK(!pemFind("CERTIFICATE", "-----BEGIN CERTIFICATE-----\nAAA\n-----END CERTIFICATE-----\n", back, sizeof(back), &got)); // A character from the armour inside the body -- what a block pasted inside itself // looks like, and what a buffer that held two writes looks like after a realloc. CHECK(!pemFind("CERTIFICATE", "-----BEGIN CERTIFICATE-----\nAA-A\n-----END CERTIFICATE-----\n", back, sizeof(back), &got)); CHECK(!pemFind("CERTIFICATE", "-----BEGIN CERTIFICATE-----\nAA<>\n-----END CERTIFICATE-----\n", back, sizeof(back), &got)); // Padding is only legal in the last two positions of the last group. CHECK(!pemFind("CERTIFICATE", "-----BEGIN CERTIFICATE-----\nA=AA\n-----END CERTIFICATE-----\n", back, sizeof(back), &got)); CHECK(!pemFind("CERTIFICATE", "-----BEGIN CERTIFICATE-----\n=AAA\n-----END CERTIFICATE-----\n", back, sizeof(back), &got)); CHECK(!pemFind("CERTIFICATE", "-----BEGIN CERTIFICATE-----\nAAAA=A\n-----END CERTIFICATE-----\n", back, sizeof(back), &got)); CHECK(!pemFind("CERTIFICATE", "-----BEGIN CERTIFICATE-----\nAAAA====AAAA\n" "-----END CERTIFICATE-----\n", back, sizeof(back), &got)); // Legal padding, for contrast: "QQ==" is one byte, "QUI=" is two. // "QQ==" is "A" and "QUI=" is "AB" -- RFC 4648's own padding examples. CHECK(pemFind("CERTIFICATE", "-----BEGIN CERTIFICATE-----\nQQ==\n-----END CERTIFICATE-----\n", back, sizeof(back), &got)); CHECK_EQ(got, 1); CHECK_EQ(back[0], 'A'); CHECK(pemFind("CERTIFICATE", "-----BEGIN CERTIFICATE-----\nQUI=\n-----END CERTIFICATE-----\n", back, sizeof(back), &got)); CHECK_EQ(got, 2); CHECK_EQ(back[0], 'A'); CHECK_EQ(back[1], 'B'); // Does not fit: a refusal with nothing written, never a truncated decode. prime(reinterpret_cast(back), 8); CHECK(!pemFind("CERTIFICATE", good, back, 2, &got)); CHECK(untouched(reinterpret_cast(back), 7)); CHECK(pemFind("CERTIFICATE", good, back, 3, &got)); CHECK_EQ(got, 3); // Degenerate arguments. CHECK(!pemFind(nullptr, good, back, sizeof(back), &got)); CHECK(!pemFind("", good, back, sizeof(back), &got)); CHECK(!pemFind("CERTIFICATE", nullptr, back, sizeof(back), &got)); CHECK(!pemFind("CERTIFICATE", good, nullptr, sizeof(back), &got)); char longLabel[kPemLabelMax + 2]; memset(longLabel, 'X', sizeof(longLabel) - 1); longLabel[sizeof(longLabel) - 1] = '\0'; CHECK(!pemFind(longLabel, good, back, sizeof(back), &got)); // A caller that does not want the length still gets the right bytes. CHECK(pemFind("CERTIFICATE", good, back, sizeof(back))); CHECK_EQ(back[0], 0); } CASE("fingerprintHex: the published SHA-256 of \"abc\""); { // FIPS 180-4. The digest arrives as bytes -- the board hashes with the platform // library -- so what is under test is the rendering the operator reads, which is // where a fingerprint goes wrong in practice: one nibble dropped, and the pinned // value never matches while every handshake succeeds. const uint8_t abc[32] = { 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad}; char hex[128]; prime(hex, sizeof(hex)); CHECK(fingerprintHex(abc, sizeof(abc), hex, 65)); CHECK_STR(hex, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); CHECK_EQ(strlen(hex), 64); CHECK_EQ(kFingerprintCap, 65); CHECK_EQ(fingerprintHexCap(32), 65); // Exactly one byte short: no output, not a 63-character fingerprint. prime(hex, sizeof(hex)); CHECK(!fingerprintHex(abc, sizeof(abc), hex, 64)); CHECK(untouched(hex, 64)); // Lowercase always: the operator copies this out of `openssl x509 -fingerprint`. const uint8_t high[1] = {0xFF}; CHECK(fingerprintHex(high, 1, hex, sizeof(hex))); CHECK_STR(hex, "ff"); // A zero-length digest is an empty string, and it still needs room for the NUL. CHECK(fingerprintHex(high, 0, hex, 1)); CHECK_STR(hex, ""); prime(hex, sizeof(hex)); CHECK(!fingerprintHex(high, 0, hex, 0)); CHECK(!fingerprintHex(nullptr, 32, hex, sizeof(hex))); CHECK(!fingerprintHex(abc, 32, nullptr, sizeof(hex))); } CASE("the names in the certificate are the names in config.h"); { // kCertCommonName is the string the operator types into a browser. If it ever stops // being kHostname + ".local", the certificate and the mDNS name diverge and every // connection starts with a warning -- so this pins the config value, not just its // formatting. const std::string derived = std::string(kHostname) + ".local"; CHECK_STR(kCertCommonName, derived.c_str()); char subject[64]; prime(subject, sizeof(subject)); const size_t n = subjectLine(subject, sizeof(subject)); CHECK_STR(subject, "CN=keyrelay.local"); CHECK_EQ(n, strlen(subject)); CHECK_EQ(n, 17); // No comma: the certificate writer parses this as a comma-separated name list. CHECK(strchr(subject, ',') == nullptr); prime(subject, sizeof(subject)); CHECK_EQ(subjectLine(subject, n), 0); CHECK(untouched(subject, n)); CHECK_EQ(subjectLine(nullptr, sizeof(subject)), 0); char san[128]; prime(san, sizeof(san)); const size_t s = sanList(san, sizeof(san)); CHECK_STR(san, "DNS:keyrelay.local, DNS:keyrelay, IP:192.168.4.1"); CHECK_EQ(s, strlen(san)); // Every name the README tells the operator to expect, taken from config.h rather // than restated here. CHECK(strstr(san, kCertCommonName) != nullptr); CHECK(strstr(san, kHostname) != nullptr); CHECK(strstr(san, kApGw) != nullptr); // The subject and the list agree on the common name, because a human is shown one // and the browser checks the other. CHECK(strstr(san, kCertCommonName) == san + strlen("DNS:")); prime(san, sizeof(san)); CHECK_EQ(sanList(san, s), 0); CHECK(untouched(san, s)); CHECK_EQ(sanList(san, 0), 0); CHECK_EQ(sanList(nullptr, sizeof(san)), 0); size_t count = 0; const SanEntry *entries = sanEntries(&count); CHECK_EQ(count, 3); CHECK(entries[0].type == SanType::Dns); CHECK_STR(entries[0].value, kCertCommonName); CHECK(entries[1].type == SanType::Dns); CHECK_STR(entries[1].value, kHostname); CHECK(entries[2].type == SanType::Ip); CHECK_STR(entries[2].value, kApGw); CHECK(sanEntries(nullptr) == entries); // one table, whoever asks CHECK_STR(sanTypeName(SanType::Dns), "DNS:"); CHECK_STR(sanTypeName(SanType::Ip), "IP:"); } CASE("kPemMax holds the largest block the generator can hand it"); { // config.h sizes kPemMax from a measured certificate of ~430 DER bytes armoured to // ~603; the DER scratch in tls.cpp is 704 bytes, so 704 is the worst case anything // can ask to be armoured. wrapPem refuses rather than truncates, so if kPemMax ever // falls below that, the board fails to get an identity at boot -- loudly, which is // the point, but only if a test says so first. fillBlob(der, 704); const size_t n = wrapPem(kPemCertLabel, der, 704, pem, kPemMax); CHECK(n > 0); CHECK(n == strlen(pem)); CHECK(n < kPemMax); CHECK_EQ(kPemMax, 1024); // the number in config.h, pinned so the headroom is a fact } CASE("sanExtensionDer: the GeneralNames a verifier actually parses"); { uint8_t out[64]; const size_t n = sanExtensionDer(out, sizeof(out)); const std::string got = hexOf(out, n); // SEQUENCE(32) { [2] "keyrelay.local", [2] "keyrelay", [7] C0 A8 04 01 } CHECK_EQ(n, 34); CHECK_STR(got.c_str(), "3020820e6b657972656c61792e6c6f63616c82086b657972656c61798704c0a80401"); // Cross-checked, not asserted: a certificate signed with // openssl req -x509 -key -subj /CN=keyrelay.local // -sha256 -addext "subjectAltName=DNS:keyrelay.local,DNS:keyrelay,IP:192.168.4.1" // contains these exact 34 bytes, and `openssl asn1parse -inform DER` reads them back // as SEQUENCE { cont[2] 14, cont[2] 8, cont[7] 4 }. This module's encoder and // OpenSSL's agree byte for byte, which is what turns "the browser will accept it" // into a measured claim instead of a hope. CHECK_EQ(sanExtensionDer(out, n - 1), 0); CHECK_EQ(sanExtensionDer(out, 1), 0); CHECK_EQ(sanExtensionDer(nullptr, sizeof(out)), 0); CHECK_EQ(sanExtensionDer(out, n), n); // an exact fit is a fit } CASE("kSanOidTlv: the extension's label, as DER content bytes and not as dotted text"); { // RFC 5280 4.2.1.6: subjectAltName is 2.5.29.17, whose OBJECT IDENTIFIER is // 06 03 55 1D 11 -- first arc 40*2 + 5 = 0x55, then 29 and 17. These five bytes are // what makes the 34 above a SAN; without them the certificate carries a name list no // client will read, and nothing on the board says so. CHECK_EQ(kSanOidTlvLen, 5); CHECK_EQ(kSanOidTlv[0], 0x06); // UNIVERSAL 6, OBJECT IDENTIFIER CHECK_EQ(kSanOidTlv[1], 0x03); // three content bytes CHECK_EQ(kSanOidTlv[2], 0x55); // 2.5 CHECK_EQ(kSanOidTlv[3], 0x1D); // .29 CHECK_EQ(kSanOidTlv[4], 0x11); // .17 size_t n = 0; const uint8_t *oid = sanOidDer(&n); CHECK_EQ(n, 3); CHECK_EQ(oid[0], 0x55); CHECK_EQ(oid[1], 0x1D); CHECK_EQ(oid[2], 0x11); CHECK_EQ(sanOidDer(nullptr), oid); // the count is optional, the bytes are not // The failure this pins is the one that actually happened: the dotted string handed to // the writer as if it were the encoding. Nine ASCII bytes are a legal OID -- // 1.10.46.53.46.50.57.46.49.55, as `openssl x509 -text` printed on a real board -- so // the library accepts them, the certificate signs, the handshake completes and only the // hostname check disagrees, in a sentence about the name. No byte of the text form may // appear in what this module hands over. for (size_t i = 0; i < n; i++) { CHECK(oid[i] != '.' && !(oid[i] >= '0' && oid[i] <= '9')); } CHECK(!derContains(kSanOidTlv, kSanOidTlvLen, reinterpret_cast("2.5"), 3)); // And the cross-check, the same way the 34 value bytes were: OpenSSL 4.0.3 writes // 30 29 06 03 55 1d 11 04 22 // for `-addext "subjectAltName=DNS:keyrelay.local,DNS:keyrelay,IP:192.168.4.1"`, which // is the sanExtensionFullDer() case below, byte for byte. } CASE("ASN.1 lengths: short form, long form, never either by accident"); { CHECK_EQ(asn1LengthBytes(0), 1); CHECK_EQ(asn1LengthBytes(127), 1); CHECK_EQ(asn1LengthBytes(128), 2); CHECK_EQ(asn1LengthBytes(255), 2); CHECK_EQ(asn1LengthBytes(256), 3); CHECK_EQ(asn1LengthBytes(65535), 3); CHECK_EQ(asn1LengthBytes(65536), 4); uint8_t len[8]; CHECK_EQ(asn1WriteLength(0, len), 1); CHECK_EQ(len[0], 0x00); CHECK_EQ(asn1WriteLength(127, len), 1); CHECK_EQ(len[0], 0x7F); CHECK_EQ(asn1WriteLength(128, len), 2); CHECK_EQ(len[0], 0x81); CHECK_EQ(len[1], 0x80); CHECK_EQ(asn1WriteLength(300, len), 3); // 0x82 + two value bytes CHECK_EQ(len[0], 0x82); CHECK_EQ(len[1], 0x01); CHECK_EQ(len[2], 0x2C); // 300 == 0x012C CHECK_EQ(asn1WriteLength(65536, len), 4); CHECK_EQ(len[0], 0x83); CHECK_EQ(len[1], 0x01); CHECK_EQ(len[2], 0x00); CHECK_EQ(len[3], 0x00); } CASE("ipv4Bytes: a dotted quad or nothing"); { uint8_t ip[4]; CHECK(ipv4Bytes("192.168.4.1", ip)); CHECK_EQ(ip[0], 192); CHECK_EQ(ip[1], 168); CHECK_EQ(ip[2], 4); CHECK_EQ(ip[3], 1); CHECK(ipv4Bytes("0.0.0.0", ip)); CHECK(ipv4Bytes("255.255.255.255", ip)); CHECK(!ipv4Bytes("256.1.1.1", ip)); CHECK(!ipv4Bytes("1.2.3.256", ip)); CHECK(!ipv4Bytes("1.2.3", ip)); // an address a resolver would silently expand CHECK(!ipv4Bytes("1.2.3.", ip)); CHECK(!ipv4Bytes(".1.2.3", ip)); CHECK(!ipv4Bytes("1.2.3.4.5", ip)); CHECK(!ipv4Bytes("1.2.3.4 ", ip)); CHECK(!ipv4Bytes(" 1.2.3.4", ip)); CHECK(!ipv4Bytes("a.b.c.d", ip)); CHECK(!ipv4Bytes("", ip)); CHECK(!ipv4Bytes("1.2.3.999", ip)); CHECK(!ipv4Bytes(nullptr, ip)); CHECK(!ipv4Bytes("1.2.3.4", nullptr)); // The address the certificate actually carries has to parse, or nothing else here // matters: this is the one value that goes on the wire. CHECK(ipv4Bytes(kApGw, ip)); } CASE("build date: __DATE__ parsed with the field widths it really has"); { int y = 0; int mo = 0; int d = 0; // Single-digit days are space padded to two characters: the classic mis-parse. CHECK(parseBuildDate("Nov 8 2026", y, mo, d)); CHECK_EQ(y, 2026); CHECK_EQ(mo, 11); CHECK_EQ(d, 8); CHECK(parseBuildDate("Jan 1 1970", y, mo, d)); CHECK_EQ(y, 1970); CHECK_EQ(mo, 1); CHECK_EQ(d, 1); CHECK(parseBuildDate("Dec 31 2049", y, mo, d)); CHECK_EQ(y, 2049); CHECK_EQ(mo, 12); CHECK_EQ(d, 31); CHECK(parseBuildDate("Feb 29 2028", y, mo, d)); // a leap day is a legal build date CHECK_EQ(d, 29); CHECK(!parseBuildDate("Nuv 8 2026", y, mo, d)); // month letters transposed CHECK(!parseBuildDate("Foo 8 2026", y, mo, d)); CHECK(!parseBuildDate("Nov 8 26", y, mo, d)); // two-digit year CHECK(!parseBuildDate("Nov 8 20261", y, mo, d)); // trailing junk CHECK(!parseBuildDate("Nov 0 2026", y, mo, d)); CHECK(!parseBuildDate("Nov 32 2026", y, mo, d)); CHECK(!parseBuildDate("Nov", y, mo, d)); CHECK(!parseBuildDate("", y, mo, d)); CHECK(!parseBuildDate(nullptr, y, mo, d)); // The build this binary came from has to parse, or there is no identity at all. CHECK(parseBuildDate(__DATE__, y, mo, d)); // And the window still has to fit UTCTime (1950..2049). This is a real, dated // expiry: a build from 2040 onwards cannot issue a ten-year UTCTime certificate and // would fail at boot with a build-date error. Better a red test in 2039 than a board // that refuses to come up in 2040. CHECK(y + kCertValidityYears <= 2049); CHECK(y >= 2020); CHECK_EQ(kCertValidityYears, 10); } CASE("calendar arithmetic: month ends, year ends, February"); { CHECK(leapYear(2028)); CHECK(!leapYear(2026)); CHECK(!leapYear(1900)); CHECK(leapYear(2000)); CHECK_EQ(daysInMonth(2028, 2), 29); CHECK_EQ(daysInMonth(2026, 2), 28); CHECK_EQ(daysInMonth(2026, 1), 31); CHECK_EQ(daysInMonth(2026, 4), 30); CHECK_EQ(daysInMonth(2026, 0), 0); CHECK_EQ(daysInMonth(2026, 13), 0); int y, mo, d; y = 2026; mo = 1; d = 1; addDays(y, mo, d, -1); CHECK_EQ(y, 2025); CHECK_EQ(mo, 12); CHECK_EQ(d, 31); // back over a year end y = 2028; mo = 3; d = 1; addDays(y, mo, d, -1); CHECK_EQ(y, 2028); CHECK_EQ(mo, 2); CHECK_EQ(d, 29); // back into a leap day y = 2026; mo = 12; d = 31; addDays(y, mo, d, 1); CHECK_EQ(y, 2027); CHECK_EQ(mo, 1); CHECK_EQ(d, 1); y = 2026; mo = 1; d = 31; addDays(y, mo, d, 1); CHECK_EQ(y, 2026); CHECK_EQ(mo, 2); CHECK_EQ(d, 1); y = 2026; mo = 6; d = 15; addDays(y, mo, d, 0); CHECK_EQ(y, 2026); CHECK_EQ(mo, 6); CHECK_EQ(d, 15); // Ten years from a leap day lands on the last day that exists, never on a date the // certificate writer would reject at build time. y = 2028; mo = 2; d = 29; addYears(y, mo, d, 1); CHECK_EQ(y, 2029); CHECK_EQ(mo, 2); CHECK_EQ(d, 28); y = 2028; mo = 2; d = 29; addYears(y, mo, d, 4); CHECK_EQ(y, 2032); CHECK_EQ(mo, 2); CHECK_EQ(d, 29); // 2032 is a leap year y = 2026; mo = 1; d = 31; addYears(y, mo, d, kCertValidityYears); CHECK_EQ(y, 2036); CHECK_EQ(mo, 1); CHECK_EQ(d, 31); y = 2026; mo = 11; d = 8; addYears(y, mo, d, 0); CHECK_EQ(y, 2026); CHECK_EQ(mo, 11); CHECK_EQ(d, 8); } CASE("utcTime: the string the certificate writer parses, or a refusal"); { char t[32]; prime(t, sizeof(t)); CHECK_EQ(utcTime(t, sizeof(t), 2026, 11, 8, 0, 0, 0), 14); CHECK_STR(t, "20261108000000"); CHECK_EQ(utcTime(t, sizeof(t), 2049, 12, 31, 23, 59, 59), 14); CHECK_STR(t, "20491231235959"); CHECK_EQ(utcTime(t, sizeof(t), 1950, 1, 1, 0, 0, 0), 14); CHECK_STR(t, "19500101000000"); CHECK_EQ(utcTime(t, sizeof(t), 2028, 2, 29, 12, 34, 56), 14); CHECK_STR(t, "20280229123456"); // Exactly enough room, and one byte short of it. CHECK_EQ(utcTime(t, 15, 2026, 11, 8, 0, 0, 0), 14); prime(t, sizeof(t)); CHECK_EQ(utcTime(t, 14, 2026, 11, 8, 0, 0, 0), 0); CHECK(untouched(t, 14)); // Outside UTCTime's window: GeneralizedTime is a different encoding, and quietly // emitting one is how every handshake starts failing in 2050. CHECK_EQ(utcTime(t, sizeof(t), 1949, 12, 31, 0, 0, 0), 0); CHECK_EQ(utcTime(t, sizeof(t), 2050, 1, 1, 0, 0, 0), 0); // Components out of range, including the date that does not exist. CHECK_EQ(utcTime(t, sizeof(t), 2026, 0, 1, 0, 0, 0), 0); CHECK_EQ(utcTime(t, sizeof(t), 2026, 13, 1, 0, 0, 0), 0); CHECK_EQ(utcTime(t, sizeof(t), 2026, 11, 0, 0, 0, 0), 0); CHECK_EQ(utcTime(t, sizeof(t), 2026, 11, 31, 0, 0, 0), 0); CHECK_EQ(utcTime(t, sizeof(t), 2026, 2, 29, 0, 0, 0), 0); // 2026 is not a leap year CHECK_EQ(utcTime(t, sizeof(t), 2026, 11, 8, 24, 0, 0), 0); CHECK_EQ(utcTime(t, sizeof(t), 2026, 11, 8, 0, 60, 0), 0); CHECK_EQ(utcTime(t, sizeof(t), 2026, 11, 8, 0, 0, 61), 0); CHECK_EQ(utcTime(nullptr, sizeof(t), 2026, 11, 8, 0, 0, 0), 0); // :60 is the leap-second slot RFC 5280 allows; accepting it is not a bug. CHECK_EQ(utcTime(t, sizeof(t), 2026, 11, 8, 23, 59, 60), 14); } CASE("the boot path's own sequence, with a blob where the key would be"); { // What task 02 does at boot: produce a certificate DER, armour it, store the text, // read it back out of storage, hand the bytes to the library. Every step here is the // real code; only key generation is replaced by a blob, because the host has no // crypto library and must not gain one. fillBlob(der, 430); const size_t n = wrapPem(kPemCertLabel, der, 430, pem, kPemMax); CHECK(n > 0); CHECK(n == strlen(pem)); // The NVS record is read back as a C string: the armour must carry no NUL of its own // and must end with the newline the writer was asked to leave in. CHECK(pem[n] == '\0'); CHECK(pem[n - 1] == '\n'); uint8_t parsed[1024]; size_t parsedLen = 0; CHECK(pemFind(kPemCertLabel, pem, parsed, sizeof(parsed), &parsedLen)); CHECK_EQ(parsedLen, 430); CHECK(memcmp(parsed, der, 430) == 0); // A record whose tail was lost: refused, not decoded into something shorter. const std::string torn(pem, n - 30); CHECK(!pemFind(kPemCertLabel, torn.c_str(), parsed, sizeof(parsed), &parsedLen)); // The private key travels through the same two functions under its own label, and a // reader looking for the certificate must not find it. fillBlob(der, 121); char keyPem[512]; CHECK(wrapPem(kPemKeyLabel, der, 121, keyPem, sizeof(keyPem)) > 0); CHECK(!pemFind(kPemCertLabel, keyPem, parsed, sizeof(parsed), &parsedLen)); CHECK(pemFind(kPemKeyLabel, keyPem, parsed, sizeof(parsed), &parsedLen)); CHECK_EQ(parsedLen, 121); CHECK(memcmp(parsed, der, 121) == 0); } CASE("fingerprintPrefix: the status frame's label, or a refusal"); { // The status frame carries the leading bytes of the fingerprint so the page can show the // same label `cert` prints. Half a stored fingerprint must not turn into a shorter label // that still looks like a label: the whole job of the value is "same board as before". const char *fp = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"; char out[80]; // a whole 64-character fingerprint plus the NUL prime(out, sizeof(out)); CHECK(fingerprintPrefix(fp, 16, out, sizeof(out))); CHECK_STR(out, "ba7816bf8f01cfea"); CHECK(fingerprintPrefix(fp, 64, out, sizeof(out))); CHECK_STR(out, fp); // Room is exact -- 16 characters need 17 bytes -- and one byte short writes nothing // rather than a 15-character label somebody believes. CHECK(fingerprintPrefix(fp, 16, out, 17)); prime(out, sizeof(out)); CHECK(!fingerprintPrefix(fp, 16, out, 16)); CHECK(untouched(out, 16)); // Not hex, or too short for the prefix asked for: a torn NVS record is a refusal, never // a prefix of garbage. Upper case is still hex -- refusing it would make a pin somebody // typed in upper case look like a different board. CHECK(!fingerprintPrefix("ba78zzzzzzzzzzzzzz", 16, out, sizeof(out))); CHECK(!fingerprintPrefix("ba78", 16, out, sizeof(out))); CHECK(!fingerprintPrefix("", 16, out, sizeof(out))); CHECK(fingerprintPrefix("BA7816BF8F01CFEA", 16, out, sizeof(out))); CHECK_STR(out, "BA7816BF8F01CFEA"); CHECK(!fingerprintPrefix(nullptr, 16, out, sizeof(out))); CHECK(!fingerprintPrefix(fp, 16, nullptr, sizeof(out))); // A zero-length prefix is a legitimate empty string, and still needs room for the NUL. CHECK(fingerprintPrefix(fp, 0, out, 1)); CHECK_STR(out, ""); } CASE("fingerprintPairs: two lines the CDC transmit ring cannot swallow"); { const char *fp = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"; char line[128]; prime(line, sizeof(line)); CHECK_EQ(fingerprintPairs(fp, 0, 16, line, sizeof(line)), 47); CHECK_STR(line, "ba:78:16:bf:8f:01:cf:ea:41:41:40:de:5d:ae:22:23"); CHECK_EQ(fingerprintPairs(fp, 16, 16, line, sizeof(line)), 47); CHECK_STR(line, "b0:03:61:a3:96:17:7a:9c:b4:10:ff:61:f2:00:15:ad"); // The two lines the console prints are 47 characters each, which with the 8-space indent // stays under the 128-byte TinyUSB CDC transmit ring. That is the entire reason the print // is wrapped: an unwrapped 95-character line with the global transmit timeout at 0 is // dropped mid-value, which is README bug 1 wearing a fingerprint. CHECK(47 + 8 < 128); // Nothing is lost on the way: take the colons out of both lines and it is the digest. char first[64], second[64]; CHECK_EQ(fingerprintPairs(fp, 0, 16, first, sizeof(first)), 47); CHECK_EQ(fingerprintPairs(fp, 16, 16, second, sizeof(second)), 47); std::string rejoined; for (const char *p = first; *p; p++) { if (*p != ':') rejoined += *p; } for (const char *p = second; *p; p++) { if (*p != ':') rejoined += *p; } CHECK_EQ(rejoined.size(), 64); CHECK_STR(rejoined.c_str(), fp); // One byte short of the 48 it needs is a refusal that writes nothing; so is a source that // stops being hex partway, which must not leave half a fingerprint in the buffer. prime(line, sizeof(line)); CHECK_EQ(fingerprintPairs(fp, 0, 16, line, 47), 0); CHECK(untouched(line, 47)); std::string bad(fp); bad[4] = 'z'; prime(line, sizeof(line)); CHECK_EQ(fingerprintPairs(bad.c_str(), 0, 16, line, sizeof(line)), 0); CHECK(untouched(line, 47)); // An empty range, or one that runs off the end of a 32-byte digest: refused, because // printing past the digest is how a fingerprint grows characters it never had. CHECK_EQ(fingerprintPairs(fp, 0, 0, line, sizeof(line)), 0); CHECK_EQ(fingerprintPairs(fp, 32, 1, line, sizeof(line)), 0); CHECK_EQ(fingerprintPairs(fp, 31, 2, line, sizeof(line)), 0); CHECK_EQ(fingerprintPairs(fp, 31, 1, line, sizeof(line)), 2); // the last pair alone CHECK_STR(line, "ad"); CHECK_EQ(fingerprintPairs(nullptr, 0, 16, line, sizeof(line)), 0); CHECK_EQ(fingerprintPairs(fp, 0, 16, nullptr, sizeof(line)), 0); } CASE("derContains: the certificate decides whether the printed names are true"); { // `cert` prints the name list from sanEntries(); this is the check that the stored // certificate really carries those bytes, so the console can say "and the certificate // agrees" rather than implying it. The needle is the exact extension value, so a name // changed by one character is a refusal, not a near match. uint8_t san[kSanDerMax]; const size_t sanLen = sanExtensionDer(san, sizeof(san)); CHECK_EQ(sanLen, 34); std::vector cert; for (size_t i = 0; i < 120; i++) cert.push_back(static_cast(0xA0 ^ (i & 0x3F))); cert.insert(cert.end(), san, san + sanLen); for (size_t i = 0; i < 200; i++) cert.push_back(static_cast(i * 7)); CHECK(derContains(cert.data(), cert.size(), san, sanLen)); // An empty needle is true of every buffer in existence, so it is a refusal instead: the // answer has to be about these bytes. CHECK(!derContains(cert.data(), cert.size(), san, 0)); CHECK(!derContains(cert.data(), 10, san, sanLen)); CHECK(!derContains(nullptr, cert.size(), san, sanLen)); CHECK(!derContains(cert.data(), cert.size(), nullptr, sanLen)); // One byte different in the needle and the certificate no longer carries it... uint8_t torn[kSanDerMax]; memcpy(torn, san, sanLen); torn[sanLen - 1] ^= 0x01; CHECK(!derContains(cert.data(), cert.size(), torn, sanLen)); // ... and one byte different inside the certificate's own SAN, either. std::vector other = cert; other[120 + 1] ^= 0x01; // the first byte of the first DNS name CHECK(!derContains(other.data(), other.size(), san, sanLen)); // A one-byte haystack is not a special case that only matches at offset 0 by luck. const uint8_t one = 0x30; CHECK(derContains(&one, 1, &one, 1)); CHECK(!derContains(&one, 1, san, sanLen)); // Found at the very end of a buffer too -- an off-by-one in the loop bound shows up here // and nowhere else, because the SAN is never at the end of a real certificate. std::vector tailCert(64, 0x5A); tailCert.insert(tailCert.end(), san, san + sanLen); CHECK(derContains(tailCert.data(), tailCert.size(), san, sanLen)); } CASE("sanExtensionFullDer / derCarriesSan: the extension a client sees, or none"); { uint8_t full[kSanExtensionFullDerMax]; prime(reinterpret_cast(full), sizeof(full)); const size_t n = sanExtensionFullDer(full, sizeof(full)); // SEQUENCE(41) { OID(3) 55 1D 11, OCTET STRING(34) }, non-critical so no // BOOLEAN -- and these 43 bytes are lifted from a certificate OpenSSL issued for the // same three names, not derived from this encoder's own output. CHECK_EQ(n, 43); CHECK_STR(hexOf(full, n).c_str(), "30290603551d1104223020820e6b657972656c61792e6c6f63616c82086b657972" "656c61798704c0a80401"); CHECK_EQ(sanExtensionFullDer(full, n), n); // exact fit prime(reinterpret_cast(full), sizeof(full)); CHECK_EQ(sanExtensionFullDer(full, n - 1), 0); // one short writes nothing... CHECK(untouched(reinterpret_cast(full), n - 1)); CHECK_EQ(sanExtensionFullDer(nullptr, sizeof(full)), 0); uint8_t san[kSanDerMax]; const size_t sanLen = sanExtensionDer(san, sizeof(san)); CHECK_EQ(sanLen, 34); // A certificate that carries the extension this build writes: yes. std::vector cert(90, 0x3C); sanExtensionFullDer(full, sizeof(full)); cert.insert(cert.end(), full, full + n); cert.insert(cert.end(), {0x03, 0x47, 0x00}); CHECK(derCarriesSan(cert.data(), cert.size())); CHECK(derCarriesSan(cert.data(), 90 + n)); // the extension ends exactly here CHECK(!derCarriesSan(cert.data(), 90 + n - 1)); // one byte short of carrying it // The certificate a board actually issued before this was fixed -- the right 34 value // bytes, wrapped the right way, under the OID 1.10.46.53.46.50.57.46.49.55 (the ASCII // of "2.5.29.17"). It has no subjectAlternativeName as far as any client is concerned, // and this is where that becomes an observation instead of a surprise. Note what the // weaker question answers here: the value really is in the certificate. static const uint8_t kPreFix[] = { 0x30, 0x2F, 0x06, 0x09, '2', '.', '5', '.', '2', '9', '.', '1', '7', 0x04, 0x22, }; std::vector legacy; legacy.insert(legacy.end(), kPreFix, kPreFix + sizeof(kPreFix)); legacy.insert(legacy.end(), san, san + sanLen); legacy.insert(legacy.end(), 40, 0x11); CHECK(derContains(legacy.data(), legacy.size(), san, sanLen)); // the easy question: yes CHECK(!derCarriesSan(legacy.data(), legacy.size())); // the real question: no // One byte of the OID and it is a different extension: a name list under a label nobody // asked for is the same failure in a different costume. std::vector shifted = cert; const size_t oidAt = 90 + 4; // SEQUENCE(2) + tag + length of the OID CHECK_EQ(shifted[oidAt], 0x55); shifted[oidAt] = 0x54; CHECK(!derCarriesSan(shifted.data(), shifted.size())); // The value alone, or the OID alone, is not the extension. std::vector onlyValue(64, 0x07); onlyValue.insert(onlyValue.end(), san, san + sanLen); CHECK(!derCarriesSan(onlyValue.data(), onlyValue.size())); std::vector onlyOid(64, 0x07); onlyOid.insert(onlyOid.end(), kSanOidTlv, kSanOidTlv + kSanOidTlvLen); CHECK(!derCarriesSan(onlyOid.data(), onlyOid.size())); // Nothing to search, and nothing to search with. CHECK(!derCarriesSan(nullptr, 100)); CHECK(!derCarriesSan(cert.data(), 0)); } CASE("dateTimeText: a validity bound as a person reads it"); { char t[32]; prime(t, sizeof(t)); CHECK_EQ(dateTimeText(t, sizeof(t), 2026, 11, 8, 0, 0, 0), 23); CHECK_STR(t, "2026-11-08 00:00:00 UTC"); CHECK_EQ(dateTimeText(t, sizeof(t), 2036, 11, 7, 23, 59, 59), 23); CHECK_STR(t, "2036-11-07 23:59:59 UTC"); // Exactly enough room, and one byte short of it. CHECK_EQ(dateTimeText(t, 24, 2026, 11, 8, 0, 0, 0), 23); prime(t, sizeof(t)); CHECK_EQ(dateTimeText(t, 23, 2026, 11, 8, 0, 0, 0), 0); CHECK(untouched(t, 23)); // The same component checks utcTime() applies before a certificate is written, including // the February that does not exist -- a date this tree would never have encoded cannot be // printed as if it had been. CHECK_EQ(dateTimeText(t, sizeof(t), 2026, 2, 29, 0, 0, 0), 0); CHECK_EQ(dateTimeText(t, sizeof(t), 2028, 2, 29, 0, 0, 0), 23); CHECK_EQ(dateTimeText(t, sizeof(t), 2026, 13, 1, 0, 0, 0), 0); CHECK_EQ(dateTimeText(t, sizeof(t), 2026, 0, 1, 0, 0, 0), 0); CHECK_EQ(dateTimeText(t, sizeof(t), 2026, 11, 31, 0, 0, 0), 0); CHECK_EQ(dateTimeText(t, sizeof(t), 2026, 11, 8, 24, 0, 0), 0); CHECK_EQ(dateTimeText(t, sizeof(t), 2026, 11, 8, 0, 60, 0), 0); // :60 is RFC 5280's leap-second slot, accepted here exactly as utcTime() accepts it. CHECK_EQ(dateTimeText(t, sizeof(t), 2026, 11, 8, 23, 59, 60), 23); // NOT utcTime's 1950..2049 window: reading a date is not the same permission as writing // one, and a certificate outliving UTCTime is the writer's problem, not the reader's. CHECK_EQ(dateTimeText(t, sizeof(t), 2050, 1, 1, 0, 0, 0), 23); CHECK_EQ(dateTimeText(t, sizeof(t), 1949, 12, 31, 0, 0, 0), 23); CHECK_EQ(dateTimeText(nullptr, sizeof(t), 2026, 11, 8, 0, 0, 0), 0); } return report("test_tls_pem"); }