local testimony = require("testimony") local litls = require("litls.core") local hex = litls.hex_encode local unhex = litls.hex_decode local testify = testimony.new("== LITLS X.509 / CSR ==") -- Test certificates generated with openssl (ECDSA P-256, SHA-256) -- CA: self-signed, CN=Test CA, valid 2026-03-05 to 2036-03-02 -- Leaf: signed by CA, CN=example.com, SANs: example.com, www.example.com local CA_CERT_DER_HEX = "308201793082011fa00302010202147e5c8ca31ae4a19931c0eb17422f8fbb8e3efe50" .. "300a06082a8648ce3d04030230123110300e06035504030c075465737420434130" .. "1e170d3236303330353138323430305a170d33363033303231383234303" .. "05a30123110300e06035504030c075465737420434130593013060" .. "72a8648ce3d020106082a8648ce3d0301070342000442a4c879d7a11ec7bacad310bfee" .. "129eef60c90007c4a10cc18283d936f67d78ef4d020fc5f4d6c087268abd48a7ac5415" .. "048a41bf2e381f5060df384a71ff9fa3533051301d0603551d0e04160414ad1d1dbe1f" .. "d5fd0d859c33a4020e95cea9d31d54301f0603551d23041830168014ad1d1dbe1fd5fd" .. "0d859c33a4020e95cea9d31d54300f0603551d130101ff040530030101ff300a06082a" .. "8648ce3d040302034800304502204cd5eb336d0b6e75ea185261f36c3c75fcda991a27" .. "fe4f7c15bede9a2fa379a3022100bb6784087d6c6ac2c5f6698924e96cd2c0d43b7774" .. "28eac21b931d4be3cd9b79" local LEAF_CERT_DER_HEX = "308201953082013ba0030201020214404907bbaf74bc210426974866d92132e61dffa6" .. "300a06082a8648ce3d04030230123110300e06035504030c075465737420434130" .. "1e170d3236303330353138323430305a170d3336303330323138323430305a3016" .. "3114301206035504030c0b6578616d706c652e636f6d3059301306072a8648ce3d0201" .. "06082a8648ce3d030107034200045e360927cf88a126e5142ca95c91294fbc09345eea" .. "e29ab609c9313051175272cd887de2efc6ca983073a50dd309e875591e06b0310e350c" .. "05315143221d106ca36b306930270603551d110420301e820b6578616d706c652e636f" .. "6d820f7777772e6578616d706c652e636f6d301d0603551d0e0416041426ef636dbbbb" .. "ef55e61338dec7806e556ea50d10301f0603551d23041830168014ad1d1dbe1fd5fd0d" .. "859c33a4020e95cea9d31d54300a06082a8648ce3d0403020348003045022100f4c9b6" .. "67d8f4238a990414c8acfc12ae1d61eec815ccec56d2cf8a0c7e5b4b0702203b7a626c" .. "7aad4b8eb3d1953a297d663ffa161ede418dd45c31399f7c20bfef79" local CA_CERT_PEM = [[-----BEGIN CERTIFICATE----- MIIBeTCCAR+gAwIBAgIUflyMoxrkoZkxwOsXQi+Pu44+/lAwCgYIKoZIzj0EAwIw EjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjAzMDUxODI0MDBaFw0zNjAzMDIxODI0 MDBaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC AARCpMh516Eex7rK0xC/7hKe72DJAAfEoQzBgoPZNvZ9eO9NAg/F9NbAhyaKvUin rFQVBIpBvy44H1Bg3zhKcf+fo1MwUTAdBgNVHQ4EFgQUrR0dvh/V/Q2FnDOkAg6V zqnTHVQwHwYDVR0jBBgwFoAUrR0dvh/V/Q2FnDOkAg6VzqnTHVQwDwYDVR0TAQH/ BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiBM1eszbQtudeoYUmHzbDx1/NqZGif+ T3wVvt6aL6N5owIhALtnhAh9bGrCxfZpiSTpbNLA1Dt3dCjqwhuTHUvjzZt5 -----END CERTIFICATE-----]] -- RSA 2048 self-signed cert: CN=RSA Test CA local RSA_CA_CERT_DER_HEX = "3082030d308201f5a0030201020214183cf45be308b6cb2487345cf5750e0a0525c2c8" .. "300d06092a864886f70d01010b050030163114301206035504030c0b52534120546573" .. "7420434130" .. "1e170d3236303330353138323431365a170d3336303330323138323431365a30163114" .. "301206035504030c0b525341205465737420434130820122300d06092a864886f70d01" .. "010105000382010f003082010a0282010100cd500b8f36b4fa19f610b019ef35f516fa" .. "953bd6ee7d7005069eab9f6bc63f09faf28fcd21e0d8cefb3140a4b30e3eb462929db2" .. "1c67e345693348ff8d00c78ab7177e4a986ce11338a1b31d976f97580a9df853a9ca23" .. "d0444b32a08c99b98fb1ef87978808d48fed4537446042db18f2a0e43a7631ed6cf066" .. "559493b36c8eb6846b983b23c4a4723c8c0120d94d4eca7e5e72c84bbd61146b68f770" .. "e6b282cf86f4ce633210ac8a80b3ffa6ec21ca180886f2e40855ca95cac0104a6ca50d" .. "d168278bd0929c37e58086be9a5f170045698a48facd3bfc3fe2100dd5424ab5fadcba" .. "82df91488204fffa9526a6ece4ed46f63e90c0bc7d8f635d8f9679fb0b0203010001a3" .. "533051301d0603551d0e041604144489fed49fb104c9ee72605743ec43248176a058301f" .. "0603551d230418301680144489fed49fb104c9ee72605743ec43248176a058300f0603" .. "551d130101ff040530030101ff300d06092a864886f70d01010b05000382010100" .. "32d66ea6433b5645f6fcd6a53027696b3125d797994e217e91c840ab2b0e54f8e8c620" .. "17497ca82f4f3bc756ad092ac39d7a087896b080e1edc72863a4420441c46ed3ee7a78" .. "b9a2e1fb9b64758aa1f90dddec93b15022f6557471e81041b87c0d0753c7b8b2bb4fba" .. "12e20c99d85bcc7c574ff6dd3dc05857022b5555f9f7698516568fba2c49b14c6531c7" .. "ce32582996ed880a200c631b830ec5e699496ce21808c55ece53d2e39860674dc08d525" .. "dc5049ec96a009fd8eccb7b57f5fa274de13e917920ee92291973ee182f1a00daceaa73" .. "ff421ea9197df1981e615c9843cb362499a10194622c67f596afe709d192a217e6b39a" .. "becbb1bfb3a24365a96f" -- ── PEM decode ────────────────────────────────────────── testify:that("pem_decode strips PEM headers and base64 decodes", function() local der = litls.pem_decode(CA_CERT_PEM) testimony.assert_equal(true, der ~= nil) local expected = unhex(CA_CERT_DER_HEX) testimony.assert_equal(hex(expected), hex(der)) end) -- ── Certificate parsing ───────────────────────────────── testify:that("parse ECDSA CA cert: CN, dates", function() local der = unhex(CA_CERT_DER_HEX) local info = litls.x509_parse(der) testimony.assert_equal(true, info ~= nil) testimony.assert_equal("Test CA", info.common_name) testimony.assert_equal("1772735040", info.not_before) testimony.assert_equal("2088095040", info.not_after) end) testify:that("parse leaf cert with SANs", function() local der = unhex(LEAF_CERT_DER_HEX) local info = litls.x509_parse(der) testimony.assert_equal(true, info ~= nil) testimony.assert_equal("example.com", info.common_name) testimony.assert_equal(true, info.sans ~= nil) testimony.assert_equal(2, #info.sans) testimony.assert_equal("example.com", info.sans[1]) testimony.assert_equal("www.example.com", info.sans[2]) end) testify:that("parse RSA cert: CN, dates", function() local der = unhex(RSA_CA_CERT_DER_HEX) local info = litls.x509_parse(der) testimony.assert_equal(true, info ~= nil) testimony.assert_equal("RSA Test CA", info.common_name) testimony.assert_equal("1772735056", info.not_before) testimony.assert_equal("2088095056", info.not_after) end) testify:that("parse rejects garbage input", function() local info, err = litls.x509_parse("not a certificate") testimony.assert_equal(true, info == nil) end) -- ── CSR generation ────────────────────────────────────── testify:that("CSR generation produces valid DER", function() local priv, pub = litls.p256_keygen() local csr = litls.csr_generate(priv, pub, "test.example.com") testimony.assert_equal(true, csr ~= nil) -- CSR should start with SEQUENCE tag (0x30) testimony.assert_equal(0x30, string.byte(csr, 1)) -- CSR should be reasonable size testimony.assert_equal(true, #csr > 100) testimony.assert_equal(true, #csr < 2048) end) testify:that("CSR with SANs", function() local priv, pub = litls.p256_keygen() local sans = { "alt1.example.com", "alt2.example.com" } local csr = litls.csr_generate(priv, pub, "main.example.com", sans) testimony.assert_equal(true, csr ~= nil) -- Verify all domain names appear in the DER output testimony.assert_equal(true, csr:find("main.example.com", 1, true) ~= nil) testimony.assert_equal(true, csr:find("alt1.example.com", 1, true) ~= nil) testimony.assert_equal(true, csr:find("alt2.example.com", 1, true) ~= nil) end) -- ── Chain validation ──────────────────────────────────── testify:that("chain validation: ECDSA leaf + CA", function() local leaf_der = unhex(LEAF_CERT_DER_HEX) local ok, err = litls.x509_verify_chain({ leaf_der }, CA_CERT_PEM) testimony.assert_equal(true, ok) end) testify:that("chain validation rejects unknown anchor", function() -- Use the RSA CA PEM as anchor, but leaf was signed by ECDSA CA local rsa_ca_pem = "-----BEGIN CERTIFICATE-----\n" .. "MIIDDTCCAfWgAwIBAgIUGDz0W+MItsskhzRc9XUOCgUlwsgwDQYJKoZIhvcNAQEL\n" .. "BQAwFjEUMBIGA1UEAwwLUlNBIFRlc3QgQ0EwHhcNMjYwMzA1MTgyNDE2WhcNMzYw\n" .. "MzAyMTgyNDE2WjAWMRQwEgYDVQQDDAtSU0EgVGVzdCBDQTCCASIwDQYJKoZIhvcN\n" .. "AQEBBQADggEPADCCAQoCggEBAM1QC482tPoZ9hCwGe819Rb6lTvW7n1wBQaeq59r\n" .. "xj8J+vKPzSHg2M77MUCksw4+tGKSnbIcZ+NFaTNI/40Ax4q3F35KmGzhEzihsx2X\n" .. "b5dYCp34U6nKI9BESzKgjJm5j7Hvh5eICNSP7UU3RGBC2xjyoOQ6djHtbPBmVZST\n" .. "s2yOtoRrmDsjxKRyPIwBINlNTsp+XnLIS71hFGto93DmsoLPhvTOYzIQrIqAs/+m\n" .. "7CHKGAiG8uQIVcqVysAQSmylDdFoJ4vQkpw35YCGvppfFwBFaYpI+s07/D/iEA3V\n" .. "Qkq1+ty6gt+RSIIE//qVJqbs5O1G9j6QwLx9j2Ndj5Z5+wsCAwEAAaNTMFEwHQYD\n" .. "VR0OBBYEFESJ/tSfsQTJ7nJgV0PsQySBdqBYMB8GA1UdIwQYMBaAFESJ/tSfsQTJ\n" .. "7nJgV0PsQySBdqBYMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB\n" .. "ADLWbqZDO1ZF9vzWpTAnaWsxJdeXmU4hfpHIQKsrDlT46MYgF0l8qC9PO8dWrQkq\n" .. "w516CHiWsIDh7ccoY6RCBEHEbtPueni5ouH7m2R1iqH5Dd3sk7FQIvZVdHHoEEG4\n" .. "fA0HU8e4srtPuhLiDJnYW8x8V0/23T3AWFcCK1VV+fdphRZWj7osSbFMZTHHzjJY\n" .. "KZbtiAogDGMbgw7F5plJbOIYCMVezlPS45hgZ03AjVJdxQSeyWoAn9jsy3tX9fon\n" .. "TeE+kXkg7pIpGXPuGC8aANrOqnP/Qh6pGX3xmB5hXJhDyzYkmaEBlGIsZ/WWr+cJ\n" .. "0ZKiF+azmr7Lsb+zokNlqW8=\n" .. "-----END CERTIFICATE-----" local leaf_der = unhex(LEAF_CERT_DER_HEX) local ok, err = litls.x509_verify_chain({ leaf_der }, rsa_ca_pem) testimony.assert_equal(true, ok == nil) testimony.assert_equal(true, err ~= nil) end) testify:that("chain validation: RSA self-signed cert validates against itself", function() local rsa_der = unhex(RSA_CA_CERT_DER_HEX) local rsa_pem = "-----BEGIN CERTIFICATE-----\n" .. "MIIDDTCCAfWgAwIBAgIUGDz0W+MItsskhzRc9XUOCgUlwsgwDQYJKoZIhvcNAQEL\n" .. "BQAwFjEUMBIGA1UEAwwLUlNBIFRlc3QgQ0EwHhcNMjYwMzA1MTgyNDE2WhcNMzYw\n" .. "MzAyMTgyNDE2WjAWMRQwEgYDVQQDDAtSU0EgVGVzdCBDQTCCASIwDQYJKoZIhvcN\n" .. "AQEBBQADggEPADCCAQoCggEBAM1QC482tPoZ9hCwGe819Rb6lTvW7n1wBQaeq59r\n" .. "xj8J+vKPzSHg2M77MUCksw4+tGKSnbIcZ+NFaTNI/40Ax4q3F35KmGzhEzihsx2X\n" .. "b5dYCp34U6nKI9BESzKgjJm5j7Hvh5eICNSP7UU3RGBC2xjyoOQ6djHtbPBmVZST\n" .. "s2yOtoRrmDsjxKRyPIwBINlNTsp+XnLIS71hFGto93DmsoLPhvTOYzIQrIqAs/+m\n" .. "7CHKGAiG8uQIVcqVysAQSmylDdFoJ4vQkpw35YCGvppfFwBFaYpI+s07/D/iEA3V\n" .. "Qkq1+ty6gt+RSIIE//qVJqbs5O1G9j6QwLx9j2Ndj5Z5+wsCAwEAAaNTMFEwHQYD\n" .. "VR0OBBYEFESJ/tSfsQTJ7nJgV0PsQySBdqBYMB8GA1UdIwQYMBaAFESJ/tSfsQTJ\n" .. "7nJgV0PsQySBdqBYMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB\n" .. "ADLWbqZDO1ZF9vzWpTAnaWsxJdeXmU4hfpHIQKsrDlT46MYgF0l8qC9PO8dWrQkq\n" .. "w516CHiWsIDh7ccoY6RCBEHEbtPueni5ouH7m2R1iqH5Dd3sk7FQIvZVdHHoEEG4\n" .. "fA0HU8e4srtPuhLiDJnYW8x8V0/23T3AWFcCK1VV+fdphRZWj7osSbFMZTHHzjJY\n" .. "KZbtiAogDGMbgw7F5plJbOIYCMVezlPS45hgZ03AjVJdxQSeyWoAn9jsy3tX9fon\n" .. "TeE+kXkg7pIpGXPuGC8aANrOqnP/Qh6pGX3xmB5hXJhDyzYkmaEBlGIsZ/WWr+cJ\n" .. "0ZKiF+azmr7Lsb+zokNlqW8=\n" .. "-----END CERTIFICATE-----" local ok, err = litls.x509_verify_chain({ rsa_der }, rsa_pem) testimony.assert_equal(true, ok) end) testify:that("chain validation rejects corrupted signature", function() -- Corrupt one byte of the leaf cert local leaf_der = unhex(LEAF_CERT_DER_HEX) -- Flip a byte near the end (in the signature area) local corrupted = leaf_der:sub(1, #leaf_der - 5) .. string.char(0xFF) .. leaf_der:sub(#leaf_der - 3) local ok, err = litls.x509_verify_chain({ corrupted }, CA_CERT_PEM) testimony.assert_equal(true, ok == nil) end) -- ── Extension parsing (BasicConstraints / KeyUsage / EKU) ────── testify:that("parse CA cert exposes BasicConstraints CA=true", function() local der = unhex(CA_CERT_DER_HEX) local info = litls.x509_parse(der) testimony.assert_equal(true, info ~= nil) -- The test CA was generated with "CA:TRUE" basicConstraints. testimony.assert_equal(true, info.is_ca == true) end) testify:that("parse leaf cert without BasicConstraints omits is_ca", function() local der = unhex(LEAF_CERT_DER_HEX) local info = litls.x509_parse(der) testimony.assert_equal(true, info ~= nil) -- Leaf has no BasicConstraints extension, so the field should be absent. testimony.assert_equal(true, info.is_ca == nil) end) -- ── Hostname verification (RFC 6125) ────────────────────────── testify:that("hostname match: SAN dNSName exact", function() local leaf_der = unhex(LEAF_CERT_DER_HEX) -- LEAF SANs: "example.com", "www.example.com" testimony.assert_equal(true, litls.x509_verify_hostname(leaf_der, "example.com") == true) testimony.assert_equal(true, litls.x509_verify_hostname(leaf_der, "www.example.com") == true) end) testify:that("hostname match: case-insensitive", function() local leaf_der = unhex(LEAF_CERT_DER_HEX) testimony.assert_equal(true, litls.x509_verify_hostname(leaf_der, "EXAMPLE.com") == true) end) testify:that("hostname match: trailing dot FQDN stripped", function() local leaf_der = unhex(LEAF_CERT_DER_HEX) testimony.assert_equal(true, litls.x509_verify_hostname(leaf_der, "example.com.") == true) end) testify:that("hostname mismatch: different domain", function() local leaf_der = unhex(LEAF_CERT_DER_HEX) local ok, err = litls.x509_verify_hostname(leaf_der, "evil.com") testimony.assert_equal(true, ok == nil) testimony.assert_equal(true, err ~= nil) end) testify:that("hostname mismatch: subdomain of SAN (no wildcard)", function() local leaf_der = unhex(LEAF_CERT_DER_HEX) -- SAN is "example.com" (not "*.example.com") so "a.example.com" must fail. local ok = litls.x509_verify_hostname(leaf_der, "a.example.com") testimony.assert_equal(true, ok == nil) end) testify:that("hostname mismatch: empty input fails closed", function() local leaf_der = unhex(LEAF_CERT_DER_HEX) local ok = litls.x509_verify_hostname(leaf_der, "") testimony.assert_equal(true, ok == nil) end) testify:conclude()