├── tmp └── .gitignore ├── shell.config ├── test ├── cross_signing │ ├── CAs │ │ ├── new_ca.srl │ │ ├── expired_ca.srl │ │ ├── third_ca.srl │ │ ├── new_ca.csr │ │ ├── new_ca.pem │ │ ├── expired_ca.pem │ │ ├── third_ca.pem │ │ ├── expired_ca_key.pem │ │ ├── new_ca_key.pem │ │ └── third_ca_key.pem │ ├── install_faketime.sh │ ├── intermediate_ca.ext │ ├── intermediate_certificates │ │ ├── regular_intermediate_cert.srl │ │ ├── regular_intermediate_cert.csr │ │ ├── cross_signed_bad_intermediate_cert.pem │ │ ├── cross_signed_good_intermediate_cert.pem │ │ ├── regular_intermediate_cert.pem │ │ └── regular_intermediate_cert_key.pem │ ├── leaf_certificates │ │ ├── localhost.csr │ │ ├── localhost.pem │ │ └── localhost_key.pem │ ├── CA_stores │ │ ├── ca_store1_for_cross_signing.pem │ │ ├── bad_ca_store_for_expiry.pem │ │ ├── ca_store2_for_cross_signing.pem │ │ └── good_ca_store_for_expiry.pem │ ├── certificate_chains │ │ ├── localhost_chain_for_cross_signing.pem │ │ └── localhost_chain_for_expiry.pem │ └── Makefile ├── common_scenarios │ ├── CAs │ │ ├── another_ca.srl │ │ ├── foobar_ca.srl │ │ ├── foobar_ca.pem │ │ ├── another_ca.pem │ │ ├── another_ca_key.pem │ │ └── foobar_ca_key.pem │ ├── intermediate_certificates │ │ ├── intermediate_cert.srl │ │ ├── intermediate_cert.csr │ │ ├── intermediate_cert.pem │ │ └── intermediate_cert_key.pem │ ├── intermediate_ca.ext │ ├── install_faketime.sh │ ├── leaf_certificates │ │ ├── localhost.csr │ │ ├── localhost2.csr │ │ ├── wrong.host.csr │ │ ├── unknown_ca.pem │ │ ├── wrong.host.pem │ │ ├── expired_certificate.pem │ │ ├── good_certificate.pem │ │ ├── future_certificate.pem │ │ ├── good_certificate_for_localhost2.pem │ │ ├── good_certificate_with_intermediate_ca.pem │ │ ├── self_signed.pem │ │ ├── localhost_key.pem │ │ ├── localhost2_key.pem │ │ ├── self_signed_key.pem │ │ └── wrong.host_key.pem │ ├── CA_stores │ │ └── foobar.pem │ ├── certificate_chains │ │ └── misordered_chain.pem │ └── Makefile ├── tls_certificate_check_dependent_app_SUITE_data │ └── dependent_app │ │ ├── apps │ │ ├── tls_certificate_check │ │ │ ├── src │ │ │ ├── rebar.lock │ │ │ ├── rebar.config │ │ │ └── rebar.config.script │ │ └── dependent_app │ │ │ ├── src │ │ │ ├── dependent_app.erl │ │ │ └── dependent_app.app.src │ │ │ └── rebar.config │ │ ├── .gitignore │ │ └── rebar.lock ├── tls_certificate_check_dependent_app_SUITE.erl ├── tls_certificate_check_hardcoded_authorities_hotswap_SUITE.erl ├── tls_certificate_check_cross_signing_SUITE.erl ├── tls_certificate_check_override_SUITE.erl ├── tls_certificate_check_test_utils.erl └── tls_certificate_check_options_SUITE.erl ├── .github ├── dependabot.yml └── workflows │ ├── otp_version_to_rebar3_version.json │ ├── hardcoded-authorities-updater.yml │ ├── hardcoded-authorities-updater.sh │ └── ci.yml ├── .gitignore ├── rebar.lock ├── src ├── tls_certificate_check.app.src ├── tls_certificate_check_app.erl ├── tls_certificate_check_sup.erl ├── tls_certificate_check_util.erl ├── tls_certificate_check.erl └── tls_certificate_check_shared_state.erl ├── LICENSE ├── Makefile ├── rebar.config.script ├── rebar.config ├── README.md ├── CHANGELOG.md └── util └── tls_certificate_check_hardcoded_authorities_updater.erl /tmp/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /shell.config: -------------------------------------------------------------------------------- 1 | [ 2 | {kernel, 3 | [{logger_level, debug} 4 | ]} 5 | ]. 6 | -------------------------------------------------------------------------------- /test/cross_signing/CAs/new_ca.srl: -------------------------------------------------------------------------------- 1 | 0241E2640351C510034A4F7440CE88B892D0704C 2 | -------------------------------------------------------------------------------- /test/cross_signing/install_faketime.sh: -------------------------------------------------------------------------------- 1 | ../common_scenarios/install_faketime.sh -------------------------------------------------------------------------------- /test/cross_signing/intermediate_ca.ext: -------------------------------------------------------------------------------- 1 | ../common_scenarios/intermediate_ca.ext -------------------------------------------------------------------------------- /test/cross_signing/CAs/expired_ca.srl: -------------------------------------------------------------------------------- 1 | 65F628749E7B498406EF0EE5722E5F274D9247AB 2 | -------------------------------------------------------------------------------- /test/cross_signing/CAs/third_ca.srl: -------------------------------------------------------------------------------- 1 | 0A9084EF6D7305CA3DC3503730B9D714E41E574E 2 | -------------------------------------------------------------------------------- /test/common_scenarios/CAs/another_ca.srl: -------------------------------------------------------------------------------- 1 | 7DCA3E778655938C3BE2C69F1B8D8A1F3A6D2B0C 2 | -------------------------------------------------------------------------------- /test/common_scenarios/CAs/foobar_ca.srl: -------------------------------------------------------------------------------- 1 | 1310676507EDC4DE531FA4AEB79B28C02B2CF57C 2 | -------------------------------------------------------------------------------- /test/common_scenarios/intermediate_certificates/intermediate_cert.srl: -------------------------------------------------------------------------------- 1 | 446B2F0155D544F588161474696AD3B1580031DC 2 | -------------------------------------------------------------------------------- /test/tls_certificate_check_dependent_app_SUITE_data/dependent_app/apps/tls_certificate_check/src: -------------------------------------------------------------------------------- 1 | ../../../../../src -------------------------------------------------------------------------------- /test/cross_signing/intermediate_certificates/regular_intermediate_cert.srl: -------------------------------------------------------------------------------- 1 | 448322F6147B49F6C3E1F357F312FD55DACD171F 2 | -------------------------------------------------------------------------------- /test/tls_certificate_check_dependent_app_SUITE_data/dependent_app/apps/tls_certificate_check/rebar.lock: -------------------------------------------------------------------------------- 1 | ../../../../../rebar.lock -------------------------------------------------------------------------------- /test/tls_certificate_check_dependent_app_SUITE_data/dependent_app/apps/tls_certificate_check/rebar.config: -------------------------------------------------------------------------------- 1 | ../../../../../rebar.config -------------------------------------------------------------------------------- /test/tls_certificate_check_dependent_app_SUITE_data/dependent_app/apps/tls_certificate_check/rebar.config.script: -------------------------------------------------------------------------------- 1 | ../../../../../rebar.config.script -------------------------------------------------------------------------------- /test/tls_certificate_check_dependent_app_SUITE_data/dependent_app/apps/dependent_app/src/dependent_app.erl: -------------------------------------------------------------------------------- 1 | -module(dependent_app). 2 | 3 | -export([]). 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | reviewers: 6 | - "g-andrade" 7 | schedule: 8 | interval: "monthly" 9 | -------------------------------------------------------------------------------- /.github/workflows/otp_version_to_rebar3_version.json: -------------------------------------------------------------------------------- 1 | { 2 | "22.3": "3.18", 3 | "23.3": "3.19", 4 | "24.3": "3.23", 5 | "25.3": "3.24", 6 | "26.2": "3.25", 7 | "27.3": "3.25", 8 | "28.1": "3.25" 9 | } 10 | -------------------------------------------------------------------------------- /test/tls_certificate_check_dependent_app_SUITE_data/dependent_app/apps/dependent_app/rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info]}. 2 | 3 | {deps, [ 4 | {tls_certificate_check, {git, "https://github.com/g-andrade/DUMMY_REPO_ADDRESS.git"}} 5 | ]}. 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | deps 12 | log 13 | erl_crash.dump 14 | .rebar 15 | logs 16 | _build 17 | .idea 18 | *.iml 19 | rebar3.crashdump 20 | /doc/ 21 | -------------------------------------------------------------------------------- /test/tls_certificate_check_dependent_app_SUITE_data/dependent_app/.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _build 3 | _checkouts 4 | _vendor 5 | .eunit 6 | *.o 7 | *.beam 8 | *.plt 9 | *.swp 10 | *.swo 11 | .erlang.cookie 12 | ebin 13 | log 14 | erl_crash.dump 15 | .rebar 16 | logs 17 | .idea 18 | *.iml 19 | rebar3.crashdump 20 | *~ 21 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.7">>},0}]}. 3 | [ 4 | {pkg_hash,[ 5 | {<<"ssl_verify_fun">>, <<"354C321CF377240C7B8716899E182CE4890C5938111A1296ADD3EC74CF1715DF">>}]}, 6 | {pkg_hash_ext,[ 7 | {<<"ssl_verify_fun">>, <<"FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8">>}]} 8 | ]. 9 | -------------------------------------------------------------------------------- /test/tls_certificate_check_dependent_app_SUITE_data/dependent_app/rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.7">>},0}]}. 3 | [ 4 | {pkg_hash,[ 5 | {<<"ssl_verify_fun">>, <<"354C321CF377240C7B8716899E182CE4890C5938111A1296ADD3EC74CF1715DF">>}]}, 6 | {pkg_hash_ext,[ 7 | {<<"ssl_verify_fun">>, <<"FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8">>}]} 8 | ]. 9 | -------------------------------------------------------------------------------- /test/tls_certificate_check_dependent_app_SUITE_data/dependent_app/apps/dependent_app/src/dependent_app.app.src: -------------------------------------------------------------------------------- 1 | {application, dependent_app, 2 | [{description, "An OTP library"}, 3 | {vsn, "0.1.0"}, 4 | {registered, []}, 5 | {applications, 6 | [kernel, 7 | stdlib, 8 | tls_certificate_check 9 | ]}, 10 | {env,[]}, 11 | {modules, []}, 12 | 13 | {licenses, ["Apache-2.0"]}, 14 | {links, []} 15 | ]}. 16 | -------------------------------------------------------------------------------- /test/common_scenarios/intermediate_ca.ext: -------------------------------------------------------------------------------- 1 | # From: https://stackoverflow.com/questions/52500165/problem-verifying-a-self-created-openssl-root-intermediate-and-end-user-certifi 2 | 3 | [ v3_intermediate_ca ] 4 | # Extensions for a typical intermediate CA (`man x509v3_config`). 5 | subjectKeyIdentifier = hash 6 | authorityKeyIdentifier = keyid:always,issuer 7 | basicConstraints = critical, CA:true, pathlen:1 8 | keyUsage = critical, digitalSignature, cRLSign, keyCertSign 9 | -------------------------------------------------------------------------------- /test/common_scenarios/install_faketime.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | function is_installed { 6 | which $1 >/dev/null; 7 | } 8 | 9 | 10 | if is_installed faketime; then 11 | echo "faketime already installed" 12 | exit 13 | fi 14 | 15 | if is_installed brew; then 16 | brew install faketime # FIXME untested 17 | exit 18 | fi 19 | 20 | if is_installed apt-get; then 21 | DEBIAN_FRONTEND=noninteractive sudo apt-get --yes install faketime 22 | exit 23 | fi 24 | 25 | >&2 echo "I don't know how to install faketime in your system" 26 | exit 1 27 | -------------------------------------------------------------------------------- /.github/workflows/hardcoded-authorities-updater.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # 3 | # Based on: 4 | # * https://github.com/elixir-mint/castore/tree/v0.1.11/.github/workflows 5 | # 6 | name: Check for updates to hardcoded CAs 7 | on: 8 | schedule: 9 | - cron: '23 10 * * 1-5' 10 | workflow_dispatch: 11 | jobs: 12 | outdated: 13 | name: Check for updates to hardcoded CAs 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v5 17 | - uses: erlef/setup-elixir@v1 18 | with: 19 | ref: 'master' 20 | otp-version: 27.2 21 | rebar3-version: 3.24 22 | - run: | 23 | .github/workflows/hardcoded-authorities-updater.sh 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /src/tls_certificate_check.app.src: -------------------------------------------------------------------------------- 1 | {application, tls_certificate_check, [ 2 | {description, "CA store + Partial chain handler"}, 3 | {vsn, git}, 4 | {registered, [ 5 | tls_certificate_check_shared_state, 6 | tls_certificate_check_sup 7 | ]}, 8 | {mod, {tls_certificate_check_app, []}}, 9 | {applications, [ 10 | crypto, 11 | kernel, 12 | public_key, 13 | ssl, 14 | stdlib, 15 | %% Dependencies outside OTP 16 | ssl_verify_fun 17 | ]}, 18 | {env, [ 19 | % {use_otp_trusted_CAs, true} %% Available on OTP 25+ 20 | ]}, 21 | {modules, []}, 22 | 23 | {licenses, ["MIT"]}, 24 | {links, [ 25 | {"GitHub", "https://github.com/g-andrade/tls_certificate_check"}, 26 | {"GitLab", "https://gitlab.com/g-andrade/tls_certificate_check"} 27 | ]} 28 | ]}. 29 | -------------------------------------------------------------------------------- /test/cross_signing/CAs/new_ca.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICVjCCAT4CAQAwETEPMA0GA1UEAwwGbmV3X2NhMIIBIjANBgkqhkiG9w0BAQEF 3 | AAOCAQ8AMIIBCgKCAQEAzP4iqWv1eA+huMTn7FW3n8MQ21N5lrslPmuW12sCeSpD 4 | Op9Xagr+1ROXg04RaTp+/azZTh8JGX2Aj5iGHCt2LPQqruNfL46QC/tPDpWVQku5 5 | 9bqDduC/TX+YOniRWt2UODPoBJ891njw40MOWmMGYsvbDsHRaRikguvGQESKDUdE 6 | Z97fpR3GynrdEu39IJGJe/HFz/Vq0tsofh61Mub4J0WCyyva8GN6HU78JPYNsk/f 7 | 8smeXVZZCFZTT5Ofmzz3Jbwno5f84Fsps3oTQIqCGzfy8IILNle1tQiaajsBzLp4 8 | BgY6vHxcl/nSwZGsFSo7mt7wxVeFijDTMD54oGMY2wIDAQABoAAwDQYJKoZIhvcN 9 | AQELBQADggEBAHzPSIx5B/C3CSGqJJf4j/xbRIwgEyAl5Qxtb+lZDRA3K48zE95h 10 | z6Z3qxAfq5yXdpkugiO303aUF3w1v+8vnzEJOmdXfMNcA1Yqiu0xEO4SRw8Eetmw 11 | az6oCmW7+0U6hZmvi96YqVW7cJSZPNb4hj3er3yjdKrjHcdv//Bde4ctKi/H9Abe 12 | q9C1maakK4KoV0UD7VC2ETWsipf6c1Tb4Tryh5Sn+KnmN1/IsYTxhOx/AH7W5Z6n 13 | XnTlJN266JSeHkg5JDa/eEYAjPU/EbxZGhnQ084SIfZoctfk8OedaTt4ofQwfyRf 14 | gNg3ZRAkTXxKMo3JwIPXBhdjgbzaHUG2gjA= 15 | -----END CERTIFICATE REQUEST----- 16 | -------------------------------------------------------------------------------- /test/cross_signing/leaf_certificates/localhost.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B 3 | AQEFAAOCAQ8AMIIBCgKCAQEAv6AZDBjBsR3LHNHHAUsUYWhwOwddM/BdCt1F8zVr 4 | uTrs5hwxebUBSaTmQRm6b3+Cuv8NUGxRuHOii/e188/6NioDcUtZ82y+G6HFDd2G 5 | ICj1nBujafv1QaH5TBrtV27HnXQ5KMHi5rLqmTLuBYHC5xtonVJHNzl/Nn3Kfv1C 6 | qChsH4OEQQdcOeZdkQMAXjl4/Av9en+9qOOkYukXqHrBCc9rvE0KqKIBCw8zPGQI 7 | ogHSUN3rA72kXBXIIWKyOnil9rQsJVOhvKqWYMN0Xuwn75iz2dRN5+FPHQ/EcTWd 8 | z8riJPWQjuEIQpmITRzWJzXBsZHiEG3b853Rdx8rhhapjQIDAQABoAAwDQYJKoZI 9 | hvcNAQELBQADggEBAHzSxez3X9tdIuNB4K2jVL1vLMHR0NSRbKECgGFvnXFixkZm 10 | sdsxoPkpj5w43+qUytClwhVu8OVPkU0GYe2FpSqvTSAHOqThawz/zqy+lWe26GQD 11 | nAOgd9IhLxW1pnAvSrmVWdYDzuOpmMQYLfkSc5dbaBZisMVYgsj6OsNYUJh0OAHD 12 | 5VWmPvS7Yd9vCoiYvttIYJl99KY7L5P5HzcxwoMbBHg5UMVkUpgfxTdVw8XmPPd7 13 | 7xyGJAQU4ZAQckOsIXPRlBdGxndPdnZLhMbdJkPHjX0j1k484q1u+PxAgAXvva0i 14 | x9ZjATqITIyB/Mk/qdfabGmqEAJIalLyc220LpE= 15 | -----END CERTIFICATE REQUEST----- 16 | -------------------------------------------------------------------------------- /test/common_scenarios/leaf_certificates/localhost.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B 3 | AQEFAAOCAQ8AMIIBCgKCAQEAqfHNUg64K3AEHo/K/gP38PRk/6OHoigXPYgIt1tt 4 | UqRNBXFd/vyDiy+BZmduYPNiDe50wWToY1pX5lez6IFZLlE2EhU0/svTY/s2o+LU 5 | G9piHIu8XfZ22NeM+dL0MzxKZnIl+O0tjlKtegE0DjmZm4FEyeHW4KlDiu+pznJT 6 | wJm98Y2V8Or95qYOSby/vQRQwB4JvRoImQfE/yELKvS3eYvaqDVy6IPMavweOKyy 7 | xo/WbjqvXs3eLta4lKKIELmShuu0ri9TVpU7C7J6XLRgEKOBENzU74gp2qIJN8d0 8 | xLZdCEXYf7uaQsWRSz3a4kBuXghReNu2XauksJyNuFAzXQIDAQABoAAwDQYJKoZI 9 | hvcNAQELBQADggEBAChviL4gpfsC0faHKDGYStIfHh7icdKcQrHMqaI6aMirf5Mv 10 | 8QWCBBB4+ykyOx7nSUNgCM3wwigRS7cMMseURVQBvmIj5bHga7KoREtSYpUpONw3 11 | cZw3OojB06K/4yLvYCFTIj2FJ+wE3M8HVeUsZeBzfcE1a63I//5uei6rkW3IEfdQ 12 | dc88ciz8xaOICmS7iP9Z/wogIZcRrGJGaaVyV2ksnm7+Qo0pvE+MBnUXjLsBTbk7 13 | NGLTEZtqFxzMGvk83eefLDJEY+oKaoB7ey0HYDq32UdEbE7UFy3J0L7U87BbAEvy 14 | mrUGownU8u5Ctb0M0AZaURlQMLkCRhS7KOsuWLw= 15 | -----END CERTIFICATE REQUEST----- 16 | -------------------------------------------------------------------------------- /test/common_scenarios/leaf_certificates/localhost2.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICWjCCAUICAQAwFTETMBEGA1UEAwwKbG9jYWxob3N0MjCCASIwDQYJKoZIhvcN 3 | AQEBBQADggEPADCCAQoCggEBAO202DtrS/E9JNaDnADtcvkWVJf6xtnJqrWwF+Ao 4 | yBQnj8+GnXAPTLfFus/tLhQKwb34EZu/t2Smhr/Q8Q5C9IvItrpJSqIP/unb+Ocw 5 | plpRtWr12nFVdrpZaRBR55rLSkIat5bFdFatbF0EcgOWMpu7wf5b94UZC8I4zTCb 6 | 54IUs2VY34PrRxXfDBwH39+t1RqfxxA9zwr/docpCgP0d05RvNQqlkc6b3FTtp9f 7 | cCiPr03IdNy+nhTQxQr9n3q+Tz/7sMKt1CdJdviwrT0ugIS9b0KCw875q3F/cr0J 8 | ft8wCp/WnfdPfzsPEUGGEgT0pUeQWiPbFq2G77TRW2EdTZcCAwEAAaAAMA0GCSqG 9 | SIb3DQEBCwUAA4IBAQBIiwQdfuqathk+LEZ4aZ4kECWIB68zfF4FCGcm1LO3PcaV 10 | JLUAJohOkGVNacpVb0h3Q9iLTbJs0QoHBM5nLmqlyhN3SSJgCVEX179gsmf4WPu1 11 | qyaMog/cEVwfU1h6ugGK57ny3Ln2dEsfCxHLkjmBupkC+9A8PFzYjNZ+pJzbTYWb 12 | CnGYNAXlPIKTo0+/JB6qQoQ7M51n1Rj1YgVY1sFyERvB3XCZfd5wKQCBtykXFiSt 13 | pzV7olMgxL7qklpNeQMgCCrMXQESY8dLQD5bxj/g5xsFlhKVaIG0P9BJ7wAGFc9N 14 | 7g7laZlesxjT3LaVPm5NZeHrYugW/rjCPBPOnE6/ 15 | -----END CERTIFICATE REQUEST----- 16 | -------------------------------------------------------------------------------- /test/common_scenarios/leaf_certificates/wrong.host.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICWjCCAUICAQAwFTETMBEGA1UEAwwKd3JvbmcuaG9zdDCCASIwDQYJKoZIhvcN 3 | AQEBBQADggEPADCCAQoCggEBAK/EevQBJ1Hx/fUJKtOnfBPDLcGT0JYPTta6j8+r 4 | Zl/qvL36PpjQbtPT1GW43MQfsdJrPstnt0JsNkyBp+lnkkjwFeA1+1HZw39DxSmA 5 | 3nfs+GioFGeoJZHzFEsuXFaG4dGmP5XRMQNBWTtUfplGdOTsQDcZe+rGGaRr1dY3 6 | eEaLV+mMkIUgBtZKRoawwOrwNI99aor/bCP+oEsaFVvpo+Smi3GicjJh6j1piKoa 7 | 8j07u7uXV0/szVHFucBZ2RtfC2lZiCunxxlDx9gBoKaQqra5bFkqeGYcFMWgICWg 8 | Fo2mOJZqnmAB0V3fptzKdkRq9SmiuDr3q/4mhUlOy0GNFPsCAwEAAaAAMA0GCSqG 9 | SIb3DQEBCwUAA4IBAQB0RybcyT+EmRrlxv8Mb6c6oe01QhSvrovMZHIuAiJsierf 10 | t4pjTSoTjx6XA8RURORttjpTVE8VXwmTUQ9qydlFpLKigSE/GfM29SmsBAA4J39j 11 | pgTdy1XoJxPkKj5hgxZMAVfE+D5l6UaJB9GDVkliLnUHZwJZxfl9O2p5Y4/dKcTh 12 | DDAVaV13CwbsKN6aaQIfPhdsOl+0QUMNeqm68NVfUxk7d8SDaYUgmseDECdQ3C2g 13 | r9ZU3TlYCT5oXdJ9++lhCiq9UXlvU2C3H8BzgsONmgX5xLSKoFJXnJJmF/pPzpV0 14 | eYNU1wLLpZSpWuP7m36p1vVZRVha3PrMod4BJ5+I 15 | -----END CERTIFICATE REQUEST----- 16 | -------------------------------------------------------------------------------- /test/common_scenarios/intermediate_certificates/intermediate_cert.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICYTCCAUkCAQAwHDEaMBgGA1UEAwwRaW50ZXJtZWRpYXRlX2NlcnQwggEiMA0G 3 | CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFn7BXO1HQfK9WZHcYMw+B41PTZ4P0 4 | emmsGj4uXdYku9QkpWcsrv+ou+F3+Y0zAyHP3H9RnlOGuIVMqjGjszKlW6dQEBYz 5 | kvyFQCF2E6LvwOt5Y+oXCaAEsWeaDEQBMmALseBSoYaap3IcY+3hC0fpaQvGN7Nj 6 | Q/TZ9zDf/8YGxvyexwCDOI1pylLuSSn9nN/LZNFvPeaZnwLIZQmt0SaduBkOB/PO 7 | oibX8/nPQnfIf9ymEjndLPOmaiL/JfULT4LaIJRq80WEdaHVV+LKa9V406mnCgMJ 8 | brxITNHGAapqJTEoWVVCJAVDL8VZCURrdrAinQVkIGiKVinVL52p8Bh/AgMBAAGg 9 | ADANBgkqhkiG9w0BAQsFAAOCAQEACN1HVLpk9Dc3UpZ6VQzTVvTumuIORBSTP+vi 10 | BZJQA2hGp2bZS0gx2fPN9+P+m6VT9xKZ/5qzEcb/XcZGPc/9+EOFZq0zILnJlgv4 11 | TwDG1gNlvgkysaag35LB7MDhxS7oqZCvr4UdYKQfDzBeXsETCWVAt1uzVpbrKjaw 12 | BA3NbcksCK+4htoyS/UZMgq0fhBvyZIPCA+l4hnieK1bdynBwp4MHhwQS0W6sIgt 13 | +D9wyW29jRMyeFbPEm3d6WrvbFncUYsh1le9s2rfcfL53vv8waCyKvKQgGnyyyjf 14 | jYrihqdGo1CjrXAHXtRIkeUUQGXRlw1x0huYkI6342L1KJnY9g== 15 | -----END CERTIFICATE REQUEST----- 16 | -------------------------------------------------------------------------------- /test/cross_signing/intermediate_certificates/regular_intermediate_cert.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICaTCCAVECAQAwJDEiMCAGA1UEAwwZcmVndWxhcl9pbnRlcm1lZGlhdGVfY2Vy 3 | dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALa453KR+DiM4Lalr/VY 4 | Kk6qSdYnOlVrmQWBesc7cpcIoItLAaw7rj97jllE0zMLOkixZR10b0LuwW+LmvX6 5 | gkXoNI7IquJCxRovkSrucPbgmOrceiE1Xfk+sOB6sKKQd8n1dX3gjBOiFzAdMpQB 6 | wjvkkzoHkUUGaWBO5LBQ5dZ92sISqBV6a5FiEuMy9TPOVhiYSqZmFUxv+8SCG7T9 7 | 3LJR9oI9VOgj252+gTmWMAB86jvryof40KIXc1g1GfTI/aNfMH2aWSunctHUJN1Q 8 | PXnShjLCsghNCmIfeh/EIUqzQ27vi9Jy7G4qGQhNV324ANJcVKXXBG6pG5sJL82H 9 | 1ccCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQAY+63kCxWN3kC97dYsg+krT7qw 10 | 6NV2EYycsqmhQEm8pCM6WbIOiKgJFXu9cEOTZLjkMqQkTVCuHc9SGhV6o7WKEhjX 11 | CGwzpk+KDZo9Ob6SBaEv4RSQPNqpSdHLrWPjqGwK1zsr8JkeiojNhWB5JDZMPAAw 12 | Rm9OYSVFVYLWu/2SYUvGRplhAgEiMYao8c0cVy54X+h6TmB/kGvlzVIlVT0D95vs 13 | PkfgnYEV3suJoQSGH2dx7IO02pMfZZizymCcKig44Pg1/JkmA5gaHJLXLvXJr9UC 14 | HRKAEKgEVYyDfjttQUDPOpz9iChH7v6LUer3Xe2beM7FptR+doKCbLpzuKfX 15 | -----END CERTIFICATE REQUEST----- 16 | -------------------------------------------------------------------------------- /test/common_scenarios/leaf_certificates/unknown_ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICsjCCAZoCFH3KPneGVZOMO+LGnxuNih86bSsMMA0GCSqGSIb3DQEBCwUAMBUx 3 | EzARBgNVBAMMCmFub3RoZXJfY2EwIBcNMTgwMzIxMTczMDA5WhgPMjA3MzAzMDcx 4 | NzMwMDlaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQAD 5 | ggEPADCCAQoCggEBAKnxzVIOuCtwBB6Pyv4D9/D0ZP+jh6IoFz2ICLdbbVKkTQVx 6 | Xf78g4svgWZnbmDzYg3udMFk6GNaV+ZXs+iBWS5RNhIVNP7L02P7NqPi1BvaYhyL 7 | vF32dtjXjPnS9DM8SmZyJfjtLY5SrXoBNA45mZuBRMnh1uCpQ4rvqc5yU8CZvfGN 8 | lfDq/eamDkm8v70EUMAeCb0aCJkHxP8hCyr0t3mL2qg1cuiDzGr8HjisssaP1m46 9 | r17N3i7WuJSiiBC5kobrtK4vU1aVOwuyely0YBCjgRDc1O+IKdqiCTfHdMS2XQhF 10 | 2H+7mkLFkUs92uJAbl4IUXjbtl2rpLCcjbhQM10CAwEAATANBgkqhkiG9w0BAQsF 11 | AAOCAQEAfNWLm1dRIethVdkF3QDYkrV/4Cm2162b9en1wwDaM5fe7Fl4rrjNm0EY 12 | 2Uxn1UF0oNEHp68gMbNbHfiYJhdzLHYg9Rj3sPMgkfiBbNzvgpvZLe1qeSCVTSGH 13 | 3ADW5K74s9VSVZbTPuSos07pWqWS6SVDJ5tODy1UvlneWN84CVAv9qNA4b4tt3A2 14 | e8+i73UEKV7dcpFHjhMlJyodspSgUa/JWtPAPEPa/LoUOAEZrwEJBoHVmuLeHqMY 15 | bsVMLs8PIg+92yIWNyZbdpGmF+n75y9k0MnWu/IYV6f92JRq9Gowxw7S2yrGvQ0a 16 | xG9e+iKd2AL4kaxh9+EdmzzHWmrrMw== 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /test/common_scenarios/leaf_certificates/wrong.host.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICsjCCAZoCFBMQZ2UH7cTeUx+krrebKMArLPV8MA0GCSqGSIb3DQEBCwUAMBQx 3 | EjAQBgNVBAMMCWZvb2Jhcl9jYTAgFw0xODAzMjExNzMwMDlaGA8yMDczMDMwNzE3 4 | MzAwOVowFTETMBEGA1UEAwwKd3JvbmcuaG9zdDCCASIwDQYJKoZIhvcNAQEBBQAD 5 | ggEPADCCAQoCggEBAK/EevQBJ1Hx/fUJKtOnfBPDLcGT0JYPTta6j8+rZl/qvL36 6 | PpjQbtPT1GW43MQfsdJrPstnt0JsNkyBp+lnkkjwFeA1+1HZw39DxSmA3nfs+Gio 7 | FGeoJZHzFEsuXFaG4dGmP5XRMQNBWTtUfplGdOTsQDcZe+rGGaRr1dY3eEaLV+mM 8 | kIUgBtZKRoawwOrwNI99aor/bCP+oEsaFVvpo+Smi3GicjJh6j1piKoa8j07u7uX 9 | V0/szVHFucBZ2RtfC2lZiCunxxlDx9gBoKaQqra5bFkqeGYcFMWgICWgFo2mOJZq 10 | nmAB0V3fptzKdkRq9SmiuDr3q/4mhUlOy0GNFPsCAwEAATANBgkqhkiG9w0BAQsF 11 | AAOCAQEACL8s8ZsyVtzVAfQpV+Ye+ms4nXgbW/CLqcHy3/59QW8ilB1XIq1D+LlK 12 | iKq4zL9MAuETtwxBQNVZCt+sHfPslBgC3lHaUtE/5hkhf3DuqL7uYb2ztKgkAhIz 13 | X0eCbe+gRD8e/7GwsRYPE9q3exKbLttuEHqxGY29NFJMqlEwu1azN81YiY/Xhp6l 14 | bt9D5EfCxbCTPKqBN5XUJ/wVYJ3bNZT2lCN8mE3t0Qbw79PKA4BnI9TO580JrOz3 15 | 7SAb7LunbU1Nnt20Vv3pi0Y5oWu3UQDvDckLHFvmeUlUrpt415BNP31vmcI7p6Vq 16 | cnrDYROC0MR/RvyO9Rw1/pbCzFy0Cw== 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /test/common_scenarios/leaf_certificates/expired_certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICrzCCAZcCFBMQZ2UH7cTeUx+krrebKMArLPV6MA0GCSqGSIb3DQEBCwUAMBQx 3 | EjAQBgNVBAMMCWZvb2Jhcl9jYTAeFw0xODAzMjExNzMwMDlaFw0yMzAyMjMxNzMw 4 | MDlaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP 5 | ADCCAQoCggEBAKnxzVIOuCtwBB6Pyv4D9/D0ZP+jh6IoFz2ICLdbbVKkTQVxXf78 6 | g4svgWZnbmDzYg3udMFk6GNaV+ZXs+iBWS5RNhIVNP7L02P7NqPi1BvaYhyLvF32 7 | dtjXjPnS9DM8SmZyJfjtLY5SrXoBNA45mZuBRMnh1uCpQ4rvqc5yU8CZvfGNlfDq 8 | /eamDkm8v70EUMAeCb0aCJkHxP8hCyr0t3mL2qg1cuiDzGr8HjisssaP1m46r17N 9 | 3i7WuJSiiBC5kobrtK4vU1aVOwuyely0YBCjgRDc1O+IKdqiCTfHdMS2XQhF2H+7 10 | mkLFkUs92uJAbl4IUXjbtl2rpLCcjbhQM10CAwEAATANBgkqhkiG9w0BAQsFAAOC 11 | AQEAplxP3UfupX3IaHKhvVTEYkXyO2tjfKB6wSjGfxuACUoLxLwzGrf6bAwzB94H 12 | LY9VDuomxPT/sFt52dXellT54ax39tiGSEi0Nk8HTCu0wnQSqONDA01faitu/HvC 13 | 6VzyS0gXbUKb+B71aws9JYyZ0qycTBVtLEydfx2tHRD0MigRoGeV6UZ1//YV0N/9 14 | ukfE7KwcCArRY5hI8jZwINcX8pIrbAYTqhm3tgiLX+/QJnanjyloTVK7mQeECg5S 15 | +/Sj1NX+sE0bFL8dg+K6khQCgK9G+aXaqoUt1hIbVfqQksepCyeL5Li0obR14DHF 16 | 6NgDvKj8bra/L4osAMP5NOirww== 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /test/common_scenarios/leaf_certificates/good_certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICsTCCAZkCFBMQZ2UH7cTeUx+krrebKMArLPV5MA0GCSqGSIb3DQEBCwUAMBQx 3 | EjAQBgNVBAMMCWZvb2Jhcl9jYTAgFw0xODAzMjExNzMwMDlaGA8yMDczMDMwNzE3 4 | MzAwOVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOC 5 | AQ8AMIIBCgKCAQEAqfHNUg64K3AEHo/K/gP38PRk/6OHoigXPYgIt1ttUqRNBXFd 6 | /vyDiy+BZmduYPNiDe50wWToY1pX5lez6IFZLlE2EhU0/svTY/s2o+LUG9piHIu8 7 | XfZ22NeM+dL0MzxKZnIl+O0tjlKtegE0DjmZm4FEyeHW4KlDiu+pznJTwJm98Y2V 8 | 8Or95qYOSby/vQRQwB4JvRoImQfE/yELKvS3eYvaqDVy6IPMavweOKyyxo/Wbjqv 9 | Xs3eLta4lKKIELmShuu0ri9TVpU7C7J6XLRgEKOBENzU74gp2qIJN8d0xLZdCEXY 10 | f7uaQsWRSz3a4kBuXghReNu2XauksJyNuFAzXQIDAQABMA0GCSqGSIb3DQEBCwUA 11 | A4IBAQAPdj6gydr1UkVGGmk72M0DZOMmX+Wdd0921/3YuwUAFB58YusGaRpqH2az 12 | Gk4a7aPrs+otmzFq1SCpG7UFzp9y20EQ+TL7NZ3s9lOlfc1y+3YhFhxs91gHdGeb 13 | ESrf51QDFayGUy0lvEK6gJ2RzDfJybkDs7L67egeBiA+YiCRdKHklV0Nn0sCHb0T 14 | hDy9ouSlMTAzQ3aYxbQFz6W0n/fs504+PsQ4uulVmJAAJ/bVIthXkdTpfMuZ4QOH 15 | BxgagYumfBpIEbeNGyGoa9kd5amKNRJkZHuZAY3V41hxAmNgb7PHo/kOyA0IbdkM 16 | 7cMd9mFtAJawmr53JE1l6Fj2ZNsF 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /test/common_scenarios/leaf_certificates/future_certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICszCCAZsCFBMQZ2UH7cTeUx+krrebKMArLPV7MA0GCSqGSIb3DQEBCwUAMBQx 3 | EjAQBgNVBAMMCWZvb2Jhcl9jYTAiGA8yMDczMDMwNzE3MzAwOVoYDzIwNzgwMjA5 4 | MTczMDA5WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUA 5 | A4IBDwAwggEKAoIBAQCp8c1SDrgrcAQej8r+A/fw9GT/o4eiKBc9iAi3W21SpE0F 6 | cV3+/IOLL4FmZ25g82IN7nTBZOhjWlfmV7PogVkuUTYSFTT+y9Nj+zaj4tQb2mIc 7 | i7xd9nbY14z50vQzPEpmciX47S2OUq16ATQOOZmbgUTJ4dbgqUOK76nOclPAmb3x 8 | jZXw6v3mpg5JvL+9BFDAHgm9GgiZB8T/IQsq9Ld5i9qoNXLog8xq/B44rLLGj9Zu 9 | Oq9ezd4u1riUoogQuZKG67SuL1NWlTsLsnpctGAQo4EQ3NTviCnaogk3x3TEtl0I 10 | Rdh/u5pCxZFLPdriQG5eCFF427Zdq6SwnI24UDNdAgMBAAEwDQYJKoZIhvcNAQEL 11 | BQADggEBAIVAeO2ysoUD4IgO9GtUYsm7SBDK+Vn4KNBWTSg73O+lOgRcNjs+GEAc 12 | lea1KmHXO7TkSdIzzV1OkQQ4RthdlNcgc81fQTvk/ATtdhdYPKx/luj52YVlx+09 13 | 3apQmVn6abaVSvPBgObMV5DBK9NFPEDj8cNjk/+hntNBp4xb+3LJyExH5PbY2c7i 14 | eUMPhkXsyDNxtLQU2GM9MsWoP9R/6rzRbRp2q/u+f5vKTz6qCjeOluR99oxrWpo7 15 | LONFCwbxlXWLRGvJKeiNxYa3V6yTW6u3i3hm1uREm4kTZWPJvwuxQNTbchXVmqus 16 | Bab4Tdq8JYMLW0QLls01B6dW2pjCRxs= 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /test/cross_signing/leaf_certificates/localhost.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICwTCCAakCFESDIvYUe0n2w+HzV/MS/VXazRcfMA0GCSqGSIb3DQEBCwUAMCQx 3 | IjAgBgNVBAMMGXJlZ3VsYXJfaW50ZXJtZWRpYXRlX2NlcnQwIBcNMTYwOTAzMTQz 4 | MjQyWhgPMjA3MTA4MjExNDMyNDJaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIw 5 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL+gGQwYwbEdyxzRxwFLFGFocDsH 6 | XTPwXQrdRfM1a7k67OYcMXm1AUmk5kEZum9/grr/DVBsUbhzoov3tfPP+jYqA3FL 7 | WfNsvhuhxQ3dhiAo9Zwbo2n79UGh+Uwa7Vdux510OSjB4uay6pky7gWBwucbaJ1S 8 | Rzc5fzZ9yn79QqgobB+DhEEHXDnmXZEDAF45ePwL/Xp/vajjpGLpF6h6wQnPa7xN 9 | CqiiAQsPMzxkCKIB0lDd6wO9pFwVyCFisjp4pfa0LCVTobyqlmDDdF7sJ++Ys9nU 10 | TefhTx0PxHE1nc/K4iT1kI7hCEKZiE0c1ic1wbGR4hBt2/Od0XcfK4YWqY0CAwEA 11 | ATANBgkqhkiG9w0BAQsFAAOCAQEAGawVTKk+Igydwk8fmI6unqYZTfCLCEP5BcJR 12 | 2JMu/xg2/xZcA8XuO28ML6DgwCvXOQEyNpJK5Uej0mhR0syDRtfDmRONiq/Nep9T 13 | KPDs7bATF/cgath2ZyYbTRZioZ4W8tdJeN2umPHqmdVCY8KjVJaJs7Yopz0qoenI 14 | ZfLjTcnHALiOOQv5FD0iEa0yK1hItCz0Pr/48Nj0hO/7+BdRYldiyA1f3Dc8zSuk 15 | n8n1S3PWCEg3FBeIFl7FNPbdJNpj4U9cDpeMDF5t227h6K71fxvj8og3qyhwp26W 16 | 5RZZe9Ub8mZZYcr2NxuLEN14WIYPN1yyLfEDPOdoI2DFgxH2kA== 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /test/common_scenarios/leaf_certificates/good_certificate_for_localhost2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICsjCCAZoCFBMQZ2UH7cTeUx+krrebKMArLPV3MA0GCSqGSIb3DQEBCwUAMBQx 3 | EjAQBgNVBAMMCWZvb2Jhcl9jYTAgFw0xODAzMjExNzMwMDlaGA8yMDczMDMwNzE3 4 | MzAwOVowFTETMBEGA1UEAwwKbG9jYWxob3N0MjCCASIwDQYJKoZIhvcNAQEBBQAD 5 | ggEPADCCAQoCggEBAO202DtrS/E9JNaDnADtcvkWVJf6xtnJqrWwF+AoyBQnj8+G 6 | nXAPTLfFus/tLhQKwb34EZu/t2Smhr/Q8Q5C9IvItrpJSqIP/unb+OcwplpRtWr1 7 | 2nFVdrpZaRBR55rLSkIat5bFdFatbF0EcgOWMpu7wf5b94UZC8I4zTCb54IUs2VY 8 | 34PrRxXfDBwH39+t1RqfxxA9zwr/docpCgP0d05RvNQqlkc6b3FTtp9fcCiPr03I 9 | dNy+nhTQxQr9n3q+Tz/7sMKt1CdJdviwrT0ugIS9b0KCw875q3F/cr0Jft8wCp/W 10 | nfdPfzsPEUGGEgT0pUeQWiPbFq2G77TRW2EdTZcCAwEAATANBgkqhkiG9w0BAQsF 11 | AAOCAQEAXpOA44p9uz7pNPZbqHWEi5gf0412ghJpt8GkQkAdSsLmFxHM0AXFUvOC 12 | p8GED2c9N99i+9k9GTazQq7RoFlQVowBhQzIqNXgk1q7uB3uqVFC/GtGa7n8O0DM 13 | APf3S3icJ5ec8qrMLcDBpxl/PzTGhAMfS6kmRu9T4q9DxWFjNR2+mQVJm88MxdNG 14 | qoo0fk7FOh+rpOpBl66GXx/lkVdJHZYVEt/yaERZNX6J3cXiZ8oOonXTrIlQZdVC 15 | qCUJC+zx/ZZdqqUIG8wLAjuLkEyoHa/pB/SeHtEmSFapVM71+n65x5oDpZgzJnYr 16 | 8MvmIGidNFdn/WqJOMG7i0ZGi0zapQ== 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /test/common_scenarios/leaf_certificates/good_certificate_with_intermediate_ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICuTCCAaECFERrLwFV1UT1iBYUdGlq07FYADHcMA0GCSqGSIb3DQEBCwUAMBwx 3 | GjAYBgNVBAMMEWludGVybWVkaWF0ZV9jZXJ0MCAXDTE4MDMyMTE3MzAwOVoYDzIw 4 | NzMwMzA3MTczMDA5WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3 5 | DQEBAQUAA4IBDwAwggEKAoIBAQCp8c1SDrgrcAQej8r+A/fw9GT/o4eiKBc9iAi3 6 | W21SpE0FcV3+/IOLL4FmZ25g82IN7nTBZOhjWlfmV7PogVkuUTYSFTT+y9Nj+zaj 7 | 4tQb2mIci7xd9nbY14z50vQzPEpmciX47S2OUq16ATQOOZmbgUTJ4dbgqUOK76nO 8 | clPAmb3xjZXw6v3mpg5JvL+9BFDAHgm9GgiZB8T/IQsq9Ld5i9qoNXLog8xq/B44 9 | rLLGj9ZuOq9ezd4u1riUoogQuZKG67SuL1NWlTsLsnpctGAQo4EQ3NTviCnaogk3 10 | x3TEtl0IRdh/u5pCxZFLPdriQG5eCFF427Zdq6SwnI24UDNdAgMBAAEwDQYJKoZI 11 | hvcNAQELBQADggEBACmCdCUKnYd+Lc1NpTuCAvy7P4XrRzWkelrRrgAgCLN3zyQv 12 | zzW2XAAFIDmcdCuQ1D5A4i14tkDsz8Z3uY1lyjDqSDa+9ClHUt6G7FWhYj5FFTCK 13 | g7jTF/6bAawgBKPD0+RA1PXcMiKllvH5KRCrb1ZYGk8TsUhshMWBDXZQlu0S5XNF 14 | pa64nDc3emxmvRKYtUF56ModNbv36BjgXgPVYgGUP62prXyitho2JtAvNTkD/T22 15 | sbYOoGnzYEeHbr2SYXMLtk0O1/VnR2PQvHF2JL9lzGGK/ZJn1gdvZh7PGjjaze/o 16 | Y1p/hMQPAWM+/BuShdlRM9sc7zG8ABLw6VWK0aU= 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2024 Guilherme Andrade 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/cross_signing/CAs/new_ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDBTCCAe2gAwIBAgIUUh7Wle+HkdLgBNjUj0LBIHS3I+owDQYJKoZIhvcNAQEL 3 | BQAwETEPMA0GA1UEAwwGbmV3X2NhMCAXDTE2MDkwMzE0MzI0MloYDzIwNzEwODIx 4 | MTQzMjQyWjARMQ8wDQYDVQQDDAZuZXdfY2EwggEiMA0GCSqGSIb3DQEBAQUAA4IB 5 | DwAwggEKAoIBAQDM/iKpa/V4D6G4xOfsVbefwxDbU3mWuyU+a5bXawJ5KkM6n1dq 6 | Cv7VE5eDThFpOn79rNlOHwkZfYCPmIYcK3Ys9Cqu418vjpAL+08OlZVCS7n1uoN2 7 | 4L9Nf5g6eJFa3ZQ4M+gEnz3WePDjQw5aYwZiy9sOwdFpGKSC68ZARIoNR0Rn3t+l 8 | HcbKet0S7f0gkYl78cXP9WrS2yh+HrUy5vgnRYLLK9rwY3odTvwk9g2yT9/yyZ5d 9 | VlkIVlNPk5+bPPclvCejl/zgWymzehNAioIbN/Lwggs2V7W1CJpqOwHMungGBjq8 10 | fFyX+dLBkawVKjua3vDFV4WKMNMwPnigYxjbAgMBAAGjUzBRMB0GA1UdDgQWBBRJ 11 | D5qLvtvZtUaYHZ5uZBgXs/hLLDAfBgNVHSMEGDAWgBRJD5qLvtvZtUaYHZ5uZBgX 12 | s/hLLDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCukG2e72B7 13 | G09eLSfO9Sd9Pwa70BG1IIUNcihVLqNe6Go4qbAg/xcxMoDNTVotdt6y36Mc3A7R 14 | IqmxPyCgHiZh4O4CwufUVRU9rILklGeqSs9BSzVSkfOnQ9oWsPivCYGTR0teQwBi 15 | KDBZgsFHUs2y2qtByg9PnYRxfMnK24gDa5YuQjaRNGrNU0O5kh1Ftan480SyX92x 16 | nr3wa3Wda2SXdTiG0LOPDZkO4xcpfoo5Yag5AAKfaP/jNDSPHl9WOUwVTkQh58Nf 17 | QrzVwLBa6MU/To2rr0SNTs1RWA0t+r+SeicbJ8OONBmohx4Bx5Arx40awkOOQQRd 18 | GDEVuqHO1qCW 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/cross_signing/CAs/expired_ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDCzCCAfOgAwIBAgIUULN9acegycFzZiIPAUW+25v7dNUwDQYJKoZIhvcNAQEL 3 | BQAwFTETMBEGA1UEAwwKRXhwaXJlZCBDQTAeFw0xNjA5MDMxNDMyNDJaFw0yMTA4 4 | MDgxNDMyNDJaMBUxEzARBgNVBAMMCkV4cGlyZWQgQ0EwggEiMA0GCSqGSIb3DQEB 5 | AQUAA4IBDwAwggEKAoIBAQDhZeZL+Lo1t6yoi25Iysioh1NFuZFqnK2DETf/lRsg 6 | 9BjaBC9gsCIoZ6Nc8Oe5ff26AmfWyvmvkbFR22JIcA7e4MxcdeFEnT2zkWKWIbFa 7 | SR+IqMaOhRFu7NRnsTgAhqBuP9orc5q785tLvq+B91H+wpaQ7md2mwpqkqhhMWwP 8 | g5I1y5Ey6QaGHGZPPUSGSRgP/FUC6x1Q0PlzYL3mxUGYtzdl5wDo1Y+y5nPgwHQU 9 | lqkXtWQ60nXerRBzGs6wkwjrxQJuG7rQp7Wo2uoQvawV/mT5rWDgqOXGVvngkFIP 10 | cTX+G10URFNLdrQfepWDjUjFHvJjcQKaDK4gGx25UEapAgMBAAGjUzBRMB0GA1Ud 11 | DgQWBBTge98vcJx5uizDUCCbJjlxjM6ESDAfBgNVHSMEGDAWgBTge98vcJx5uizD 12 | UCCbJjlxjM6ESDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBg 13 | g+1ilKYIaEaS2d743IjmlZkUkhHevmubLnjRcYHMOUlUpvse8Tv0xg5IATzq4Ol8 14 | W1x6zjXojVPt0cU7m4z0bP5+Ogy8rovHKCDWZpeYtpfbl41nnEZBB2Fk/CULCDSf 15 | h/M++AnGDJtrutldD7/kjrFoFpfOEyx0UJUl5dBAl+sodAZMs5ERULdCcH433cnA 16 | Hc1BcMKgHyLTVRoCGv5eNhqol+5bnHO4Aj1RiXdwBUT905+E5rc0Ki2NkFgPrs3f 17 | T2gYkFE6Ky2OR+zQcd9owlSMvuqm/7i8WatZ9MzhVjPoBsxJ8ClrD60zXGomaUfF 18 | y/W3vrRwiumzbknnenv+ 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/cross_signing/CAs/third_ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDCTCCAfGgAwIBAgIUR2EOt5xqv7bIZq1GbCKRPv4S2LswDQYJKoZIhvcNAQEL 3 | BQAwEzERMA8GA1UEAwwIdGhpcmRfY2EwIBcNMTYwOTAzMTQzMjQyWhgPMjA3MTA4 4 | MjExNDMyNDJaMBMxETAPBgNVBAMMCHRoaXJkX2NhMIIBIjANBgkqhkiG9w0BAQEF 5 | AAOCAQ8AMIIBCgKCAQEAno5PkBvpIDT8G5It/LNVWH/LT2HixlS7paP7ZDbS5ZVg 6 | yw2FN4tVX/AF/f/SW3F4bblJZOCrD38aLz2b/o4uxH41ObKisYdp76UBUwuuFZx6 7 | vzTaiwW8ovy/VQYHha8/PlePBKKbuVqfzni88qSaaxFV4UV6xWrxlJusV/TGOPsa 8 | hUBdBBqr7XvULRWnvdqMHm1dCRqpbJUAvCxfIf5p1jox4sRFXMMktOjVN+768vcI 9 | MaolMA7qlhJJyvdoy7PzZvo7TsQzbP26Cri17D6dFxx6fCRLn5DaFD/rJZr69qoH 10 | WF/58mezaAiUcQs5HOkqYQ85VeqgRn1kJkzg/ashnwIDAQABo1MwUTAdBgNVHQ4E 11 | FgQUED/UkSxSXBjQx7n5rd9grRZbRrwwHwYDVR0jBBgwFoAUED/UkSxSXBjQx7n5 12 | rd9grRZbRrwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAN5Bx 13 | 6PsVN4h2rhIrOaMpSVrGQ08bYdjX6JQrbFcAdw0I6CmIGyiRlc2K6HQcjeym6afD 14 | ojkkuAbV6ueOWorlh1Hv86daejN04pvKBCJx3nkyMGtTo6JUh0i6V+DjOmMzb5u0 15 | 6rgZDa6CPxinUPzKfU8nqvKUXcmCmHIkTCPSg2CUBQPw9SZpxC8Hvfgw5eM/DQRk 16 | D1xzqZDx27MZk8aym6CM1DF4ir6FIMH06HOodtUbJ3XFn/s8ZWEB8BgRAcFFeLO8 17 | RDUiWhhJKYztcLYeG0D0pGeCvw1LF43LnVM/dZTSQtYyvfhBquWe7Zeu0C+sGXms 18 | tRBDiiwyVoQOpFNU6w== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/common_scenarios/CA_stores/foobar.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDCzCCAfOgAwIBAgIURTXzKFl6PNQU/eSMa0m8SkNHe+YwDQYJKoZIhvcNAQEL 3 | BQAwFDESMBAGA1UEAwwJZm9vYmFyX2NhMCAXDTE4MDMyMTE3MzAwOVoYDzIwNzMw 4 | MzA3MTczMDA5WjAUMRIwEAYDVQQDDAlmb29iYXJfY2EwggEiMA0GCSqGSIb3DQEB 5 | AQUAA4IBDwAwggEKAoIBAQDMNfIQyReoAl9RwGzZGjpi0FM6nICVASOyLJA/IsOp 6 | eKTmM37164UWIaLCNhk8qSjEWrVAfeooJSn+5wL6nCupvHOvm+W9YVwoutUdkpo6 7 | NGQ1lOAFEzhkL8GlfreceyBB/Q3FaTN4LqRCXVXhO2lmco+dThRFlx/JXfDOa98d 8 | B/XdZMBNlbPo+NwoFknwG5sF3ZvMNjMcEqlPrjlv8x6ojsP8ks07OF5woIFGr9np 9 | KLRq7MHB3avs34LAF+EcfiEXSY/SzGPuLq8dUr/lJ4p1v3amxhqqhOfsI67xa8Sx 10 | PXRV2UeJEPc+Kgy+c4xRwcUk9uCbhHdE7XPBzJm088Z7AgMBAAGjUzBRMB0GA1Ud 11 | DgQWBBRdgYv0v0z0Ig4rJFNIfCVkc4DJBDAfBgNVHSMEGDAWgBRdgYv0v0z0Ig4r 12 | JFNIfCVkc4DJBDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCm 13 | 86+NrjW36v9IpI1g8zVPUaGB1naGyLOa2870j44eV/geVB5FeMunIXcbs2kxVH8J 14 | fF9Trd7NbWrS3NoZPeBJkMMDYyOy6bkMHm78B4s1s5wpzoRpbdZvfcXimZieQ7iQ 15 | W89rdE8qdihqW3RN9px/O19BgUpVNvbxrPo/6oNphnl3axqLSYvSQg1QYtuHMmoN 16 | joOCMPRv+CzqyQCbwfBAsO83JbPWRamk7zVW4c68WqBi412O/ncvFI5TZVuJ5tML 17 | e/tREDZouMC67klWJLQWmZmvU60+icChQGadeini551bYb8bAjxo6BlcIcnbEEaR 18 | Bff2gqkp/BZuBUPJj+7T 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/common_scenarios/CAs/foobar_ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDCzCCAfOgAwIBAgIURTXzKFl6PNQU/eSMa0m8SkNHe+YwDQYJKoZIhvcNAQEL 3 | BQAwFDESMBAGA1UEAwwJZm9vYmFyX2NhMCAXDTE4MDMyMTE3MzAwOVoYDzIwNzMw 4 | MzA3MTczMDA5WjAUMRIwEAYDVQQDDAlmb29iYXJfY2EwggEiMA0GCSqGSIb3DQEB 5 | AQUAA4IBDwAwggEKAoIBAQDMNfIQyReoAl9RwGzZGjpi0FM6nICVASOyLJA/IsOp 6 | eKTmM37164UWIaLCNhk8qSjEWrVAfeooJSn+5wL6nCupvHOvm+W9YVwoutUdkpo6 7 | NGQ1lOAFEzhkL8GlfreceyBB/Q3FaTN4LqRCXVXhO2lmco+dThRFlx/JXfDOa98d 8 | B/XdZMBNlbPo+NwoFknwG5sF3ZvMNjMcEqlPrjlv8x6ojsP8ks07OF5woIFGr9np 9 | KLRq7MHB3avs34LAF+EcfiEXSY/SzGPuLq8dUr/lJ4p1v3amxhqqhOfsI67xa8Sx 10 | PXRV2UeJEPc+Kgy+c4xRwcUk9uCbhHdE7XPBzJm088Z7AgMBAAGjUzBRMB0GA1Ud 11 | DgQWBBRdgYv0v0z0Ig4rJFNIfCVkc4DJBDAfBgNVHSMEGDAWgBRdgYv0v0z0Ig4r 12 | JFNIfCVkc4DJBDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCm 13 | 86+NrjW36v9IpI1g8zVPUaGB1naGyLOa2870j44eV/geVB5FeMunIXcbs2kxVH8J 14 | fF9Trd7NbWrS3NoZPeBJkMMDYyOy6bkMHm78B4s1s5wpzoRpbdZvfcXimZieQ7iQ 15 | W89rdE8qdihqW3RN9px/O19BgUpVNvbxrPo/6oNphnl3axqLSYvSQg1QYtuHMmoN 16 | joOCMPRv+CzqyQCbwfBAsO83JbPWRamk7zVW4c68WqBi412O/ncvFI5TZVuJ5tML 17 | e/tREDZouMC67klWJLQWmZmvU60+icChQGadeini551bYb8bAjxo6BlcIcnbEEaR 18 | Bff2gqkp/BZuBUPJj+7T 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/common_scenarios/CAs/another_ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDDTCCAfWgAwIBAgIUI1cO2NBI5DxGzMT1Oq8yrk2HjFUwDQYJKoZIhvcNAQEL 3 | BQAwFTETMBEGA1UEAwwKYW5vdGhlcl9jYTAgFw0xODAzMjExNzMwMDlaGA8yMDcz 4 | MDMwNzE3MzAwOVowFTETMBEGA1UEAwwKYW5vdGhlcl9jYTCCASIwDQYJKoZIhvcN 5 | AQEBBQADggEPADCCAQoCggEBAK3ELQVbQmLvf3KSYlmpTUmdcnY1f8/25FnWRqMo 6 | qJl0fJRu2WyFx/MDlITT9pG7zhM7LgbzNqCjOvDGz9/e1UalMbanuXvAAMlScDH4 7 | wyFo4kOnH6f7VuuEce90gatUyyNXXtvJGOEIS9JOoUKqP7tJq1YPJ5W/atGEKjS3 8 | eBrWeCnPo41r2pr7ZcTJX6O41/kR+fR7SweFvbIhqMzuE5WpiPvTMbwRnNyS3HUN 9 | 5OxtFjurYIGnunOOWAGdWepILv4h1cjCC2Ob3lRsOVplyfJmz3KoZRT6bswK+0LR 10 | +eSNiC69mxs1g4DXxXg5wUoO7N8YyJVhq7mVTKIlscol0o0CAwEAAaNTMFEwHQYD 11 | VR0OBBYEFByVw3EsKJ+0vaHTMkpFW3XtYI82MB8GA1UdIwQYMBaAFByVw3EsKJ+0 12 | vaHTMkpFW3XtYI82MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB 13 | AGRhIMZ/H9zwJeanaU35nDZB1vNwn8ntiL8HB8TV5OAzmx1NkGW3iBkkXQprchIS 14 | k5gNybgdE5E1O1faH7KIF5xKTkxurNvGDUAuQ0Nz0WS0kLJR8pgrbCCqGQ2HJUxb 15 | oyCqKGvf85KHzrrXip8XDIZKX8TUOaTo8gqLBSfVdnJi8NcOr1TsZJD1H2LkZnkc 16 | aZ8UksQQ5bREA8JS1kJQ8f8LeYy80LVC/uuFWUu6kOy/F1uddmtHRmW3aOJdAwgN 17 | UPKE1OjoNQLWks1hMTNFPw/2pdvZHF0SoDrVNWHibRnzH1sXnoJU3UC1RV+T0xwI 18 | y5wmJCdt5TAvTQattOnWR7w= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/cross_signing/CA_stores/ca_store1_for_cross_signing.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDBTCCAe2gAwIBAgIUUh7Wle+HkdLgBNjUj0LBIHS3I+owDQYJKoZIhvcNAQEL 3 | BQAwETEPMA0GA1UEAwwGbmV3X2NhMCAXDTE2MDkwMzE0MzI0MloYDzIwNzEwODIx 4 | MTQzMjQyWjARMQ8wDQYDVQQDDAZuZXdfY2EwggEiMA0GCSqGSIb3DQEBAQUAA4IB 5 | DwAwggEKAoIBAQDM/iKpa/V4D6G4xOfsVbefwxDbU3mWuyU+a5bXawJ5KkM6n1dq 6 | Cv7VE5eDThFpOn79rNlOHwkZfYCPmIYcK3Ys9Cqu418vjpAL+08OlZVCS7n1uoN2 7 | 4L9Nf5g6eJFa3ZQ4M+gEnz3WePDjQw5aYwZiy9sOwdFpGKSC68ZARIoNR0Rn3t+l 8 | HcbKet0S7f0gkYl78cXP9WrS2yh+HrUy5vgnRYLLK9rwY3odTvwk9g2yT9/yyZ5d 9 | VlkIVlNPk5+bPPclvCejl/zgWymzehNAioIbN/Lwggs2V7W1CJpqOwHMungGBjq8 10 | fFyX+dLBkawVKjua3vDFV4WKMNMwPnigYxjbAgMBAAGjUzBRMB0GA1UdDgQWBBRJ 11 | D5qLvtvZtUaYHZ5uZBgXs/hLLDAfBgNVHSMEGDAWgBRJD5qLvtvZtUaYHZ5uZBgX 12 | s/hLLDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCukG2e72B7 13 | G09eLSfO9Sd9Pwa70BG1IIUNcihVLqNe6Go4qbAg/xcxMoDNTVotdt6y36Mc3A7R 14 | IqmxPyCgHiZh4O4CwufUVRU9rILklGeqSs9BSzVSkfOnQ9oWsPivCYGTR0teQwBi 15 | KDBZgsFHUs2y2qtByg9PnYRxfMnK24gDa5YuQjaRNGrNU0O5kh1Ftan480SyX92x 16 | nr3wa3Wda2SXdTiG0LOPDZkO4xcpfoo5Yag5AAKfaP/jNDSPHl9WOUwVTkQh58Nf 17 | QrzVwLBa6MU/To2rr0SNTs1RWA0t+r+SeicbJ8OONBmohx4Bx5Arx40awkOOQQRd 18 | GDEVuqHO1qCW 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/common_scenarios/leaf_certificates/self_signed.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDCzCCAfOgAwIBAgIUezNT+E3mev3pLdr8MeFVdLpbK5gwDQYJKoZIhvcNAQEL 3 | BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTE4MDMyMTE3MzAwOVoYDzIwNzMw 4 | MzA3MTczMDA5WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB 5 | AQUAA4IBDwAwggEKAoIBAQD0/c4Q+YWVf5BwSHCA/QcBRMrgGLTBKAWsdTMnOkV+ 6 | TgIEoqYPXzbOlTAL/8iq3UfJh7P2Isv/AJtMnts5tDaloKQs/9FSLiM0RBi1prcQ 7 | lKYcCnFCCCmgLlQYXdp3IXKYSA5T13vPO2LvkvnKCOfGqwGei5jMfn7BCra1/yu8 8 | CEQCIuMAFnQ8LZrXHzg4vmK66WU9yjRBU04ogLHQ3gWB8zt+2LOrdyPATS+Yh1TR 9 | zTuzJrPc5IWRfN8Axn4QuGVwb8VkVS919oT60GCFsBQW5/UcUumKynVTC6MWDjOG 10 | CKkecldZ3MO1kzTvuv9R0qcjt+3fEfKDusKGK1CFFNEZAgMBAAGjUzBRMB0GA1Ud 11 | DgQWBBRPSAj49QChVQr511EqzuQdRtBpgDAfBgNVHSMEGDAWgBRPSAj49QChVQr5 12 | 11EqzuQdRtBpgDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAP 13 | 0iS844gSfqyeEPy5536lol/rM6rF5Sbf/vrMWAphUshJMlWf4+rrcZPhgZ2O4t3Y 14 | W+u/UBKwKkW1oB1D8vFwOHhSA1/NjCsSskcYzzaoCmm22pMB2NRt2/AlytVVfINv 15 | h4X4/Z/UM7tEQvn+kWI1Z2iYts4CIAFOUl3gSlnph11nRUyCChKzt26vs/FChe8p 16 | 771ttZamL07+1vMRTvKvHOPqoRBijlIB64KvnsUs7B0o0l5lsJEWFqolCvYe2K2X 17 | bK6sayLlb8SRtyHAkYjR1yyKHT5GQzmmfOuXmmyWaxvszpcxen+wPusroOondRMf 18 | vsLBgO5P1h1LZW7jzxyG 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/cross_signing/CA_stores/bad_ca_store_for_expiry.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDCzCCAfOgAwIBAgIUULN9acegycFzZiIPAUW+25v7dNUwDQYJKoZIhvcNAQEL 3 | BQAwFTETMBEGA1UEAwwKRXhwaXJlZCBDQTAeFw0xNjA5MDMxNDMyNDJaFw0yMTA4 4 | MDgxNDMyNDJaMBUxEzARBgNVBAMMCkV4cGlyZWQgQ0EwggEiMA0GCSqGSIb3DQEB 5 | AQUAA4IBDwAwggEKAoIBAQDhZeZL+Lo1t6yoi25Iysioh1NFuZFqnK2DETf/lRsg 6 | 9BjaBC9gsCIoZ6Nc8Oe5ff26AmfWyvmvkbFR22JIcA7e4MxcdeFEnT2zkWKWIbFa 7 | SR+IqMaOhRFu7NRnsTgAhqBuP9orc5q785tLvq+B91H+wpaQ7md2mwpqkqhhMWwP 8 | g5I1y5Ey6QaGHGZPPUSGSRgP/FUC6x1Q0PlzYL3mxUGYtzdl5wDo1Y+y5nPgwHQU 9 | lqkXtWQ60nXerRBzGs6wkwjrxQJuG7rQp7Wo2uoQvawV/mT5rWDgqOXGVvngkFIP 10 | cTX+G10URFNLdrQfepWDjUjFHvJjcQKaDK4gGx25UEapAgMBAAGjUzBRMB0GA1Ud 11 | DgQWBBTge98vcJx5uizDUCCbJjlxjM6ESDAfBgNVHSMEGDAWgBTge98vcJx5uizD 12 | UCCbJjlxjM6ESDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBg 13 | g+1ilKYIaEaS2d743IjmlZkUkhHevmubLnjRcYHMOUlUpvse8Tv0xg5IATzq4Ol8 14 | W1x6zjXojVPt0cU7m4z0bP5+Ogy8rovHKCDWZpeYtpfbl41nnEZBB2Fk/CULCDSf 15 | h/M++AnGDJtrutldD7/kjrFoFpfOEyx0UJUl5dBAl+sodAZMs5ERULdCcH433cnA 16 | Hc1BcMKgHyLTVRoCGv5eNhqol+5bnHO4Aj1RiXdwBUT905+E5rc0Ki2NkFgPrs3f 17 | T2gYkFE6Ky2OR+zQcd9owlSMvuqm/7i8WatZ9MzhVjPoBsxJ8ClrD60zXGomaUfF 18 | y/W3vrRwiumzbknnenv+ 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/cross_signing/CA_stores/ca_store2_for_cross_signing.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDCTCCAfGgAwIBAgIUR2EOt5xqv7bIZq1GbCKRPv4S2LswDQYJKoZIhvcNAQEL 3 | BQAwEzERMA8GA1UEAwwIdGhpcmRfY2EwIBcNMTYwOTAzMTQzMjQyWhgPMjA3MTA4 4 | MjExNDMyNDJaMBMxETAPBgNVBAMMCHRoaXJkX2NhMIIBIjANBgkqhkiG9w0BAQEF 5 | AAOCAQ8AMIIBCgKCAQEAno5PkBvpIDT8G5It/LNVWH/LT2HixlS7paP7ZDbS5ZVg 6 | yw2FN4tVX/AF/f/SW3F4bblJZOCrD38aLz2b/o4uxH41ObKisYdp76UBUwuuFZx6 7 | vzTaiwW8ovy/VQYHha8/PlePBKKbuVqfzni88qSaaxFV4UV6xWrxlJusV/TGOPsa 8 | hUBdBBqr7XvULRWnvdqMHm1dCRqpbJUAvCxfIf5p1jox4sRFXMMktOjVN+768vcI 9 | MaolMA7qlhJJyvdoy7PzZvo7TsQzbP26Cri17D6dFxx6fCRLn5DaFD/rJZr69qoH 10 | WF/58mezaAiUcQs5HOkqYQ85VeqgRn1kJkzg/ashnwIDAQABo1MwUTAdBgNVHQ4E 11 | FgQUED/UkSxSXBjQx7n5rd9grRZbRrwwHwYDVR0jBBgwFoAUED/UkSxSXBjQx7n5 12 | rd9grRZbRrwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAN5Bx 13 | 6PsVN4h2rhIrOaMpSVrGQ08bYdjX6JQrbFcAdw0I6CmIGyiRlc2K6HQcjeym6afD 14 | ojkkuAbV6ueOWorlh1Hv86daejN04pvKBCJx3nkyMGtTo6JUh0i6V+DjOmMzb5u0 15 | 6rgZDa6CPxinUPzKfU8nqvKUXcmCmHIkTCPSg2CUBQPw9SZpxC8Hvfgw5eM/DQRk 16 | D1xzqZDx27MZk8aym6CM1DF4ir6FIMH06HOodtUbJ3XFn/s8ZWEB8BgRAcFFeLO8 17 | RDUiWhhJKYztcLYeG0D0pGeCvw1LF43LnVM/dZTSQtYyvfhBquWe7Zeu0C+sGXms 18 | tRBDiiwyVoQOpFNU6w== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/common_scenarios/intermediate_certificates/intermediate_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDJjCCAg6gAwIBAgIUExBnZQftxN5TH6Sut5sowCss9XgwDQYJKoZIhvcNAQEL 3 | BQAwFDESMBAGA1UEAwwJZm9vYmFyX2NhMCAXDTE4MDMyMTE3MzAwOVoYDzIwNzMw 4 | MzA3MTczMDA5WjAcMRowGAYDVQQDDBFpbnRlcm1lZGlhdGVfY2VydDCCASIwDQYJ 5 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMWfsFc7UdB8r1ZkdxgzD4HjU9Nng/R6 6 | aawaPi5d1iS71CSlZyyu/6i74Xf5jTMDIc/cf1GeU4a4hUyqMaOzMqVbp1AQFjOS 7 | /IVAIXYTou/A63lj6hcJoASxZ5oMRAEyYAux4FKhhpqnchxj7eELR+lpC8Y3s2ND 8 | 9Nn3MN//xgbG/J7HAIM4jWnKUu5JKf2c38tk0W895pmfAshlCa3RJp24GQ4H886i 9 | Jtfz+c9Cd8h/3KYSOd0s86ZqIv8l9QtPgtoglGrzRYR1odVX4spr1XjTqacKAwlu 10 | vEhM0cYBqmolMShZVUIkBUMvxVkJRGt2sCKdBWQgaIpWKdUvnanwGH8CAwEAAaNm 11 | MGQwHQYDVR0OBBYEFPYAMZ3gTsH9xw7V2e3z+NBhmb6RMB8GA1UdIwQYMBaAFF2B 12 | i/S/TPQiDiskU0h8JWRzgMkEMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/ 13 | BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IBAQBG0TYq2GgUvhujNly3PfoZwQpgR77Z 14 | d3F68vmgDEPrbJcB8kbmiMUOkMIxRSLs8A9dKJqtE6IU4Wjb9hhLra21eL5m4h5s 15 | SKlTsNnke072HmyGlt8J1i4x9Twqo8CrxxQjpCtlPoYnXK/65oNoTAdxK0IORBES 16 | Kfs6f4JfuFW85gN1dWjp28N4J63FQnGeax78upK0VAddm72KgZhjtiTuRZ4PULe6 17 | 5/svWUzvHAaUBp57qm3CDcLL48F/jOipTASIr8vCYv6uWMNTbLQvdZ9zP1lJwXjd 18 | XDH+1kQGTZN9a7tUflVEBpPLTBqpGTZnewnl9rx5rcQbtB12TSng+7JQ 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/cross_signing/intermediate_certificates/cross_signed_bad_intermediate_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDHDCCAgSgAwIBAgIUZfYodJ57SYQG7w7lci5fJ02SR6swDQYJKoZIhvcNAQEL 3 | BQAwFTETMBEGA1UEAwwKRXhwaXJlZCBDQTAgFw0xNjA5MDMxNDMyNDJaGA8yMDcx 4 | MDgyMTE0MzI0MlowETEPMA0GA1UEAwwGbmV3X2NhMIIBIjANBgkqhkiG9w0BAQEF 5 | AAOCAQ8AMIIBCgKCAQEAzP4iqWv1eA+huMTn7FW3n8MQ21N5lrslPmuW12sCeSpD 6 | Op9Xagr+1ROXg04RaTp+/azZTh8JGX2Aj5iGHCt2LPQqruNfL46QC/tPDpWVQku5 7 | 9bqDduC/TX+YOniRWt2UODPoBJ891njw40MOWmMGYsvbDsHRaRikguvGQESKDUdE 8 | Z97fpR3GynrdEu39IJGJe/HFz/Vq0tsofh61Mub4J0WCyyva8GN6HU78JPYNsk/f 9 | 8smeXVZZCFZTT5Ofmzz3Jbwno5f84Fsps3oTQIqCGzfy8IILNle1tQiaajsBzLp4 10 | BgY6vHxcl/nSwZGsFSo7mt7wxVeFijDTMD54oGMY2wIDAQABo2YwZDAdBgNVHQ4E 11 | FgQUSQ+ai77b2bVGmB2ebmQYF7P4SywwHwYDVR0jBBgwFoAU4HvfL3Ccebosw1Ag 12 | myY5cYzOhEgwEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8EBAMCAYYwDQYJ 13 | KoZIhvcNAQELBQADggEBACc0bdqom82iwDgUarAkQY8n8dKGOHSJHr8YUqg8mF6S 14 | 0lZ+o4RbuNKS0YMOLfGZTY2k+NSWjGrj3+kbxi1kifgmcefCWsM59wHUJRFAFryF 15 | il30BFYj8enwL4pr27LvB0I6WdeIEPci0v3O4/6zrRxtUwfuTWx7BjOJggnZ3I3W 16 | OjEREFXpI7dA6d3bW5pzLVryw2/SsNRF7j+GZyzwKTeQ2nKQshLUuXyNL0Wu5a99 17 | 5h6tYIETM9JrmvKUf8qzMSUjnNmahD1NqLFtWH5dAetyWTzEaf6F0Sg2K/UBfKrs 18 | oCfScrrsWNv+KUW+gIJXkpcTDP1bxqlQvdMqMSyN3QM= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/cross_signing/intermediate_certificates/cross_signed_good_intermediate_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDGjCCAgKgAwIBAgIUCpCE721zBco9w1A3MLnXFOQeV04wDQYJKoZIhvcNAQEL 3 | BQAwEzERMA8GA1UEAwwIdGhpcmRfY2EwIBcNMTYwOTAzMTQzMjQyWhgPMjA3MTA4 4 | MjExNDMyNDJaMBExDzANBgNVBAMMBm5ld19jYTCCASIwDQYJKoZIhvcNAQEBBQAD 5 | ggEPADCCAQoCggEBAMz+Iqlr9XgPobjE5+xVt5/DENtTeZa7JT5rltdrAnkqQzqf 6 | V2oK/tUTl4NOEWk6fv2s2U4fCRl9gI+Yhhwrdiz0Kq7jXy+OkAv7Tw6VlUJLufW6 7 | g3bgv01/mDp4kVrdlDgz6ASfPdZ48ONDDlpjBmLL2w7B0WkYpILrxkBEig1HRGfe 8 | 36Udxsp63RLt/SCRiXvxxc/1atLbKH4etTLm+CdFgssr2vBjeh1O/CT2DbJP3/LJ 9 | nl1WWQhWU0+Tn5s89yW8J6OX/OBbKbN6E0CKghs38vCCCzZXtbUImmo7Acy6eAYG 10 | Orx8XJf50sGRrBUqO5re8MVXhYow0zA+eKBjGNsCAwEAAaNmMGQwHQYDVR0OBBYE 11 | FEkPmou+29m1Rpgdnm5kGBez+EssMB8GA1UdIwQYMBaAFBA/1JEsUlwY0Me5+a3f 12 | YK0WW0a8MBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqG 13 | SIb3DQEBCwUAA4IBAQB/iCTxdh2q6zCZhrOXfmSYeXtviLVdzp8XO/8Qw/MqBK4V 14 | JE5jo7+cmHBP0VZB4Eu1UlaAfIGbsNFGKoWGsTE56NJlvtcEKompJifuMoimLJjM 15 | HvQEfHGhm59UbXSqdirR5Frw2z48vECgHEExczkAFqyNpifN+WMEqqESg6M0gjEl 16 | bs5ioT5VgfPZLplrjexvrghdRCted63MU2JUdFsVk8dmVPS0fmUeYnZpGBq1C5aU 17 | 8VY9gd9jtP3I7Dl3FT/4OtJDrno7CqdZR1+TgawwwhMr7VyMLAIfvz6/LI6Hi32I 18 | d7HUs1C1gIbGJgu16NJJiP4ThQcsbtnugAuqALsw 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/cross_signing/intermediate_certificates/regular_intermediate_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDKzCCAhOgAwIBAgIUAkHiZANRxRADSk90QM6IuJLQcEwwDQYJKoZIhvcNAQEL 3 | BQAwETEPMA0GA1UEAwwGbmV3X2NhMCAXDTE2MDkwMzE0MzI0MloYDzIwNzEwODIx 4 | MTQzMjQyWjAkMSIwIAYDVQQDDBlyZWd1bGFyX2ludGVybWVkaWF0ZV9jZXJ0MIIB 5 | IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtrjncpH4OIzgtqWv9VgqTqpJ 6 | 1ic6VWuZBYF6xztylwigi0sBrDuuP3uOWUTTMws6SLFlHXRvQu7Bb4ua9fqCReg0 7 | jsiq4kLFGi+RKu5w9uCY6tx6ITVd+T6w4HqwopB3yfV1feCME6IXMB0ylAHCO+ST 8 | OgeRRQZpYE7ksFDl1n3awhKoFXprkWIS4zL1M85WGJhKpmYVTG/7xIIbtP3cslH2 9 | gj1U6CPbnb6BOZYwAHzqO+vKh/jQohdzWDUZ9Mj9o18wfZpZK6dy0dQk3VA9edKG 10 | MsKyCE0KYh96H8QhSrNDbu+L0nLsbioZCE1XfbgA0lxUpdcEbqkbmwkvzYfVxwID 11 | AQABo2YwZDAdBgNVHQ4EFgQULNjqrkZoFu/MHvpb8U5wUvxpe8wwHwYDVR0jBBgw 12 | FoAUSQ+ai77b2bVGmB2ebmQYF7P4SywwEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNV 13 | HQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggEBAHGg/UWjmGq9Y3gO4ijaGi15 14 | Y0iuEUonjPqwLOA1bKdvQHux3+cqcOUiYdtb1qYYe+y3gzSUVtz+0cYSa6GwEnvH 15 | 49k0q5ycJGwjkG5L3wRk6WFW2TvHIgW2V+iyxqTapEm1MioaUA3e4AtcaYMSTldr 16 | BLhw1mrVqq45n6vorz7KMMrXyuxZfl1pdXF6xm6fdfapWQCteDV4cyiGwwHK5PGE 17 | 3BGuf+2cq6o5DFUrnXjZiK1mGBKsZCXjvMUpI0oL9253lWUEQFiz0HDeWz5r7147 18 | g9TMmaqU2fUsybAt2eypx2fpMWf4FYD5A2R+uo7DOn4yQK0cmh8UZ0vKdiAwVns= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /.github/workflows/hardcoded-authorities-updater.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | # 6 | # inspired by: 7 | # * https://github.com/elixir-mint/castore/tree/v0.1.11/.github/workflows 8 | # 9 | 10 | git config user.name "GitHub Actions" 11 | git config user.email "actions@users.noreply.github.com" 12 | 13 | BASE_BRANCH=master 14 | git fetch origin 15 | git checkout "${BASE_BRANCH}" 16 | git reset --hard "origin/${BASE_BRANCH}" 17 | git clean -ffdx 18 | 19 | make hardcoded-authorities-update 20 | if [[ -z $(git status -s) ]]; then 21 | # no update 22 | exit 23 | fi 24 | 25 | DATE=$(date -r tmp/cacerts.pem '+%Y/%m/%d') # linux-specific 26 | BRANCH=automation/hardcoded-authorities-update/$DATE 27 | if git branch -a | grep "${BRANCH}" >/dev/null; then 28 | # branch already created 29 | exit 30 | fi 31 | 32 | REMOTE=origin 33 | PR_TITLE="Update bundled CAs to latest as of $DATE" 34 | git checkout -b "$BRANCH" 35 | git add . 36 | git commit -a -m "${PR_TITLE}" 37 | git push "$REMOTE" "$BRANCH" 38 | 39 | PR_LABEL="hardcoded authorities update" 40 | if ! gh pr list --state open --label "$PR_LABEL" | grep "${PR_TITLE}" >/dev/null; then 41 | gh pr create --fill \ 42 | --title "${PR_TITLE}" \ 43 | --body "Stay safe 🔐" \ 44 | --label "${PR_LABEL}" \ 45 | --reviewer "g-andrade" 46 | fi 47 | -------------------------------------------------------------------------------- /test/cross_signing/CAs/expired_ca_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA4WXmS/i6NbesqItuSMrIqIdTRbmRapytgxE3/5UbIPQY2gQv 3 | YLAiKGejXPDnuX39ugJn1sr5r5GxUdtiSHAO3uDMXHXhRJ09s5FiliGxWkkfiKjG 4 | joURbuzUZ7E4AIagbj/aK3Oau/ObS76vgfdR/sKWkO5ndpsKapKoYTFsD4OSNcuR 5 | MukGhhxmTz1EhkkYD/xVAusdUND5c2C95sVBmLc3ZecA6NWPsuZz4MB0FJapF7Vk 6 | OtJ13q0QcxrOsJMI68UCbhu60Ke1qNrqEL2sFf5k+a1g4Kjlxlb54JBSD3E1/htd 7 | FERTS3a0H3qVg41IxR7yY3ECmgyuIBsduVBGqQIDAQABAoIBAQCOv9yEzPgRwUvm 8 | SrgD/fnrUhlZFVeVb7ksQyF2CtbtneqHWRMwZxya/lgmKqR3g+XHgnKFRHOxz9G/ 9 | h/PM8LtaSIDlJfsPW46SRUds0dXi2mDQSiPGa1xJJlC3gzN2kczVRjdEQakD1BrM 10 | c+3vn9UcRdzZ2UNJiJOmP0/IjmZjw732F1GVZ0/s94gEMEFtcsANUblWmqvsiJKU 11 | jafuRrCr80tcAuH2T56yyQXwEqhxp7sRuWqdfqrdPRcz/R/hH30IVNyUy2h6Bljr 12 | 7KF+zLOEdUVxGgCCWcru/Pw2PvYf/SOrYl8XSxCFVNTwvviduBCzWbYlc+a+Sxb/ 13 | 36MJ08khAoGBAPF5aeqNELDWdV++bO68/tqKZ0O9t+gLLLrETYLlhMkpk1E9j7nQ 14 | nPxroVXQcBktZAmb3tcsa552HjGHijqnmO2Zd7G9duH6uuBf0QZAfHvO4zYq/kEb 15 | Xygbd4g+QpMUritXuOpTPHNgpCYNqtDWLqhfpioEtcAwWdihWDj/ZwJdAoGBAO70 16 | 63dGWZQdXkoBha7AkVIopcQJChckIpdYUU/5zB2UKNNAXn6CzYiLQMVW7mt67Wp+ 17 | Vta7IPVPd8N3haZW6r97/Ar4tVga7NobWbeIRoDVjWxSkSnVlNtHxPbgDF1/f0KA 18 | jRbhfBOsxChdjXGhQ6VlWI2PKR5nd4Q/R09lKSi9AoGAIPVSaIoiehB7arsCAT/Y 19 | hRmY6J0PaeOEkmVSswgCrLY4/kCpylHX63gBSEYv1q/kVI9qxfGxKqqyik4WptTT 20 | QbLNrHvQqx1evls1m2XX18owJueNVnOW9FFzaiwdSBftgcryWUFNIe5R2UPKScDC 21 | E4cild+Yswema7Px562scJ0CgYAu0A59HGwaJzK8ntW4qYMOrGXlJLw0UmDwc13i 22 | pBZHySnCUP7yG9/2GwnCX0hwB/IRBdUvVhOb20ikI3ssXiS5BKoY4CZurz+tbuRB 23 | n1HolW2SRg6oOm5iEwpULwpdGsgsmBCL5fqvxA3dto4qL6SX92XEvcIZcb2pS9hF 24 | hpTspQKBgEZ+izWSWqh3SI/8SArztH3xyrerNJIII1B+k6sNXmWVeloLg+iF4hb5 25 | 4HWztSwU/JgQYOM7GA6XJQlKHNml3Mpy5jJeEDr3NORBdVL5iyzi59EhnR1vg/nd 26 | Ffgtj7ehLjYYtn2MthxsKU337/unfAQFemmbERW0fXyGSSlTCGSQ 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/cross_signing/CAs/new_ca_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAzP4iqWv1eA+huMTn7FW3n8MQ21N5lrslPmuW12sCeSpDOp9X 3 | agr+1ROXg04RaTp+/azZTh8JGX2Aj5iGHCt2LPQqruNfL46QC/tPDpWVQku59bqD 4 | duC/TX+YOniRWt2UODPoBJ891njw40MOWmMGYsvbDsHRaRikguvGQESKDUdEZ97f 5 | pR3GynrdEu39IJGJe/HFz/Vq0tsofh61Mub4J0WCyyva8GN6HU78JPYNsk/f8sme 6 | XVZZCFZTT5Ofmzz3Jbwno5f84Fsps3oTQIqCGzfy8IILNle1tQiaajsBzLp4BgY6 7 | vHxcl/nSwZGsFSo7mt7wxVeFijDTMD54oGMY2wIDAQABAoIBAF4Yrcp9PvE8o6yP 8 | 3jHS6vYP8XXr7F0vTJpJgrUhbsI1jySirdqEb5NZaewg8scNzesd46YDgwbLYC8K 9 | AU1++cEK12jt/+xxkVeepRx5t0j2P8c3NU9lPQsS7cqxO2+tVHM0CGXOvvDrFRAM 10 | 4NLGr9C76LLubvTCFgnutlMb4w7Se5HR7EI4P/vdiiBx7oVi47XeHdpXivAyev6d 11 | 9AHHS8vsF2fr+eC/KKuyzX1qVEAW7sWpH+4GFPlbSRQ0bN5nbtkRzO4M1Thfk4NH 12 | MSLFgGbviELx+5ytm2GWiTmPy7VnEW+370aoLHDKZAr30B6s4Y+Ip0F5MZFczNQt 13 | MOYMewkCgYEA/OgJXW2GbZpkmjcA0VA/mwHuzQE37XxuM0/kQPhnjteP7haomg3E 14 | iugeAetZglwluIGiXPkrZS74lZgN+Y3nRtynlOFgpDFbLcGl/BUS2qjS5yR5n+Gh 15 | BZDKZ/IONEXGM4ihGmVdEZHAg+WyaRpijvmXZUety9BWlW95U2AORA8CgYEAz4AP 16 | QWNrubjPLdCDqsTCoDj/j9XeN0L+hBw0feo45yKgGXDSlVsTm8vI+Bzd8eJuEmaX 17 | 9D2B+UHw1mzV+v1gVQw4qc3vqomZ5KeTFtaV2GiRA7VpauiG3M7L/1Mj5jiL/5ra 18 | TmG66SdNh1oxUh91CKSP+2S+FJ9xiP8sFnnsInUCgYEAwLHTINpoiG4QpOevucvZ 19 | CrPoTU7WXB5ooLQIh/A8hzt7ML8W63n9/JBq/K9jXc3z96beHNTE2a12PhTC4Rfb 20 | 1cQd83PdfH0WqcBYRb+QxAXJ28+Q097IH0N41MY7SFy5bV520XPV+6L/+f8zdp1h 21 | 5uoJDmQU6avtrKJo4LvZYsUCgYEAyXf9V+/AWMMn5cPfE5+XmRm+iVmOOPnDRWJd 22 | VSRlu4XTDOCOlpWnpE1pA/6DQKgpV5RlVa+i0vFDde6WlOwO7ITBP53EZkDUOz65 23 | giM3urdMld0+TZ8X0qy3RWDkKlQdGkCwVgpuAJM4o4LfAG2qpImjguN962gkRG/9 24 | zpTywUECgYAqFcfjI7vpWutNP8TfH49sOkdsf1WDppxlQwPLm7F14DZ61WFoqi2z 25 | DPXPYF3px4w/o15LUW5WW97KWi+AFcuk3xT3CaWw7Xzf3Qqb589sIlumFYstxSbY 26 | BjbXJZ5XIwx33gNF//GPmGZVg7+Q4uL3gJ6hz1VEkTx+IqIk1NpEkQ== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/cross_signing/CAs/third_ca_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAno5PkBvpIDT8G5It/LNVWH/LT2HixlS7paP7ZDbS5ZVgyw2F 3 | N4tVX/AF/f/SW3F4bblJZOCrD38aLz2b/o4uxH41ObKisYdp76UBUwuuFZx6vzTa 4 | iwW8ovy/VQYHha8/PlePBKKbuVqfzni88qSaaxFV4UV6xWrxlJusV/TGOPsahUBd 5 | BBqr7XvULRWnvdqMHm1dCRqpbJUAvCxfIf5p1jox4sRFXMMktOjVN+768vcIMaol 6 | MA7qlhJJyvdoy7PzZvo7TsQzbP26Cri17D6dFxx6fCRLn5DaFD/rJZr69qoHWF/5 7 | 8mezaAiUcQs5HOkqYQ85VeqgRn1kJkzg/ashnwIDAQABAoIBAEcvvakF3vUG1OnM 8 | B7mdCoRhW/aYDCjfgiAsL4Ie2zEYkAyaVWVKuVoJeadAZEGKAHfC6KDR/ftHJYpw 9 | LslrLLQrtdnhygnKslVVz/JpHA+34aRXJoekW70RnRWfT2FF/S8QQLXkC+Z8OaOJ 10 | sct++5V2gwhRGX3pNe1ytz4GN7j8z1FiEO5wu02qVbwZ3IAc9jEGQCvD8lsZpny8 11 | qjoPAs5ho2/V+FrZKKiWXQfhMy7tuKXQKfl5WDnSrox2GQXa/rH1/G8A7IjGFKnb 12 | 0RgNfy2cpX8FJobYMAUv7zyl8wZaJMsjn4UMDy5rykPzavOrKtxfjiyHrrNj8eqQ 13 | kQGuAoECgYEAys2g1gxqFuRJdn3AzzomSopf3FL8vJ1nF6yNbfPpE5qPsloDYbmQ 14 | map3HkWbRZYgQVDrvAOudD8/DliU5sgvTa/XsJHtJyox5hQYnxlHqid+AgJVwbR8 15 | o+WbY1kzDD0DG4i3f25Tx65pRDRlNshmvWLtToUmC3fSMJiz5LH4Iz0CgYEAyCVx 16 | 2PSc3oz50ZZqw/biavCn3zb8ZIjqPg90SNJEIyrIjQY8SzMflKF9xtTUp/9sfyfw 17 | zicygmwq/5h8oYJwbD/vDLZ1pHfPm90lfhTSRlzuoFOSWnvdTgJgJEu8BA5QYI8h 18 | 6aqJg15YzLB88+4jx0WYAu6lcMu478XQtN0J9gsCgYB8I2p0G51E7fBZ65f+QSX3 19 | MsSC5Pl3qxHT1/eqekcNjOqwzZCMXT46EbJggVMOQVr1IEdsCwTae2xpp6ddrOPn 20 | td9dYbW854MrxtlbaeoIKaxkJnG/5G73FDENmyg4MZCZZhKMmkgGTqh462mZIwpg 21 | mxGPoskq8+lhfdeAAcBWMQKBgQC77jT9U4psihEVFFkB0rL9WSU3uei5+bmBNn+o 22 | 1Q63urd/e8hnkARqSkzQww0/Bd55cyZwXGvX/PtO7n6wJMutSUcMN9M08G8XWkqX 23 | HbPn7+D82BwHLGE7m41s51QOtuYQFLXyM3hVyuQ0ps9dOFGODjHTJwRfalsngSYc 24 | faSZywKBgDYBSteHu7uICW0eHW1QhWPxpBfftKeVwccL5mjDo/1OW5l3LEViNsFr 25 | 2U85DCCVkTDAEt+h7DiXIVKg0sCIEasACsa9YIEcBjNxXidwLX6Ek3JyOV1HEUTJ 26 | CVnUA0ZW2E/K4pFLDLa25sW8ZflWfLgeOei7wRLMISnMWSAMPOBt 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/common_scenarios/CAs/another_ca_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEArcQtBVtCYu9/cpJiWalNSZ1ydjV/z/bkWdZGoyiomXR8lG7Z 3 | bIXH8wOUhNP2kbvOEzsuBvM2oKM68MbP397VRqUxtqe5e8AAyVJwMfjDIWjiQ6cf 4 | p/tW64Rx73SBq1TLI1de28kY4QhL0k6hQqo/u0mrVg8nlb9q0YQqNLd4GtZ4Kc+j 5 | jWvamvtlxMlfo7jX+RH59HtLB4W9siGozO4TlamI+9MxvBGc3JLcdQ3k7G0WO6tg 6 | gae6c45YAZ1Z6kgu/iHVyMILY5veVGw5WmXJ8mbPcqhlFPpuzAr7QtH55I2ILr2b 7 | GzWDgNfFeDnBSg7s3xjIlWGruZVMoiWxyiXSjQIDAQABAoIBAHfQrg5kd6kKZQGW 8 | KBSU5YLd+p/BkejO33OGiVxVW7O8G26p5Ges80sAa/cOxJSGP7kI9Fq8dy1af/in 9 | zYwa9Upp+5ZrnjzTDnfCzTPXyIyP6nwlj3Ffn/Zzc7Bd0E9X/13WD8VSq0H3W3ie 10 | 2RjDlYBJcse7WVsrCrd+/y8sVreBiZsBJlgXyAIXL36VciiAlb46Npa1rLyTKGCY 11 | 9IOQbV69dnLXxBJMGPwu49gZSFv63SLBSPM26HHvjAn6+x9etvD9NUyaHM9NZ0qK 12 | H/+f3vQWbebogFcFlTaNQ1sZPZrw+uXmSO+G/CgScIZpUQxi0z3jtyTpTWXN9veD 13 | f+Ys2EECgYEA11vY5jDup4qXRxhWgg/+exIIkNHNFyIANa47ZmU0OsSpbPKgzKMw 14 | Nk7Rni02etRppFaWdqJYOnQvpqh7V4lvHbDYkAI0/zCoQyU2nVnG7dw4lttcWKZw 15 | vrffYBRmm4lnK2M9RTrv/yzkR+RY417uXYJjHxuGqh4drXiQgP2tJp0CgYEAzo72 16 | tCd1F5bLXOUJpL1MxqGYIeoRU/oB1zzAoZZQ6DaTl/JjUbSInwzVwaYKOk0zp3o8 17 | UOOlKH1cleLvqbqZ1+YvsWnZxfvFuKHEQOF68gWqJSQMu+qe8SFclNnzD4UBhIIH 18 | ly29/494jfko7J2LXzATFzA30kq0IFF303q8oLECgYBzF1LDMkNYpmhSRl91Arct 19 | bmXFf1nrkvApwANepZdUEOW6o6/rrQqG+3hq4B7NZddPTDb9D6W3F4CfwEqqbXhE 20 | lc4mqAdKmeZSfADD9sapN5fGh1TCy7BVPE7UOhLb7IKGXXLRHL5F0Ofuw8n9OS6K 21 | KrRuW3eMUqvpmRUg2nE+wQKBgAM71bzoSTr/T1u68jW0CPmrGfid83t/FHxMJS/i 22 | bI3WHOVqFAf5bZIf31DSyqIzBzX+Aee6SKGjpMlr7hiYJURuBB0g7RneYTAMyEfD 23 | v+r+D7XZJKHkgGyB+zMRXm7FxEpQxtGlE65mjpqaZfueU8o/tGzOgnE2Xi+LVvbm 24 | mwFhAoGAMmIeAHvYsGE5xZizOMVwKbe/5GJm1zARroJhDi/memVsnY4OQhnN90vl 25 | EmiM8/3203W6ygf+XT+pKB9CodWsizBzMnybExLySgfevCYxvKUx54ZUbjVgYkSk 26 | +Ol4pRgSVmKi5j5cVcFroR4Tuff7Mbmn7w+uuhkLkk+77L8uijU= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/common_scenarios/CAs/foobar_ca_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAzDXyEMkXqAJfUcBs2Ro6YtBTOpyAlQEjsiyQPyLDqXik5jN+ 3 | 9euFFiGiwjYZPKkoxFq1QH3qKCUp/ucC+pwrqbxzr5vlvWFcKLrVHZKaOjRkNZTg 4 | BRM4ZC/BpX63nHsgQf0NxWkzeC6kQl1V4TtpZnKPnU4URZcfyV3wzmvfHQf13WTA 5 | TZWz6PjcKBZJ8BubBd2bzDYzHBKpT645b/MeqI7D/JLNOzhecKCBRq/Z6Si0auzB 6 | wd2r7N+CwBfhHH4hF0mP0sxj7i6vHVK/5SeKdb92psYaqoTn7COu8WvEsT10VdlH 7 | iRD3PioMvnOMUcHFJPbgm4R3RO1zwcyZtPPGewIDAQABAoIBAQCxZQ9iG34sWds5 8 | 5kHoGsureTND0B420mWS6vlnjyiPeJkuzH823CChiBieQ2aK9pvObsnTO06FDzu7 9 | DizdTXr0mRuZ6oCZkhD/5MUrSmYlt8txbGFqpzaueXnr7PdNvPvZtx0WZ3vppALX 10 | D8OBxB2cfFpbmS8tAMoBIFUGzFhmGy2Z0jP90dY9X4YuDb7wALdSDh7wkbSW+9DI 11 | S7bu3dCHautQHpX8VVndv4L+t/T//i4sZdlHxyoVhvMhsuQmgboiN9d7qmXOBMjV 12 | JNnN8hmwcWSMHbMueQGD7ZRKf9fR7+RrcS6bXiIYIvTjupk1Ka/Q4l372Y+6OcqB 13 | CHrL42TRAoGBAO5EKm+dOY8m88I8iJ5VSRfd9EDbIN1f5TN4lOmZ67oazfAzVD9/ 14 | gP6iuHyIjEWXCXDSKOs+fNwOTNwYk9/EJerLxt5rbQH1+W1+tjNMeQOixeK+KPyg 15 | fQ2x4N5v2epAX9DfRreSEGTP/zg6kwnp8KQOaXczbbz1B3fWqe+ev6W5AoGBANto 16 | 5gDhwtoO6NnFw4JO+DKN2V6d7ZDM3OdLlTnBOVJx0DrHF0UvMHbV+L4oTjxu/pO2 17 | Geaz8LJ1RQfus80RytfFS0JljU4Z9RgPyQDnWx6aMfBoSoaQQ7QFCJFveqGLpQ4o 18 | n99OX6ytG2NxeXNR/ZQ1lUXD10XMwCjGCbaQNyfTAoGAMckwjM3vfwgGmymG6n6r 19 | nT8VkI+5GEi3Xb/ubblp1iNPsMco7XO5AnwBzJPDq67ZnCdtwWn3MQwT51GDZJhZ 20 | y+v0i/kqd+Rr0ANQEvwBtU7Hxo8xhUgLmVb3w8LfZ+E3pC9jWWw93LLRg3BJ1SJ6 21 | 0+znAK9QWNFiVBoYvCcRqJECgYEAvLaMnVWQcl1AiY4/imTBtTmsLGVJX7HUjlvE 22 | DdjTpMWfg+6ZZTghRxTizSt9nx0YeZO202cRL20N4rS8kTUsckmPYZxW3UYWOIvb 23 | JpvXE4ssSMpqzzJmCQALTovDvg6N+I+YckjGoY6CcBW5kCaFJGSx7z9yeOaSoH80 24 | +4Eu72ECgYBmjkHKNe2TR9cQjh64vY/ogWfnF5A/YSFw5ip/3UFljyWUaqCYjWhK 25 | l0B3IrzaW6iw+RZ36sqjp7H2g7SMkOgf4OFvBdx4YPWaLqPBDIChMN/LGcJONG9b 26 | swqnUE4aMtnMzQPp/DTaVQGmiuh2sSchReQKdBdbMPBFXaP6cJKM0w== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/common_scenarios/leaf_certificates/localhost_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAqfHNUg64K3AEHo/K/gP38PRk/6OHoigXPYgIt1ttUqRNBXFd 3 | /vyDiy+BZmduYPNiDe50wWToY1pX5lez6IFZLlE2EhU0/svTY/s2o+LUG9piHIu8 4 | XfZ22NeM+dL0MzxKZnIl+O0tjlKtegE0DjmZm4FEyeHW4KlDiu+pznJTwJm98Y2V 5 | 8Or95qYOSby/vQRQwB4JvRoImQfE/yELKvS3eYvaqDVy6IPMavweOKyyxo/Wbjqv 6 | Xs3eLta4lKKIELmShuu0ri9TVpU7C7J6XLRgEKOBENzU74gp2qIJN8d0xLZdCEXY 7 | f7uaQsWRSz3a4kBuXghReNu2XauksJyNuFAzXQIDAQABAoIBAGIiMwHzZf4PTRGo 8 | zvsnp4EIX1oPQD8vD1yEtSPMM7DJF4DRGgooHn29DOfpSh/J6xFIZTtX3amhfaiR 9 | dF4Kc0j/tE102YlYgdT2N3MjhIAg9w6s0jUgITjOH6YTXthHNF88AxlfPDM3It2U 10 | 3E5qjHzzhJlfRAmvNEWCZ3fDJQHN1KSJ3XmO4xNKEq+AXDto9LqLfQZYkoXFTuaB 11 | Pg9KLrpcCG5QbIwMsyEYUpmQhWxcVykh2+mUdap9iU1OdsOier3QNw3CDetm8QFL 12 | pUdRM2G9Pm36l+Z0sRVNldAP5Jlfx2nUqUd1z1x9Wn7CX2Jjhbeek0Iz+3Z+B7G+ 13 | QaiZBwECgYEA1NN1N3T7v+FWWKvUdBC6wl9Ft9DHEute+ifBWAyF7ghtqexBWOD1 14 | 3LI0cO27RuyxuMy7hl6PntuDpVJJdMOOdHeCjSryacDZhWuOhSQRjWrdNz8kz/Nw 15 | 0CtlrlTWVAiuaqdgrDMXSEPoIFr1VMljsW97epg/rE4M+BnQtp/niZECgYEAzGtp 16 | SYt2sU1VTBQGJJM5IXiumUp6BrWWr4pkPd76o2vLuXHVqyr3LBqnc87OCSLHOj+s 17 | bABgPOW3VASTOTkjAsiGUUcNbVKYbCkGVqptVktJcISfVFcOxqKpn37eQ378EBPp 18 | 1UdRSS+el1xgPVTwfW50FqYdQlpYXI07PnB3Rw0CgYBrKwPmZhDzAVxCkFwOCfKR 19 | N/paYlYsT7tmyigQ11WFlFbsbtxFDhqgBFhl2UApqbygau+u7OTzQGRdFj+I/YQ5 20 | X4is30hsOkEObye4IX55MKuuSyyVTmerW377Nxsp+6kgaaJDdAKwUAmOaV8KT9LU 21 | oWeTRyEg+biMIiu4GnM0EQKBgQC3sJGFB/CZdH+AhMHcXftJi5OBpf4vRz/n7/kR 22 | 6+vXrAb+Vj+Ft28yz6THmP9mED1sp0j+J/xS3K23rwDIvAIs/xpLsQhsw3q6Psc2 23 | W6EymSaeGF7AQBbEAelgrHV3u4UvwWUvRQ3TMRpWsyi8FDaiz/07WPwT4vFiAXtK 24 | KW/VhQKBgBBbsSdLOR4C4fbg8v7E0+js1zG9G4XR88lmHTPYMdlQcdYvggUr+K38 25 | 6kDJ2Z1fef/AIofu4Wvu2+yx0uk71UCxJmXLwqGqgT9EH53E081GjUC7nn1NqnZ5 26 | B8qpZg5QIcLaDkNBgRSPL1JkXL0bDcxb8xAkxwLDJmoZb7ItXFBa 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/cross_signing/leaf_certificates/localhost_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAv6AZDBjBsR3LHNHHAUsUYWhwOwddM/BdCt1F8zVruTrs5hwx 3 | ebUBSaTmQRm6b3+Cuv8NUGxRuHOii/e188/6NioDcUtZ82y+G6HFDd2GICj1nBuj 4 | afv1QaH5TBrtV27HnXQ5KMHi5rLqmTLuBYHC5xtonVJHNzl/Nn3Kfv1CqChsH4OE 5 | QQdcOeZdkQMAXjl4/Av9en+9qOOkYukXqHrBCc9rvE0KqKIBCw8zPGQIogHSUN3r 6 | A72kXBXIIWKyOnil9rQsJVOhvKqWYMN0Xuwn75iz2dRN5+FPHQ/EcTWdz8riJPWQ 7 | juEIQpmITRzWJzXBsZHiEG3b853Rdx8rhhapjQIDAQABAoIBAEYZ5G/Xcqw/sPlH 8 | CN1BQSBtW3lMtVz11qPN1PQnG9R0aQu5TqQugl0VlSJFrj1x0TpladJ8/dxTeShp 9 | I0ghUnCUKBQ/M5XotxdWs8bhvlpvgYJtXfe8s7kh7xhXVXJB1YaVaP+vBjnNRCNt 10 | TUP4d3uiPEApRGJAh3hAxl50efkXxSM+NtgA5Nk/cAUdnZdxLyX7Nt7SL9JgiWGT 11 | tXgDWolBFwm5Y4hekCHUJQaoCCiFsmU5px6FTf/KZMQAfldbLCbpxxXpv3dRV7jt 12 | 5QmV0QNHipMfY9/ggHAvXOE19Q61p6mUpf6l5uxkDDaFakvQ1zhflN6nQBG+c6N/ 13 | teTDzGECgYEA/CqVKyg6pHJVLV7yAPAmLLAQvBrYVRcZspiSXAPiaw1lyCnSzelk 14 | kEGIly4dVBPGi6ECKoorvJ9AwsCjHZnl7kUPQDYOa7VdMcxcWvhX0TC37B0Moyvt 15 | UF2q+9RJ52I0StoqNLkIGjWQzE+oJVSYCozVnKzTGSWVqpGljETdXKkCgYEAwonk 16 | pBhoS3vhQhXsc2lY9tyu2MCWF8k2ouQXuQ0cdPzgM7axJoH9vgE6mJD2BqE9U78o 17 | vudE3r7z6qWucGYruY/GaDzz+yaGtfk8uCyfOUuVg5hpagNk6MwlJLEtomko+AUN 18 | 7bkKMOw7dUPWTF/RVNonN5n+9CAmLznAS0/FMEUCgYA8BibbzwemfVCpLCCO+Jsd 19 | qAtTu1AjcQw85vRv0rzinp3Lt6jmOtglPnQ1CFUS3mI0em75kUo36/COayxor2l7 20 | /hRTRIrKOLGmWO7i+JpuwRoGqiOH87zZlHJO34l3+nhYGPZS9nhnRLji6VMtD6Xh 21 | XQS79qeJJNkrcN/nM709gQKBgQC9O/FjBjL9qkVWSYk2QvW9PLIAOmZGQP6auRGy 22 | emSCEDHvW/k6ohw3pW5gVjJcBKBpRHvYJ5NAsbMHP59NyrUhzg+L+i7C0ZkcjpNX 23 | /F/uFe4ckjeIXkDciyheZtMjNvQZPqFr6yFUsZas3Q4qQNKZnZOkbHIq+tLqAV1i 24 | ZPhFmQKBgBCSTLZveA8FkKJoH3KoxR8YjOQJ8OjSOgXMTn+A+NgD7a9id3rLGZRG 25 | tC671ir0ZNfWJfrUu5i6DNWXOGGsPtKg+DJlvLrqpxB4+S75TjW3GgETIFfgIxDv 26 | xXy2YWpZmi4prni2W0d1F/jINZegBfMQ9KBr8IuHW+81/7UD70wa 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/common_scenarios/leaf_certificates/localhost2_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA7bTYO2tL8T0k1oOcAO1y+RZUl/rG2cmqtbAX4CjIFCePz4ad 3 | cA9Mt8W6z+0uFArBvfgRm7+3ZKaGv9DxDkL0i8i2uklKog/+6dv45zCmWlG1avXa 4 | cVV2ullpEFHnmstKQhq3lsV0Vq1sXQRyA5Yym7vB/lv3hRkLwjjNMJvnghSzZVjf 5 | g+tHFd8MHAff363VGp/HED3PCv92hykKA/R3TlG81CqWRzpvcVO2n19wKI+vTch0 6 | 3L6eFNDFCv2fer5PP/uwwq3UJ0l2+LCtPS6AhL1vQoLDzvmrcX9yvQl+3zAKn9ad 7 | 909/Ow8RQYYSBPSlR5BaI9sWrYbvtNFbYR1NlwIDAQABAoIBAQDo84vdX1+DQSqy 8 | 1zOxbQJ5vTqV3nu1v37+o14inZTGu6ah+AieY0AJ8UsMriG0Hn6UD2IiQpYVm2T+ 9 | 4mL6WP+TZdY9ZcowXICy6pJY70J3zQ9/jQTQgLLkGO9vwPS6rHNPqSM2LLfAoiBK 10 | YCrj/PQHzs9k2PgPheYSJtxfwMn2nTNX4aSDp3CNNBP3apuGbvjNjPHytKphIE0M 11 | Zy5W75d9SX0XAD6e/FJjiu5jg/oDxaBYtHXpqFjs+oITr+Uf++iwYq1MQ6+5fXxP 12 | +up7k2StDN73VRpmld2jDNJLusvzOtFiAY8esTPg5kg4CCNklaxLEFhC91suo4lh 13 | fExvnDPxAoGBAPebk9vIbRtoaO82pPi7+THJo/SAUTd1UQdZHKJXhTnQh/gBxBGL 14 | dokfEYkE+dNU/O5VRiWNi3fV6ABvdHt7WQkXcX6KkBiIOeuB0o3jtRJT7okN61eZ 15 | dMfbr3oXX7HQiU2LArPt5E2x835gId1MhMx0vBBdxpCLISkd7mATePsjAoGBAPXD 16 | WzXrAVi5VDY/oBlZme9v/Lmu9tDQwAT99hrRBo1bmdk6VQWT2rnAUhD5c6kvD0QA 17 | sWGQfwXsLf3H6DnA2CdYkp+pNXcTHjjEOLqJGUHNN7qfXb1MybFyU+6Ribk7K8lM 18 | hfoXUgIWM2nAniPA5AqIdx2rErSumHoEBcVn7jT9AoGAOAmNvP/vMXS/yuOjEmf4 19 | TbxEzuD7gIGchP017cfA7wSywP4lUZMukHnnjgsCP0HMvGOwLG+MUOKoTvDqe8e+ 20 | rsAPf4P8eJDcCuCmtlYCdNNp+LqduIWSWGjvncIMdkGr0VDUm8QLF0pewQsgnuTi 21 | aSNCLLIMbkoOhy+u8y91AeECgYAayy6hhTqDWsk0lOE2gk9HDHWIJOwhvfCBWtK+ 22 | /7TwKmFujm6AiisZfz8zPI8AjfHjHxQ3p9JBagtllKrbyQuH1p89quv6TGpKFBJ/ 23 | O702MFJ4FTHaFooSv30pEFLsj+fpLV+Z8XruYU8/o1FaYog6IcCiymjiq/pRqyiZ 24 | 6gfNXQKBgQDUCvTlIuGdFpC8fHq7/InM5njAczhqSN718ZLLqFmAVjSJkSpNn0F4 25 | y1/U39RS+NhrjLy9iB+Rr9Kg73ehHwvuLpUb9+ERnoQKC09BsPiQ8WxBvXwS16/+ 26 | 2czfUNOPWIj27m1oOFDEDtsNW02NaA4fjUDvLOOAlpxLcQK7nGLfYg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/common_scenarios/leaf_certificates/self_signed_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA9P3OEPmFlX+QcEhwgP0HAUTK4Bi0wSgFrHUzJzpFfk4CBKKm 3 | D182zpUwC//Iqt1HyYez9iLL/wCbTJ7bObQ2paCkLP/RUi4jNEQYtaa3EJSmHApx 4 | QggpoC5UGF3adyFymEgOU9d7zzti75L5ygjnxqsBnouYzH5+wQq2tf8rvAhEAiLj 5 | ABZ0PC2a1x84OL5iuullPco0QVNOKICx0N4FgfM7ftizq3cjwE0vmIdU0c07syaz 6 | 3OSFkXzfAMZ+ELhlcG/FZFUvdfaE+tBghbAUFuf1HFLpisp1UwujFg4zhgipHnJX 7 | WdzDtZM077r/UdKnI7ft3xHyg7rChitQhRTRGQIDAQABAoIBAQCf1q+JN6LGBPIx 8 | r7FnFq5pT36lcrkLhJcfC2ASadxJKY/7gjqhsNdLNN6iFYHQqIpbm+P35H//oV+x 9 | P7t8DG3S5dn3Wfgxi9SAFyWf7mNlt/YgOVfnYvf1gARx2Vnde78OR4TBgsdOT0qf 10 | T98Ux3agkZV+4UkRsj30qI7IcvEPvXYIVCrbjfkM/4E0LMDZ/WbjIkDfOAM6UXW/ 11 | mDK+JqnqCRVoUojapjDrlr7c2kcUxbgDKeQwNkX0WH7z8gEIWNLEkfgsUzjdII1Z 12 | Q5lgE7c99vpkbkqXNKexActll2gEQvYNYuNkTOSPFZKGpBRdNzEhkz0xb9IGFSUR 13 | t/SiU/DZAoGBAPrOPlsaCpRwHFQCrz3+RlOy4HZDwbLHX2hfG/P2HgV3KwpElFJP 14 | JNFlwfrdeh42WUjXnZmFbVxsete60thdH7unIZJEsicXG95rkM66JNJVF3CWEiXu 15 | k8Ob9/BiiAn99oTtuJF0x8W/+7kTLKiKc+Er7JYpwVfsIB/W9fNWgaA7AoGBAPoQ 16 | vBkpTSqz9bLRDIYxPDexcQCwoAPHiuHK/IbUhuHNeq+AOMIVHaAFT+4ex3+6eUXG 17 | V2zVHBg7qSWaB6++9fwaDmJD0KXeBn923/NK1kGjpleBHKSu9ULJYHmryKiCfCYI 18 | RUge9uIhjIFzyeS6JzXNz1lz1Tw1+FP4wsd29/K7AoGAA48dr2lFMSoOHNsU5YpQ 19 | MSvep476lrDhl7eRy/p4Hpe8Up76g9h/q0EIaaju7LLUvYXhxKXyEVewvpL2Hv/j 20 | TWRnw42aAZgAWz2nhmd50kpG1OAsTcF+802pTQ345DtoXiaJcfb4RsQcrFmrTpD4 21 | Z/+Zkd/x0G+XXRxNzTAzErUCgYAI93ynMlHwDA/tX42hajaMkLJKogq61/bk7dBs 22 | TUag5itWC7T9UKCFUmtxUG066Ubb96a+4k1lmQ4RqRpJdQGBDrYhU+kxaWq2O18A 23 | bFiOdf3K+Xg7d4KX6HyrIDvLvxIQkw9Y7LNRnH5bTfmZWAt75Gtg+8RTqmPDLxJc 24 | 8KknbQKBgCiMxEjLx1atbsF8K2q3Thx9J/wV6cuc30ONqyK3UqfS8qtd+n1IHkcu 25 | la6peZHCQceT9YxygffxB/VE8PJTdxJxk38hcG5QDT3aTBV2Kd2yt1r/9ICJdRPA 26 | tG7YH6uWcnfb59ae914gWnWfzldTWfBc51sa/LT9XDd3IstAIgwR 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/common_scenarios/leaf_certificates/wrong.host_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAr8R69AEnUfH99Qkq06d8E8MtwZPQlg9O1rqPz6tmX+q8vfo+ 3 | mNBu09PUZbjcxB+x0ms+y2e3Qmw2TIGn6WeSSPAV4DX7UdnDf0PFKYDed+z4aKgU 4 | Z6glkfMUSy5cVobh0aY/ldExA0FZO1R+mUZ05OxANxl76sYZpGvV1jd4RotX6YyQ 5 | hSAG1kpGhrDA6vA0j31qiv9sI/6gSxoVW+mj5KaLcaJyMmHqPWmIqhryPTu7u5dX 6 | T+zNUcW5wFnZG18LaVmIK6fHGUPH2AGgppCqtrlsWSp4ZhwUxaAgJaAWjaY4lmqe 7 | YAHRXd+m3Mp2RGr1KaK4Over/iaFSU7LQY0U+wIDAQABAoIBACD880aQYNzUWiF/ 8 | EvaeXH8pBpFqOJ+xR86tPQhw1snxuHK/YLg1hldjTl25EFYOzMT9M8/hK1fgD2DU 9 | hLsdoSrnRHPMLdryz+o2we5ELXy8kPKzeQDrYgG+aXSiwtnKft9mIFDx7gz6WtDz 10 | zr7dplrArFyq/8LUY0OkqYGG75/tfzlRBbGkasm9l7hIWhb5y9NAMj66mxAZyhWL 11 | KrB0PWljfOa3ErOrheLvV+QFXyj5L6pEyZ0qzU8krAmmZTil23z8KwrBCW0mohjC 12 | KX4ru6OVI7u7TiBOWAjLQCwfqzU9uXlBUqdaAu3zZ7jnsrvduXZ+Uh6uIIE+l/+2 13 | FGeGUkECgYEA5TrOucjoo2Ro5TnVP3rySqLmaQrQexgTmuZNb2zsY6rFKbvnuNtH 14 | 37rPo0l2oSjGPQGa24tBhtGSnIXreFZ6FSWfaXClPRQWF8dq12i3hbi6Ngl3wSBv 15 | EykmJaomXXqJ86RJiPcAMPzWMneU1PTim/o3qiNYY9yW1zgB9AP2pNkCgYEAxEtV 16 | YMt9pOtWUZwcpIAlJoKGvZbkugs7+tWpwr8kQdt0MMuKvcdcxtu9DYSp6WHgS0nA 17 | y1EF9/Qg5iZ7uMlTb0D8o0pI93N8dbfA8rG1pDcsw+enMD7W3/7P39rNZ7fuF1hN 18 | 9VgXpAr9LopaGh5SNPdPkbFiMRVXis3Sn9g2k/MCgYEAhQEMNcwW/8cg/fAChaTG 19 | tPu0CTrXILUlmvkJ8R8YQfQyFjcLCr1hBLWWCL51cKkB7R/jRIOWb61mDh++MsKq 20 | b163EizkE352GzIAEiCWa6Z1lSkFLS+ug0Wa9ru1KwQQyyeF60y19baeagXse2vj 21 | mHEknzmG7dpuQ04iA/3QvHECgYBBPNxKSDcg2Yodo2mtjl1KeDT5y62G23b6vYon 22 | DEmrjZkHIo6g1iJSgPNLC91AkGPwAboOmIYIS12nwAB8gIS3Q/zy9BxeCUbDBOge 23 | gMtioWfCUDozgZ7+YPyLZHfB0dJSdemq8bUhsvlEP30EkzE1I0CRpkeBEpu+qPso 24 | Xr582wKBgGFzkqVcvbI92QBkGzUop9gytCNcFKQeQLWWcqy7vbOml0Ty/koCH9+k 25 | V518wQNIW+Lca7huQkeFrqxorxyMAlK6F+vGzwe5WXmpP7utCCamNKZL+33j7DjF 26 | 7k8IhAWYTlrgT9v+BeHhyPM5x/tjnZno+7v1Ep+oqLQTbQbEeoEE 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/common_scenarios/intermediate_certificates/intermediate_cert_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAxZ+wVztR0HyvVmR3GDMPgeNT02eD9HpprBo+Ll3WJLvUJKVn 3 | LK7/qLvhd/mNMwMhz9x/UZ5ThriFTKoxo7MypVunUBAWM5L8hUAhdhOi78DreWPq 4 | FwmgBLFnmgxEATJgC7HgUqGGmqdyHGPt4QtH6WkLxjezY0P02fcw3//GBsb8nscA 5 | gziNacpS7kkp/Zzfy2TRbz3mmZ8CyGUJrdEmnbgZDgfzzqIm1/P5z0J3yH/cphI5 6 | 3Szzpmoi/yX1C0+C2iCUavNFhHWh1VfiymvVeNOppwoDCW68SEzRxgGqaiUxKFlV 7 | QiQFQy/FWQlEa3awIp0FZCBoilYp1S+dqfAYfwIDAQABAoIBACb+78kGx4ogUGj3 8 | whhgWvsCZJQepQck8J0pQIPduFffSGlCyi0jCwe8qy0FbTtXruBZXjY4y7qVxpfu 9 | LYVj4uaGQ2mlAvyCUJnqoa+vCEPPCagWcybAm3bRmuPm77RcwF8OZnlnrivuRrIb 10 | deEmXLZOKCig7SD1U+d+w4Iv8l97o1Ik3bx7VB8O2vTew/rauxRBzHC/Vt2c2dOP 11 | MqoNl3xi6uU3zfsgoZ2FlbZ425ZWbcTX6LBfwe9LiG+Jz4sQbiZn9nB/LUb3tg8H 12 | 9GQwhz3AgHD9NiUuCbncF1KQlZ1OuU9MpbP8hcC6TWtY/6mqp7mXFPSzOimpeUcS 13 | HoKZQSECgYEA9nJtVH54qqcR7wdh8h1UMoLGeWc8oVkce6JOFMuvRNJFAEpDvOXP 14 | pB9v5Bvu/+/qPwARC8BOHsb/XNalhY26M9VQA5xxhKw6g9SZx/URxbsAwNisBTvD 15 | ITG78UwNWOUySgZB++040I8VmAeB9I5r9D0YTHJKd+uNQPD3P2rbwnMCgYEAzUjF 16 | /0r2079qMkJnt1RQKTnLOiDzvOEZdm7WxiPtv7Tp9xvsYFfxsbaNXUf7S9YCunxS 17 | J4jybF6OfblIoR8J6/u++F6Ws6hic3d6VdJ7pYLqnvpH/t1YU5ZpQwHVwAWdfcd3 18 | dTEVdJRtg8EM1bW4l9z9gYVFiCjjTH5StHaDMsUCgYBUZDbAGlbjyTZA7fxCVnzA 19 | lzuz0wBWGBfC1n96a41nOcHbKZMsml2STBmauV4MUnGahUFRDtcxft9vQSmJJFFA 20 | 8qjls6rDzd5zylemu2sMlM7bOtcdK6vUMB21sV9Ys5OW+bGavU8ZTAYO+3Mq9aJO 21 | DqXFd9W8XvMN6/Yex07dtQKBgFcGneF/XyofQJ7z5dIjbSQ8+vm2lH2jE89QZ75x 22 | 5s1ZncIDVW72eWD0WfOMm2zn2hpCr/VQqizk3RVRYgfkB6nB/JCAUPTy0mHO6c7O 23 | P1rIKDyjL7hND6/H2PHuKjaQatayo+LLnyPf9JlhUtaahhnjNpkc7YAujUWVW+xj 24 | PXVdAoGBAORNlIq9F9UTaxcqqjY2Hq/3ahU5m+fotWJFB6xxkKyhgG1r8+ekywDB 25 | iQRv2UwPGtbDTZVIJn4JFX4g/p23imvRfYWaM2Kq8JQa1Xc2Qv56tMnnhT1To3Zt 26 | ZgEuVCG8IhbN9CLY18m0oupj6h4dvCp+CIov7fYR2yuzKwz0HC6b 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/cross_signing/intermediate_certificates/regular_intermediate_cert_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAtrjncpH4OIzgtqWv9VgqTqpJ1ic6VWuZBYF6xztylwigi0sB 3 | rDuuP3uOWUTTMws6SLFlHXRvQu7Bb4ua9fqCReg0jsiq4kLFGi+RKu5w9uCY6tx6 4 | ITVd+T6w4HqwopB3yfV1feCME6IXMB0ylAHCO+STOgeRRQZpYE7ksFDl1n3awhKo 5 | FXprkWIS4zL1M85WGJhKpmYVTG/7xIIbtP3cslH2gj1U6CPbnb6BOZYwAHzqO+vK 6 | h/jQohdzWDUZ9Mj9o18wfZpZK6dy0dQk3VA9edKGMsKyCE0KYh96H8QhSrNDbu+L 7 | 0nLsbioZCE1XfbgA0lxUpdcEbqkbmwkvzYfVxwIDAQABAoIBAETBBrXhnxkISXXw 8 | mmkjJ6g6A1aFpDmIJygfNGfLQq76HA7Nxm9cdZCFmWaVszK9wsyfk4Cl00fINxkV 9 | 2tU70dEkMsKXV1WT8j6ML3+MAQ2kLWYSWBIu1pF2uW9+OwCwg6KGrhMTbyHkF/2O 10 | vv1aPA/1TkDDoDHs27HVfc+qYRF33w+W70ryOA47xcVtO/2TDUDlx0ON7BPPwoET 11 | 3st7b6ADsoSz63ChhEy2626PpgFUWnRFU1GOVRS7jWOOKMqhEr+Lg0WmTlFCiEhw 12 | 1hp68pp0XVek5JKzen9/znhe3+uKdt1GoRr5Y3nSXxvOYvHUcuUI52P4pWGjX4yd 13 | AVkIGmECgYEA3PA3+jkgxS5obg3dXeTnSqX0bY79FsBU46KNEolsvWaUW4k5VlD6 14 | AY6ts9OfrqkEbcllyKANFjrTydxLPJBKxmcBWo/A6G0CfODCmngwkSajcslhFRN5 15 | KGNldeI4/t3kznnMo8HrgUkoc39pPOzYuh2Zav4ScwcfgRm/htKhrL0CgYEA07gh 16 | MyK4C8kLdTvJQ6XyxFED3f28S/kRhQB2rWhAvVg/IuEH0DOeZu3anGzdlaMarxne 17 | 1UdJNud8ciQNSCT7e0nWr1cuZn4bKmXoUlf9sdVKpn3+8rWLHnc1HfE/o7U+DOY2 18 | pcZBwZDTteoVvSIMh9fPcuKw6amN06SpOss2rtMCgYBFQE5rcgihhaBaDqJ6Uctl 19 | jIsG5DDUNsjk6QiK7ORWoZaS8no0YgFbB5vOp+B/YJ7wwpXSwsgXVa0YsqIqg5fM 20 | hHDaMADpWXHULqcQv+eJN7E3w4NYldf+Htmoj5YJ3Qdg6LCKC7iDt6ikOyWDYUG1 21 | ZJ23+ZdS0sK/NpDf85mViQKBgQCkvNcIZPF0KiECPVzjiFKXsrohl3dWXhzlsnCl 22 | iUMmkW2/A/4CYMfWKyk5IfRgpKL0sFqU9Xzgfcp1Pdwf0LblPZUQpfNyyJj2qojU 23 | 2aaLIv2NAujHjb8MG5F3FOqBxVrcypym7NbpeHljSRtVKRM6+A9wwzcI+Wdj0PHk 24 | z45jBQKBgB5qaZ7nGHAejjbmtqTl8x7HCz/e7hgtSrL2Zq2HJXPuq75TLxiy8UsD 25 | /oJiUtC60QqPS8Z7fHx3IX1mOhaEvNota0mYYLQYYle3tZlH+U3dFDhaSAqJaKyg 26 | IDpt19eF0zGYxRska0CeI/nv93CrXZh9fHhPoHpQ0urD5dIC/RhG 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /src/tls_certificate_check_app.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2021-2024 Guilherme Andrade 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a 4 | %% copy of this software and associated documentation files (the "Software"), 5 | %% to deal in the Software without restriction, including without limitation 6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | %% and/or sell copies of the Software, and to permit persons to whom the 8 | %% Software is furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | %% DEALINGS IN THE SOFTWARE. 20 | 21 | %% @private 22 | -module(tls_certificate_check_app). 23 | -behaviour(application). 24 | 25 | %% ------------------------------------------------------------------ 26 | %% application Function Exports 27 | %% ------------------------------------------------------------------ 28 | 29 | -export([start/2, 30 | stop/1]). 31 | 32 | %% ------------------------------------------------------------------ 33 | %% application Function Definitions 34 | %% ------------------------------------------------------------------ 35 | 36 | -spec start(term(), list()) -> {ok, pid()} | {error, term()}. 37 | start(_StartType, _StartArgs) -> 38 | tls_certificate_check_sup:start_link(). 39 | 40 | -spec stop(term()) -> ok. 41 | stop(_State) -> 42 | ok. 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | AUTHORITIES_URL = https://curl.se/ca/cacert.pem 2 | AUTHORITIES_FILE = tmp/cacerts.pem 3 | AUTHORITIES_MODULE = src/tls_certificate_check_hardcoded_authorities.erl 4 | 5 | MIX_CHECK=$(shell mix -v >/dev/null 2>/dev/null || /bin/echo "no") 6 | ifeq (no, $(MIX_CHECK)) 7 | $(warning skipping Elixir-dependent tests) 8 | TEST_PROFILES = test 9 | else 10 | # $(info "mix check: $(MIX_CHECK)") 11 | TEST_PROFILES = test,elixir_test 12 | endif 13 | 14 | .PHONY: all build clean \ 15 | check dialyzer xref \ 16 | test cover \ 17 | shell \ 18 | doc-dry \ 19 | publish \ 20 | hardcoded-authorities-update \ 21 | hardcoded-authorities-updater \ 22 | download-latest-authorities \ 23 | invoke-hardcoded-authorities-updater 24 | 25 | .NOTPARALLEL: check 26 | 27 | 28 | all: build 29 | 30 | build: 31 | @rebar3 compile 32 | 33 | rebar3: 34 | wget $(REBAR3_URL) || curl -Lo rebar3 $(REBAR3_URL) 35 | @chmod a+x rebar3 36 | 37 | clean: 38 | @rebar3 clean 39 | 40 | check: dialyzer xref 41 | 42 | dialyzer: 43 | @rebar3 as hardcoded_authorities_update dialyzer 44 | 45 | xref: 46 | @rebar3 as hardcoded_authorities_update xref 47 | 48 | test: 49 | @rebar3 as $(TEST_PROFILES) do eunit, ct, cover 50 | 51 | cover: test 52 | 53 | shell: export ERL_FLAGS = +pc unicode 54 | shell: 55 | @rebar3 as development shell 56 | 57 | doc-dry: 58 | @rebar3 hex build 59 | 60 | publish: 61 | @rebar3 hex publish 62 | 63 | hardcoded-authorities-update: hardcoded-authorities-updater 64 | hardcoded-authorities-update: download-latest-authorities 65 | hardcoded-authorities-update: 66 | @make invoke-hardcoded-authorities-updater 67 | 68 | hardcoded-authorities-updater: 69 | @rebar3 as hardcoded_authorities_update escriptize 70 | 71 | download-latest-authorities: 72 | @curl \ 73 | -o "$(AUTHORITIES_FILE)" \ 74 | --remote-time \ 75 | "$(AUTHORITIES_URL)" 76 | 77 | invoke-hardcoded-authorities-updater: hardcoded-authorities-updater 78 | @./_build/hardcoded_authorities_update/bin/tls_certificate_check_hardcoded_authorities_updater \ 79 | "$(AUTHORITIES_FILE)" \ 80 | "$(AUTHORITIES_URL)" \ 81 | "$(AUTHORITIES_MODULE)" \ 82 | "CHANGELOG.md" 83 | -------------------------------------------------------------------------------- /test/cross_signing/CA_stores/good_ca_store_for_expiry.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDCzCCAfOgAwIBAgIUULN9acegycFzZiIPAUW+25v7dNUwDQYJKoZIhvcNAQEL 3 | BQAwFTETMBEGA1UEAwwKRXhwaXJlZCBDQTAeFw0xNjA5MDMxNDMyNDJaFw0yMTA4 4 | MDgxNDMyNDJaMBUxEzARBgNVBAMMCkV4cGlyZWQgQ0EwggEiMA0GCSqGSIb3DQEB 5 | AQUAA4IBDwAwggEKAoIBAQDhZeZL+Lo1t6yoi25Iysioh1NFuZFqnK2DETf/lRsg 6 | 9BjaBC9gsCIoZ6Nc8Oe5ff26AmfWyvmvkbFR22JIcA7e4MxcdeFEnT2zkWKWIbFa 7 | SR+IqMaOhRFu7NRnsTgAhqBuP9orc5q785tLvq+B91H+wpaQ7md2mwpqkqhhMWwP 8 | g5I1y5Ey6QaGHGZPPUSGSRgP/FUC6x1Q0PlzYL3mxUGYtzdl5wDo1Y+y5nPgwHQU 9 | lqkXtWQ60nXerRBzGs6wkwjrxQJuG7rQp7Wo2uoQvawV/mT5rWDgqOXGVvngkFIP 10 | cTX+G10URFNLdrQfepWDjUjFHvJjcQKaDK4gGx25UEapAgMBAAGjUzBRMB0GA1Ud 11 | DgQWBBTge98vcJx5uizDUCCbJjlxjM6ESDAfBgNVHSMEGDAWgBTge98vcJx5uizD 12 | UCCbJjlxjM6ESDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBg 13 | g+1ilKYIaEaS2d743IjmlZkUkhHevmubLnjRcYHMOUlUpvse8Tv0xg5IATzq4Ol8 14 | W1x6zjXojVPt0cU7m4z0bP5+Ogy8rovHKCDWZpeYtpfbl41nnEZBB2Fk/CULCDSf 15 | h/M++AnGDJtrutldD7/kjrFoFpfOEyx0UJUl5dBAl+sodAZMs5ERULdCcH433cnA 16 | Hc1BcMKgHyLTVRoCGv5eNhqol+5bnHO4Aj1RiXdwBUT905+E5rc0Ki2NkFgPrs3f 17 | T2gYkFE6Ky2OR+zQcd9owlSMvuqm/7i8WatZ9MzhVjPoBsxJ8ClrD60zXGomaUfF 18 | y/W3vrRwiumzbknnenv+ 19 | -----END CERTIFICATE----- 20 | -----BEGIN CERTIFICATE----- 21 | MIIDBTCCAe2gAwIBAgIUUh7Wle+HkdLgBNjUj0LBIHS3I+owDQYJKoZIhvcNAQEL 22 | BQAwETEPMA0GA1UEAwwGbmV3X2NhMCAXDTE2MDkwMzE0MzI0MloYDzIwNzEwODIx 23 | MTQzMjQyWjARMQ8wDQYDVQQDDAZuZXdfY2EwggEiMA0GCSqGSIb3DQEBAQUAA4IB 24 | DwAwggEKAoIBAQDM/iKpa/V4D6G4xOfsVbefwxDbU3mWuyU+a5bXawJ5KkM6n1dq 25 | Cv7VE5eDThFpOn79rNlOHwkZfYCPmIYcK3Ys9Cqu418vjpAL+08OlZVCS7n1uoN2 26 | 4L9Nf5g6eJFa3ZQ4M+gEnz3WePDjQw5aYwZiy9sOwdFpGKSC68ZARIoNR0Rn3t+l 27 | HcbKet0S7f0gkYl78cXP9WrS2yh+HrUy5vgnRYLLK9rwY3odTvwk9g2yT9/yyZ5d 28 | VlkIVlNPk5+bPPclvCejl/zgWymzehNAioIbN/Lwggs2V7W1CJpqOwHMungGBjq8 29 | fFyX+dLBkawVKjua3vDFV4WKMNMwPnigYxjbAgMBAAGjUzBRMB0GA1UdDgQWBBRJ 30 | D5qLvtvZtUaYHZ5uZBgXs/hLLDAfBgNVHSMEGDAWgBRJD5qLvtvZtUaYHZ5uZBgX 31 | s/hLLDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCukG2e72B7 32 | G09eLSfO9Sd9Pwa70BG1IIUNcihVLqNe6Go4qbAg/xcxMoDNTVotdt6y36Mc3A7R 33 | IqmxPyCgHiZh4O4CwufUVRU9rILklGeqSs9BSzVSkfOnQ9oWsPivCYGTR0teQwBi 34 | KDBZgsFHUs2y2qtByg9PnYRxfMnK24gDa5YuQjaRNGrNU0O5kh1Ftan480SyX92x 35 | nr3wa3Wda2SXdTiG0LOPDZkO4xcpfoo5Yag5AAKfaP/jNDSPHl9WOUwVTkQh58Nf 36 | QrzVwLBa6MU/To2rr0SNTs1RWA0t+r+SeicbJ8OONBmohx4Bx5Arx40awkOOQQRd 37 | GDEVuqHO1qCW 38 | -----END CERTIFICATE----- 39 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | % vim: set ft=erlang: 2 | case erlang:function_exported(rebar3, main, 1) 3 | orelse erlang:function_exported('Elixir.Mix', module_info, 1) 4 | orelse {deps, lists:keyfind(deps, 1, CONFIG)} 5 | of 6 | true -> 7 | % either rebar3 or mix 8 | CONFIG; 9 | {deps, false} -> 10 | % no override needed 11 | CONFIG; 12 | {deps, {_, Deps}} -> 13 | % 14 | % rebar 2.x - convert deps to old format 15 | % THIS MAY STOP WORKING AT ANY MOMENT. 16 | % USE AT YOUR OWN RISK. 17 | % 18 | error_logger:warning_msg("~n~n" 19 | "\t!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!~n" 20 | "\t!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!~n" 21 | "\t*********************************************************************~n" 22 | "\t~n" 23 | "\t[tls_certificate_check] You're **strongly** incentivized to use `rebar3`.~n" 24 | "\tCompatibility with rebar 2 is unmaintained and will be removed in the near future.~n" 25 | "\t~n" 26 | "\t*********************************************************************~n" 27 | "\t!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!~n" 28 | "\t!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!~n"), 29 | 30 | {_, ErlOpts} = lists:keyfind(erl_opts, 1, CONFIG), 31 | OverriddenErlOpts = ErlOpts -- [warn_missing_spec], 32 | OverriddenSortableDeps = 33 | lists:foldr( 34 | fun ({ssl_verify_fun, Version}, Acc) -> 35 | [{3, {ssl_verify_fun, ".*", {git, "https://github.com/deadtrickster/ssl_verify_fun.erl.git", 36 | {tag, Version}}}} 37 | | Acc]; 38 | ({Name, Version}, _Acc) -> 39 | MsgErrorIo = io_lib:format("Hex package '~ts', version \"~ts\": missing Git repo", 40 | [Name, Version]), 41 | error(MsgErrorIo) 42 | end, 43 | [], Deps), 44 | 45 | OverriddenDeps = [element(2, Pair) || Pair <- lists:keysort(1, OverriddenSortableDeps)], 46 | Config2 = lists:keystore(deps, 1, CONFIG, {deps,OverriddenDeps}), 47 | lists:keystore(erl_opts, 1, Config2, {erl_opts,OverriddenErlOpts}) 48 | end. 49 | -------------------------------------------------------------------------------- /src/tls_certificate_check_sup.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2021-2024 Guilherme Andrade 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a 4 | %% copy of this software and associated documentation files (the "Software"), 5 | %% to deal in the Software without restriction, including without limitation 6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | %% and/or sell copies of the Software, and to permit persons to whom the 8 | %% Software is furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | %% DEALINGS IN THE SOFTWARE. 20 | 21 | %% @private 22 | -module(tls_certificate_check_sup). 23 | -behaviour(supervisor). 24 | 25 | %% ------------------------------------------------------------------ 26 | %% API Function Exports 27 | %% ------------------------------------------------------------------ 28 | 29 | -export([start_link/0]). 30 | 31 | %% ------------------------------------------------------------------ 32 | %% supervisor Function Exports 33 | %% ------------------------------------------------------------------ 34 | 35 | -export([init/1]). 36 | 37 | %% ------------------------------------------------------------------ 38 | %% Macro Definitions 39 | %% ------------------------------------------------------------------ 40 | 41 | -define(SERVER, ?MODULE). 42 | 43 | %% ------------------------------------------------------------------ 44 | %% API Function Definitions 45 | %% ------------------------------------------------------------------ 46 | 47 | -spec start_link() -> {ok, pid()} | {error, term()}. 48 | start_link() -> 49 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 50 | 51 | %% ------------------------------------------------------------------ 52 | %% supervisor Function Definitions 53 | %% ------------------------------------------------------------------ 54 | 55 | -spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec(), ...]}}. 56 | init([]) -> 57 | SupFlags = #{strategy => one_for_one, intensity => 5, period => 1}, 58 | ChildSpecs = [tls_certificate_check_shared_state:child_spec()], 59 | {ok, {SupFlags, ChildSpecs}}. 60 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - develop 8 | pull_request: 9 | branches: 10 | - master 11 | - develop 12 | workflow_dispatch: 13 | jobs: 14 | ci: 15 | name: > 16 | Run checks and tests over ${{matrix.otp_vsn}} and ${{matrix.os}} 17 | runs-on: ${{matrix.os}} 18 | strategy: 19 | matrix: 20 | otp_vsn: [ 21 | '24.3', 22 | '25.3', 23 | '26.2', 24 | '27.3', 25 | '28.1' 26 | ] 27 | os: ['ubuntu-22.04', 'windows-2022'] 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v5 31 | 32 | - id: otp-version-to-rebar3-version 33 | name: "Read #{OTP version => rebar3 version} map" 34 | uses: juliangruber/read-file-action@v1 35 | with: 36 | path: ./.github/workflows/otp_version_to_rebar3_version.json 37 | 38 | - id: setup-beam 39 | name: Setup BEAM 40 | uses: erlef/setup-beam@v1 41 | with: 42 | otp-version: ${{matrix.otp_vsn}} 43 | rebar3-version: | 44 | ${{ fromJson(steps.otp-version-to-rebar3-version.outputs.content)[matrix.otp_vsn] }} 45 | env: 46 | GITHUB_TOKEN: ${{ github.token }} 47 | 48 | - name: "Non-Windows: set dynamic env (1)" 49 | if: ${{runner.os != 'Windows'}} 50 | run: | 51 | echo "prev_github_run_number=$((${{github.run_number}} - 1))" >> "$GITHUB_ENV" 52 | echo "build_cache_prefix=_build-cache-for-os-${{runner.os}}-otp-${{matrix.otp_vsn}}-rebar3-${{steps.setup-beam.outputs.rebar3-version}}" >> "$GITHUB_ENV" 53 | 54 | - name: "Non-Windows: set dynamic env (2)" 55 | if: ${{runner.os != 'Windows'}} 56 | run: | 57 | echo "build_cache_prefix_with_hash=${{env.build_cache_prefix}}-hash-${{hashFiles('rebar.lock')}}" >> "$GITHUB_ENV" 58 | 59 | - name: "Windows: set dynamic env (1)" 60 | if: ${{runner.os == 'Windows'}} 61 | run: | 62 | # https://github.com/actions/runner-images/issues/5251#issuecomment-1071030822 63 | echo "prev_github_run_number=$(${{github.run_number}} - 1)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 64 | echo "build_cache_prefix=_build-cache-for-os-${{runner.os}}B-otp-${{matrix.otp_vsn}}-rebar3-${{steps.setup-beam.outputs.rebar3-version}}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 65 | 66 | - name: "Windows: set dynamic env (2)" 67 | if: ${{runner.os == 'Windows'}} 68 | run: | 69 | echo "build_cache_prefix_with_hash=${{env.build_cache_prefix}}-hash-${{hashFiles('rebar.lock')}}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 70 | 71 | - name: Restore cached build artifacts 72 | uses: actions/cache/restore@v4 73 | with: 74 | path: _build 75 | key: ${{env.build_cache_prefix_with_hash}}-${{env.prev_github_run_number}} 76 | restore-keys: |- 77 | ${{env.build_cache_prefix_with_hash}}- 78 | ${{env.build_cache_prefix}}- 79 | 80 | - name: Run Checks 81 | run: make check 82 | 83 | - name: Run Tests 84 | run: make test 85 | 86 | - name: Save build artifacts to cache 87 | if: always() 88 | uses: actions/cache/save@v4 89 | with: 90 | path: _build 91 | key: ${{env.build_cache_prefix_with_hash}}-${{github.run_number}} 92 | -------------------------------------------------------------------------------- /test/common_scenarios/certificate_chains/misordered_chain.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICuTCCAaECFERrLwFV1UT1iBYUdGlq07FYADHcMA0GCSqGSIb3DQEBCwUAMBwx 3 | GjAYBgNVBAMMEWludGVybWVkaWF0ZV9jZXJ0MCAXDTE4MDMyMTE3MzAwOVoYDzIw 4 | NzMwMzA3MTczMDA5WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3 5 | DQEBAQUAA4IBDwAwggEKAoIBAQCp8c1SDrgrcAQej8r+A/fw9GT/o4eiKBc9iAi3 6 | W21SpE0FcV3+/IOLL4FmZ25g82IN7nTBZOhjWlfmV7PogVkuUTYSFTT+y9Nj+zaj 7 | 4tQb2mIci7xd9nbY14z50vQzPEpmciX47S2OUq16ATQOOZmbgUTJ4dbgqUOK76nO 8 | clPAmb3xjZXw6v3mpg5JvL+9BFDAHgm9GgiZB8T/IQsq9Ld5i9qoNXLog8xq/B44 9 | rLLGj9ZuOq9ezd4u1riUoogQuZKG67SuL1NWlTsLsnpctGAQo4EQ3NTviCnaogk3 10 | x3TEtl0IRdh/u5pCxZFLPdriQG5eCFF427Zdq6SwnI24UDNdAgMBAAEwDQYJKoZI 11 | hvcNAQELBQADggEBACmCdCUKnYd+Lc1NpTuCAvy7P4XrRzWkelrRrgAgCLN3zyQv 12 | zzW2XAAFIDmcdCuQ1D5A4i14tkDsz8Z3uY1lyjDqSDa+9ClHUt6G7FWhYj5FFTCK 13 | g7jTF/6bAawgBKPD0+RA1PXcMiKllvH5KRCrb1ZYGk8TsUhshMWBDXZQlu0S5XNF 14 | pa64nDc3emxmvRKYtUF56ModNbv36BjgXgPVYgGUP62prXyitho2JtAvNTkD/T22 15 | sbYOoGnzYEeHbr2SYXMLtk0O1/VnR2PQvHF2JL9lzGGK/ZJn1gdvZh7PGjjaze/o 16 | Y1p/hMQPAWM+/BuShdlRM9sc7zG8ABLw6VWK0aU= 17 | -----END CERTIFICATE----- 18 | -----BEGIN CERTIFICATE----- 19 | MIIDCzCCAfOgAwIBAgIURTXzKFl6PNQU/eSMa0m8SkNHe+YwDQYJKoZIhvcNAQEL 20 | BQAwFDESMBAGA1UEAwwJZm9vYmFyX2NhMCAXDTE4MDMyMTE3MzAwOVoYDzIwNzMw 21 | MzA3MTczMDA5WjAUMRIwEAYDVQQDDAlmb29iYXJfY2EwggEiMA0GCSqGSIb3DQEB 22 | AQUAA4IBDwAwggEKAoIBAQDMNfIQyReoAl9RwGzZGjpi0FM6nICVASOyLJA/IsOp 23 | eKTmM37164UWIaLCNhk8qSjEWrVAfeooJSn+5wL6nCupvHOvm+W9YVwoutUdkpo6 24 | NGQ1lOAFEzhkL8GlfreceyBB/Q3FaTN4LqRCXVXhO2lmco+dThRFlx/JXfDOa98d 25 | B/XdZMBNlbPo+NwoFknwG5sF3ZvMNjMcEqlPrjlv8x6ojsP8ks07OF5woIFGr9np 26 | KLRq7MHB3avs34LAF+EcfiEXSY/SzGPuLq8dUr/lJ4p1v3amxhqqhOfsI67xa8Sx 27 | PXRV2UeJEPc+Kgy+c4xRwcUk9uCbhHdE7XPBzJm088Z7AgMBAAGjUzBRMB0GA1Ud 28 | DgQWBBRdgYv0v0z0Ig4rJFNIfCVkc4DJBDAfBgNVHSMEGDAWgBRdgYv0v0z0Ig4r 29 | JFNIfCVkc4DJBDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCm 30 | 86+NrjW36v9IpI1g8zVPUaGB1naGyLOa2870j44eV/geVB5FeMunIXcbs2kxVH8J 31 | fF9Trd7NbWrS3NoZPeBJkMMDYyOy6bkMHm78B4s1s5wpzoRpbdZvfcXimZieQ7iQ 32 | W89rdE8qdihqW3RN9px/O19BgUpVNvbxrPo/6oNphnl3axqLSYvSQg1QYtuHMmoN 33 | joOCMPRv+CzqyQCbwfBAsO83JbPWRamk7zVW4c68WqBi412O/ncvFI5TZVuJ5tML 34 | e/tREDZouMC67klWJLQWmZmvU60+icChQGadeini551bYb8bAjxo6BlcIcnbEEaR 35 | Bff2gqkp/BZuBUPJj+7T 36 | -----END CERTIFICATE----- 37 | -----BEGIN CERTIFICATE----- 38 | MIIDJjCCAg6gAwIBAgIUExBnZQftxN5TH6Sut5sowCss9XgwDQYJKoZIhvcNAQEL 39 | BQAwFDESMBAGA1UEAwwJZm9vYmFyX2NhMCAXDTE4MDMyMTE3MzAwOVoYDzIwNzMw 40 | MzA3MTczMDA5WjAcMRowGAYDVQQDDBFpbnRlcm1lZGlhdGVfY2VydDCCASIwDQYJ 41 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMWfsFc7UdB8r1ZkdxgzD4HjU9Nng/R6 42 | aawaPi5d1iS71CSlZyyu/6i74Xf5jTMDIc/cf1GeU4a4hUyqMaOzMqVbp1AQFjOS 43 | /IVAIXYTou/A63lj6hcJoASxZ5oMRAEyYAux4FKhhpqnchxj7eELR+lpC8Y3s2ND 44 | 9Nn3MN//xgbG/J7HAIM4jWnKUu5JKf2c38tk0W895pmfAshlCa3RJp24GQ4H886i 45 | Jtfz+c9Cd8h/3KYSOd0s86ZqIv8l9QtPgtoglGrzRYR1odVX4spr1XjTqacKAwlu 46 | vEhM0cYBqmolMShZVUIkBUMvxVkJRGt2sCKdBWQgaIpWKdUvnanwGH8CAwEAAaNm 47 | MGQwHQYDVR0OBBYEFPYAMZ3gTsH9xw7V2e3z+NBhmb6RMB8GA1UdIwQYMBaAFF2B 48 | i/S/TPQiDiskU0h8JWRzgMkEMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/ 49 | BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IBAQBG0TYq2GgUvhujNly3PfoZwQpgR77Z 50 | d3F68vmgDEPrbJcB8kbmiMUOkMIxRSLs8A9dKJqtE6IU4Wjb9hhLra21eL5m4h5s 51 | SKlTsNnke072HmyGlt8J1i4x9Twqo8CrxxQjpCtlPoYnXK/65oNoTAdxK0IORBES 52 | Kfs6f4JfuFW85gN1dWjp28N4J63FQnGeax78upK0VAddm72KgZhjtiTuRZ4PULe6 53 | 5/svWUzvHAaUBp57qm3CDcLL48F/jOipTASIr8vCYv6uWMNTbLQvdZ9zP1lJwXjd 54 | XDH+1kQGTZN9a7tUflVEBpPLTBqpGTZnewnl9rx5rcQbtB12TSng+7JQ 55 | -----END CERTIFICATE----- 56 | -------------------------------------------------------------------------------- /test/cross_signing/certificate_chains/localhost_chain_for_cross_signing.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICwTCCAakCFESDIvYUe0n2w+HzV/MS/VXazRcfMA0GCSqGSIb3DQEBCwUAMCQx 3 | IjAgBgNVBAMMGXJlZ3VsYXJfaW50ZXJtZWRpYXRlX2NlcnQwIBcNMTYwOTAzMTQz 4 | MjQyWhgPMjA3MTA4MjExNDMyNDJaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIw 5 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL+gGQwYwbEdyxzRxwFLFGFocDsH 6 | XTPwXQrdRfM1a7k67OYcMXm1AUmk5kEZum9/grr/DVBsUbhzoov3tfPP+jYqA3FL 7 | WfNsvhuhxQ3dhiAo9Zwbo2n79UGh+Uwa7Vdux510OSjB4uay6pky7gWBwucbaJ1S 8 | Rzc5fzZ9yn79QqgobB+DhEEHXDnmXZEDAF45ePwL/Xp/vajjpGLpF6h6wQnPa7xN 9 | CqiiAQsPMzxkCKIB0lDd6wO9pFwVyCFisjp4pfa0LCVTobyqlmDDdF7sJ++Ys9nU 10 | TefhTx0PxHE1nc/K4iT1kI7hCEKZiE0c1ic1wbGR4hBt2/Od0XcfK4YWqY0CAwEA 11 | ATANBgkqhkiG9w0BAQsFAAOCAQEAGawVTKk+Igydwk8fmI6unqYZTfCLCEP5BcJR 12 | 2JMu/xg2/xZcA8XuO28ML6DgwCvXOQEyNpJK5Uej0mhR0syDRtfDmRONiq/Nep9T 13 | KPDs7bATF/cgath2ZyYbTRZioZ4W8tdJeN2umPHqmdVCY8KjVJaJs7Yopz0qoenI 14 | ZfLjTcnHALiOOQv5FD0iEa0yK1hItCz0Pr/48Nj0hO/7+BdRYldiyA1f3Dc8zSuk 15 | n8n1S3PWCEg3FBeIFl7FNPbdJNpj4U9cDpeMDF5t227h6K71fxvj8og3qyhwp26W 16 | 5RZZe9Ub8mZZYcr2NxuLEN14WIYPN1yyLfEDPOdoI2DFgxH2kA== 17 | -----END CERTIFICATE----- 18 | -----BEGIN CERTIFICATE----- 19 | MIIDKzCCAhOgAwIBAgIUAkHiZANRxRADSk90QM6IuJLQcEwwDQYJKoZIhvcNAQEL 20 | BQAwETEPMA0GA1UEAwwGbmV3X2NhMCAXDTE2MDkwMzE0MzI0MloYDzIwNzEwODIx 21 | MTQzMjQyWjAkMSIwIAYDVQQDDBlyZWd1bGFyX2ludGVybWVkaWF0ZV9jZXJ0MIIB 22 | IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtrjncpH4OIzgtqWv9VgqTqpJ 23 | 1ic6VWuZBYF6xztylwigi0sBrDuuP3uOWUTTMws6SLFlHXRvQu7Bb4ua9fqCReg0 24 | jsiq4kLFGi+RKu5w9uCY6tx6ITVd+T6w4HqwopB3yfV1feCME6IXMB0ylAHCO+ST 25 | OgeRRQZpYE7ksFDl1n3awhKoFXprkWIS4zL1M85WGJhKpmYVTG/7xIIbtP3cslH2 26 | gj1U6CPbnb6BOZYwAHzqO+vKh/jQohdzWDUZ9Mj9o18wfZpZK6dy0dQk3VA9edKG 27 | MsKyCE0KYh96H8QhSrNDbu+L0nLsbioZCE1XfbgA0lxUpdcEbqkbmwkvzYfVxwID 28 | AQABo2YwZDAdBgNVHQ4EFgQULNjqrkZoFu/MHvpb8U5wUvxpe8wwHwYDVR0jBBgw 29 | FoAUSQ+ai77b2bVGmB2ebmQYF7P4SywwEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNV 30 | HQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggEBAHGg/UWjmGq9Y3gO4ijaGi15 31 | Y0iuEUonjPqwLOA1bKdvQHux3+cqcOUiYdtb1qYYe+y3gzSUVtz+0cYSa6GwEnvH 32 | 49k0q5ycJGwjkG5L3wRk6WFW2TvHIgW2V+iyxqTapEm1MioaUA3e4AtcaYMSTldr 33 | BLhw1mrVqq45n6vorz7KMMrXyuxZfl1pdXF6xm6fdfapWQCteDV4cyiGwwHK5PGE 34 | 3BGuf+2cq6o5DFUrnXjZiK1mGBKsZCXjvMUpI0oL9253lWUEQFiz0HDeWz5r7147 35 | g9TMmaqU2fUsybAt2eypx2fpMWf4FYD5A2R+uo7DOn4yQK0cmh8UZ0vKdiAwVns= 36 | -----END CERTIFICATE----- 37 | -----BEGIN CERTIFICATE----- 38 | MIIDGjCCAgKgAwIBAgIUCpCE721zBco9w1A3MLnXFOQeV04wDQYJKoZIhvcNAQEL 39 | BQAwEzERMA8GA1UEAwwIdGhpcmRfY2EwIBcNMTYwOTAzMTQzMjQyWhgPMjA3MTA4 40 | MjExNDMyNDJaMBExDzANBgNVBAMMBm5ld19jYTCCASIwDQYJKoZIhvcNAQEBBQAD 41 | ggEPADCCAQoCggEBAMz+Iqlr9XgPobjE5+xVt5/DENtTeZa7JT5rltdrAnkqQzqf 42 | V2oK/tUTl4NOEWk6fv2s2U4fCRl9gI+Yhhwrdiz0Kq7jXy+OkAv7Tw6VlUJLufW6 43 | g3bgv01/mDp4kVrdlDgz6ASfPdZ48ONDDlpjBmLL2w7B0WkYpILrxkBEig1HRGfe 44 | 36Udxsp63RLt/SCRiXvxxc/1atLbKH4etTLm+CdFgssr2vBjeh1O/CT2DbJP3/LJ 45 | nl1WWQhWU0+Tn5s89yW8J6OX/OBbKbN6E0CKghs38vCCCzZXtbUImmo7Acy6eAYG 46 | Orx8XJf50sGRrBUqO5re8MVXhYow0zA+eKBjGNsCAwEAAaNmMGQwHQYDVR0OBBYE 47 | FEkPmou+29m1Rpgdnm5kGBez+EssMB8GA1UdIwQYMBaAFBA/1JEsUlwY0Me5+a3f 48 | YK0WW0a8MBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqG 49 | SIb3DQEBCwUAA4IBAQB/iCTxdh2q6zCZhrOXfmSYeXtviLVdzp8XO/8Qw/MqBK4V 50 | JE5jo7+cmHBP0VZB4Eu1UlaAfIGbsNFGKoWGsTE56NJlvtcEKompJifuMoimLJjM 51 | HvQEfHGhm59UbXSqdirR5Frw2z48vECgHEExczkAFqyNpifN+WMEqqESg6M0gjEl 52 | bs5ioT5VgfPZLplrjexvrghdRCted63MU2JUdFsVk8dmVPS0fmUeYnZpGBq1C5aU 53 | 8VY9gd9jtP3I7Dl3FT/4OtJDrno7CqdZR1+TgawwwhMr7VyMLAIfvz6/LI6Hi32I 54 | d7HUs1C1gIbGJgu16NJJiP4ThQcsbtnugAuqALsw 55 | -----END CERTIFICATE----- 56 | -------------------------------------------------------------------------------- /test/cross_signing/certificate_chains/localhost_chain_for_expiry.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICwTCCAakCFESDIvYUe0n2w+HzV/MS/VXazRcfMA0GCSqGSIb3DQEBCwUAMCQx 3 | IjAgBgNVBAMMGXJlZ3VsYXJfaW50ZXJtZWRpYXRlX2NlcnQwIBcNMTYwOTAzMTQz 4 | MjQyWhgPMjA3MTA4MjExNDMyNDJaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIw 5 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL+gGQwYwbEdyxzRxwFLFGFocDsH 6 | XTPwXQrdRfM1a7k67OYcMXm1AUmk5kEZum9/grr/DVBsUbhzoov3tfPP+jYqA3FL 7 | WfNsvhuhxQ3dhiAo9Zwbo2n79UGh+Uwa7Vdux510OSjB4uay6pky7gWBwucbaJ1S 8 | Rzc5fzZ9yn79QqgobB+DhEEHXDnmXZEDAF45ePwL/Xp/vajjpGLpF6h6wQnPa7xN 9 | CqiiAQsPMzxkCKIB0lDd6wO9pFwVyCFisjp4pfa0LCVTobyqlmDDdF7sJ++Ys9nU 10 | TefhTx0PxHE1nc/K4iT1kI7hCEKZiE0c1ic1wbGR4hBt2/Od0XcfK4YWqY0CAwEA 11 | ATANBgkqhkiG9w0BAQsFAAOCAQEAGawVTKk+Igydwk8fmI6unqYZTfCLCEP5BcJR 12 | 2JMu/xg2/xZcA8XuO28ML6DgwCvXOQEyNpJK5Uej0mhR0syDRtfDmRONiq/Nep9T 13 | KPDs7bATF/cgath2ZyYbTRZioZ4W8tdJeN2umPHqmdVCY8KjVJaJs7Yopz0qoenI 14 | ZfLjTcnHALiOOQv5FD0iEa0yK1hItCz0Pr/48Nj0hO/7+BdRYldiyA1f3Dc8zSuk 15 | n8n1S3PWCEg3FBeIFl7FNPbdJNpj4U9cDpeMDF5t227h6K71fxvj8og3qyhwp26W 16 | 5RZZe9Ub8mZZYcr2NxuLEN14WIYPN1yyLfEDPOdoI2DFgxH2kA== 17 | -----END CERTIFICATE----- 18 | -----BEGIN CERTIFICATE----- 19 | MIIDKzCCAhOgAwIBAgIUAkHiZANRxRADSk90QM6IuJLQcEwwDQYJKoZIhvcNAQEL 20 | BQAwETEPMA0GA1UEAwwGbmV3X2NhMCAXDTE2MDkwMzE0MzI0MloYDzIwNzEwODIx 21 | MTQzMjQyWjAkMSIwIAYDVQQDDBlyZWd1bGFyX2ludGVybWVkaWF0ZV9jZXJ0MIIB 22 | IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtrjncpH4OIzgtqWv9VgqTqpJ 23 | 1ic6VWuZBYF6xztylwigi0sBrDuuP3uOWUTTMws6SLFlHXRvQu7Bb4ua9fqCReg0 24 | jsiq4kLFGi+RKu5w9uCY6tx6ITVd+T6w4HqwopB3yfV1feCME6IXMB0ylAHCO+ST 25 | OgeRRQZpYE7ksFDl1n3awhKoFXprkWIS4zL1M85WGJhKpmYVTG/7xIIbtP3cslH2 26 | gj1U6CPbnb6BOZYwAHzqO+vKh/jQohdzWDUZ9Mj9o18wfZpZK6dy0dQk3VA9edKG 27 | MsKyCE0KYh96H8QhSrNDbu+L0nLsbioZCE1XfbgA0lxUpdcEbqkbmwkvzYfVxwID 28 | AQABo2YwZDAdBgNVHQ4EFgQULNjqrkZoFu/MHvpb8U5wUvxpe8wwHwYDVR0jBBgw 29 | FoAUSQ+ai77b2bVGmB2ebmQYF7P4SywwEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNV 30 | HQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggEBAHGg/UWjmGq9Y3gO4ijaGi15 31 | Y0iuEUonjPqwLOA1bKdvQHux3+cqcOUiYdtb1qYYe+y3gzSUVtz+0cYSa6GwEnvH 32 | 49k0q5ycJGwjkG5L3wRk6WFW2TvHIgW2V+iyxqTapEm1MioaUA3e4AtcaYMSTldr 33 | BLhw1mrVqq45n6vorz7KMMrXyuxZfl1pdXF6xm6fdfapWQCteDV4cyiGwwHK5PGE 34 | 3BGuf+2cq6o5DFUrnXjZiK1mGBKsZCXjvMUpI0oL9253lWUEQFiz0HDeWz5r7147 35 | g9TMmaqU2fUsybAt2eypx2fpMWf4FYD5A2R+uo7DOn4yQK0cmh8UZ0vKdiAwVns= 36 | -----END CERTIFICATE----- 37 | -----BEGIN CERTIFICATE----- 38 | MIIDHDCCAgSgAwIBAgIUZfYodJ57SYQG7w7lci5fJ02SR6swDQYJKoZIhvcNAQEL 39 | BQAwFTETMBEGA1UEAwwKRXhwaXJlZCBDQTAgFw0xNjA5MDMxNDMyNDJaGA8yMDcx 40 | MDgyMTE0MzI0MlowETEPMA0GA1UEAwwGbmV3X2NhMIIBIjANBgkqhkiG9w0BAQEF 41 | AAOCAQ8AMIIBCgKCAQEAzP4iqWv1eA+huMTn7FW3n8MQ21N5lrslPmuW12sCeSpD 42 | Op9Xagr+1ROXg04RaTp+/azZTh8JGX2Aj5iGHCt2LPQqruNfL46QC/tPDpWVQku5 43 | 9bqDduC/TX+YOniRWt2UODPoBJ891njw40MOWmMGYsvbDsHRaRikguvGQESKDUdE 44 | Z97fpR3GynrdEu39IJGJe/HFz/Vq0tsofh61Mub4J0WCyyva8GN6HU78JPYNsk/f 45 | 8smeXVZZCFZTT5Ofmzz3Jbwno5f84Fsps3oTQIqCGzfy8IILNle1tQiaajsBzLp4 46 | BgY6vHxcl/nSwZGsFSo7mt7wxVeFijDTMD54oGMY2wIDAQABo2YwZDAdBgNVHQ4E 47 | FgQUSQ+ai77b2bVGmB2ebmQYF7P4SywwHwYDVR0jBBgwFoAU4HvfL3Ccebosw1Ag 48 | myY5cYzOhEgwEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8EBAMCAYYwDQYJ 49 | KoZIhvcNAQELBQADggEBACc0bdqom82iwDgUarAkQY8n8dKGOHSJHr8YUqg8mF6S 50 | 0lZ+o4RbuNKS0YMOLfGZTY2k+NSWjGrj3+kbxi1kifgmcefCWsM59wHUJRFAFryF 51 | il30BFYj8enwL4pr27LvB0I6WdeIEPci0v3O4/6zrRxtUwfuTWx7BjOJggnZ3I3W 52 | OjEREFXpI7dA6d3bW5pzLVryw2/SsNRF7j+GZyzwKTeQ2nKQshLUuXyNL0Wu5a99 53 | 5h6tYIETM9JrmvKUf8qzMSUjnNmahD1NqLFtWH5dAetyWTzEaf6F0Sg2K/UBfKrs 54 | oCfScrrsWNv+KUW+gIJXkpcTDP1bxqlQvdMqMSyN3QM= 55 | -----END CERTIFICATE----- 56 | -------------------------------------------------------------------------------- /test/tls_certificate_check_dependent_app_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2025 Guilherme Andrade 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a 4 | %% copy of this software and associated documentation files (the "Software"), 5 | %% to deal in the Software without restriction, including without limitation 6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | %% and/or sell copies of the Software, and to permit persons to whom the 8 | %% Software is furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | %% DEALINGS IN THE SOFTWARE. 20 | 21 | -module(tls_certificate_check_dependent_app_SUITE). 22 | -compile(export_all). 23 | 24 | -include_lib("common_test/include/ct.hrl"). 25 | -include_lib("stdlib/include/assert.hrl"). 26 | 27 | %% ------------------------------------------------------------------ 28 | %% Macros 29 | %% ------------------------------------------------------------------ 30 | 31 | -define(SHARED_STATE_PROC_NAME, tls_certificate_check_shared_state). 32 | 33 | %% ------------------------------------------------------------------ 34 | %% Setup 35 | %% ------------------------------------------------------------------ 36 | 37 | all() -> 38 | [ 39 | get_options_forces_tls_cert_check_to_start 40 | ]. 41 | 42 | init_per_suite(Config) -> 43 | % Build the test consumer app 44 | TestAppDir = filename:join(?config(data_dir, Config), "dependent_app"), 45 | % logger:emergency("TestAppDir: ~p", [TestAppDir]), 46 | 47 | % Compile the test app 48 | {ok, OldDir} = file:get_cwd(), 49 | ok = file:set_cwd(TestAppDir), 50 | 51 | % Run rebar3 compile in the test app directory 52 | os:cmd("rebar3 compile"), 53 | 54 | ok = file:set_cwd(OldDir), 55 | TestAppEbin = filename:join([TestAppDir, "_build", "default", "lib", "dependent_app", "ebin"]), 56 | true = code:add_patha(TestAppEbin), 57 | 58 | [{test_app_dir, TestAppDir} | Config]. 59 | 60 | end_per_suite(_Config) -> 61 | %_ = application:stop(dependent_app), 62 | _ = application:stop(tls_certificate_check), 63 | ok. 64 | 65 | %% ------------------------------------------------------------------ 66 | %% Test Cases 67 | %% ------------------------------------------------------------------ 68 | 69 | get_options_forces_tls_cert_check_to_start(_Config) -> 70 | % Ensure that the workaround starts `tls_certificate_check' 71 | ?assertEqual(undefined, whereis(?SHARED_STATE_PROC_NAME)), 72 | ?assertMatch([_ | _], tls_certificate_check:options("example.com")), 73 | ?assertNotEqual(undefined, whereis(?SHARED_STATE_PROC_NAME)), 74 | 75 | % Ensure that `dependent_app' is not yet started 76 | ?assertMatch(false, lists:keyfind(dependent_app, 1, application:which_applications())), 77 | 78 | % Ensure that we can start `dependent_app' 79 | ?assertMatch({ok, _}, application:ensure_all_started(dependent_app)), 80 | ?assertNotMatch(false, lists:keyfind(dependent_app, 1, application:which_applications())), 81 | 82 | % Ensure that options are still available 83 | ?assertMatch([_ | _], tls_certificate_check:options("example.com")). 84 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [ 2 | debug_info, 3 | warn_export_all, 4 | warn_export_vars, 5 | warn_missing_spec, 6 | warn_obsolete_guards, 7 | warn_shadow_vars, 8 | warn_unused_import, 9 | warnings_as_errors, 10 | {platform_define, "^23\.[1-3]", 'HARDCODED_AUTHORITIES_UPDATER_SUPPORTED'}, 11 | {platform_define, "^2[4-9]", 'HARDCODED_AUTHORITIES_UPDATER_SUPPORTED'}, 12 | {platform_define, "^[3-9]", 'HARDCODED_AUTHORITIES_UPDATER_SUPPORTED'}, 13 | {platform_define, "^22\.", 'EXPIRED_CAs_ARE_CONSIDERED_VALID'}, 14 | {platform_define, "^23\.[0-2][^0-9]", 'EXPIRED_CAs_ARE_CONSIDERED_VALID'}, 15 | {platform_define, "^22\.", 'FLAKY_CROSS_SIGNING_VALIDATION'}, 16 | {platform_define, "^23\.[0-1][^0-9]", 'FLAKY_CROSS_SIGNING_VALIDATION'}, 17 | {platform_define, "^2[2-4]\.", 'NO_PUBLIC_KEY_CACERTS_GET'}, 18 | 19 | {platform_define, "^22\.", 'MISMATCHED_SNI_FAILS_HANDSHAKE'}, 20 | {platform_define, "^2[3-6]\.", 'MISMATCHED_SNI_CLOSES_CONN'}, 21 | {platform_define, "^27\.[0-2]\.", 'MISMATCHED_SNI_CLOSES_CONN'} 22 | ]}. 23 | 24 | {minimum_otp_vsn, "22.0"}. % but only 24+ is supported 25 | 26 | {deps, [ 27 | {ssl_verify_fun, "~> 1.1"} 28 | ]}. 29 | 30 | {shell, [ 31 | {apps, [tls_certificate_check]}, 32 | {config, "shell.config"} 33 | ]}. 34 | 35 | {dialyzer, [ 36 | {plt_include_all_deps, true}, 37 | {warnings, [ 38 | unmatched_returns, 39 | error_handling, 40 | underspecs, 41 | unknown 42 | ]}, 43 | {plt_extra_apps, [changelog_updater]} 44 | ]}. 45 | 46 | {xref_checks, [ 47 | undefined_function_calls, 48 | undefined_functions, 49 | locals_not_used, 50 | exports_not_used, 51 | deprecated_function_calls, 52 | deprecated_functions 53 | ]}. 54 | 55 | {project_plugins, [ 56 | {rebar3_ex_doc, "0.2.28"}, 57 | {rebar3_hex, "7.0.9"} 58 | ]}. 59 | 60 | {profiles, [ 61 | {development, [ 62 | {deps, [{recon, "~> 2.5"}]}, 63 | {erl_opts, [ 64 | nowarn_missing_spec, 65 | nowarnings_as_errors 66 | ]} 67 | ]}, 68 | 69 | {test, [ 70 | {erl_opts, [ 71 | debug_info, 72 | nowarn_export_all, 73 | nowarn_missing_spec, 74 | nowarnings_as_errors 75 | ]}, 76 | {deps, [ 77 | {certifi, "~> 2.10"}, 78 | {meck, "0.9.2"} 79 | ]}, 80 | {cover_enabled, true}, 81 | {cover_opts, [verbose]}, 82 | {eunit_tests, [ 83 | % Prevent eunit from failing because it detects additional modules 84 | % under `test/tls_certificate_check_dependent_app_SUITE_data' and 85 | % tries running tests in them. 86 | {application, tls_certificate_check} 87 | ]} 88 | ]}, 89 | 90 | {elixir_test, [ 91 | {deps, [{castore, "0.1.20"}]}, 92 | {plugins, [{rebar_mix, "0.5.1"}]}, 93 | {provider_hooks, [{post, [{compile, {mix, consolidate_protocols}}]}]} 94 | ]}, 95 | 96 | {hardcoded_authorities_update, [ 97 | {erl_opts, [ 98 | {src_dirs, ["src", "util"]} 99 | ]}, 100 | {deps, [ 101 | {changelog_updater, {git, "https://github.com/g-andrade/changelog_updater.git", {ref, "07b13ce"}}} % FIXME 102 | ]}, 103 | {escript_incl_apps, [tls_certificate_check, changelog_updater]}, 104 | {escript_name, "tls_certificate_check_hardcoded_authorities_updater"}, 105 | {escript_emu_args, "%%! -noinput\n"} 106 | ]} 107 | ]}. 108 | 109 | {ex_doc, [ 110 | {source_url, <<"https://github.com/g-andrade/tls_certificate_check">>}, 111 | {extras, [<<"README.md">>, <<"CHANGELOG.md">>, <<"LICENSE">>]}, 112 | {main, <<"readme">>}, 113 | {proglang, erlang} 114 | ]}. 115 | {hex, [ 116 | {doc, #{ 117 | provider => ex_doc 118 | }} 119 | ]}. 120 | -------------------------------------------------------------------------------- /test/tls_certificate_check_hardcoded_authorities_hotswap_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2021-2024 Guilherme Andrade 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a 4 | %% copy of this software and associated documentation files (the "Software"), 5 | %% to deal in the Software without restriction, including without limitation 6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | %% and/or sell copies of the Software, and to permit persons to whom the 8 | %% Software is furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | %% DEALINGS IN THE SOFTWARE. 20 | 21 | -module(tls_certificate_check_hardcoded_authorities_hotswap_SUITE). 22 | -compile(export_all). 23 | 24 | -include_lib("stdlib/include/assert.hrl"). 25 | 26 | %% ------------------------------------------------------------------ 27 | %% Setup 28 | %% ------------------------------------------------------------------ 29 | 30 | all() -> 31 | [code_swap_success, 32 | code_swap_failure 33 | ]. 34 | 35 | init_per_suite(Config) -> 36 | _ = application:load(tls_certificate_check), 37 | application:set_env(tls_certificate_check, use_otp_trusted_CAs, false), 38 | {ok, _} = application:ensure_all_started(tls_certificate_check), 39 | Config. 40 | 41 | end_per_suite(_Config) -> 42 | ok = application:stop(tls_certificate_check). 43 | 44 | %% ------------------------------------------------------------------ 45 | %% Test Cases 46 | %% ------------------------------------------------------------------ 47 | 48 | code_swap_success(_Config) -> 49 | EncodedAuthorities = tls_certificate_check_hardcoded_authorities:encoded_list(), 50 | SharedStateKeyBefore = tls_certificate_check_shared_state:latest_shared_state_key(), 51 | 52 | % existing list, twice 53 | NewEncodedAuthorities = <>, 54 | ok = file:write_file("tls_certificate_check_hardcoded_authorities_mock_value.txt", 55 | io_lib:format("~p.", [NewEncodedAuthorities])), 56 | try 57 | ?assertEqual(false, code:purge(tls_certificate_check_hardcoded_authorities)), 58 | ?assertMatch({module, _}, code:load_file(tls_certificate_check_hardcoded_authorities)), 59 | 60 | SharedStateKeyAfter = tls_certificate_check_shared_state:latest_shared_state_key(), 61 | ?assertNotEqual(SharedStateKeyBefore, SharedStateKeyAfter) % because the hotswap succeeded 62 | after 63 | ok = file:delete("tls_certificate_check_hardcoded_authorities_mock_value.txt") 64 | end. 65 | 66 | code_swap_failure(_Config) -> 67 | SharedStateKeyBefore = tls_certificate_check_shared_state:latest_shared_state_key(), 68 | 69 | % gibberish 70 | NewEncodedAuthorities = crypto:strong_rand_bytes(32), 71 | ok = file:write_file("tls_certificate_check_hardcoded_authorities_mock_value.txt", 72 | io_lib:format("~p.", [NewEncodedAuthorities])), 73 | try 74 | ?assertEqual(false, code:purge(tls_certificate_check_hardcoded_authorities)), 75 | ?assertEqual({error, on_load_failure}, code:load_file(tls_certificate_check_hardcoded_authorities)), 76 | 77 | SharedStateKeyAfter = tls_certificate_check_shared_state:latest_shared_state_key(), 78 | ?assertEqual(SharedStateKeyBefore, SharedStateKeyAfter) % because the hotswap failed 79 | after 80 | ok = file:delete("tls_certificate_check_hardcoded_authorities_mock_value.txt") 81 | end. 82 | -------------------------------------------------------------------------------- /src/tls_certificate_check_util.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2021-2024 Guilherme Andrade 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a 4 | %% copy of this software and associated documentation files (the "Software"), 5 | %% to deal in the Software without restriction, including without limitation 6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | %% and/or sell copies of the Software, and to permit persons to whom the 8 | %% Software is furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | %% DEALINGS IN THE SOFTWARE. 20 | 21 | %% @private 22 | -module(tls_certificate_check_util). 23 | 24 | %% ------------------------------------------------------------------ 25 | %% API Function Exports 26 | %% ------------------------------------------------------------------ 27 | 28 | -export([process_authorities/1, 29 | is_termination_reason_wholesome/1]). 30 | 31 | %% ------------------------------------------------------------------ 32 | %% API Function Definitions 33 | %% ------------------------------------------------------------------ 34 | 35 | -spec process_authorities(binary() | [public_key:der_encoded()]) 36 | -> {ok, [public_key:der_encoded(), ...]} 37 | | {error, no_authoritative_certificates_found} 38 | | {error, {failed_to_decode, {atom(), term(), list()}}} 39 | | {error, {certificate_encrypted, public_key:der_encoded()}} 40 | | {error, {unexpected_certificate_format, tuple()}}. 41 | process_authorities(<>) -> 42 | try public_key:pem_decode(EncodedAuthorities) of 43 | List when is_list(List) -> 44 | process_authorities(List) 45 | catch 46 | Class:Reason:Stacktrace -> 47 | {error, {failed_to_decode, {Class, Reason, Stacktrace}}} 48 | end; 49 | process_authorities([_|_] = AuthoritativeCertificateValues) -> 50 | authoritative_certificate_values(AuthoritativeCertificateValues); 51 | process_authorities([]) -> 52 | {error, no_authoritative_certificates_found}. 53 | 54 | -spec is_termination_reason_wholesome(term()) -> boolean(). 55 | is_termination_reason_wholesome(normal) -> 56 | true; 57 | is_termination_reason_wholesome(shutdown) -> 58 | true; 59 | is_termination_reason_wholesome({shutdown, _}) -> 60 | true; 61 | is_termination_reason_wholesome(_) -> 62 | false. 63 | 64 | %% ------------------------------------------------------------------ 65 | %% API Function Definitions 66 | %% ------------------------------------------------------------------ 67 | 68 | authoritative_certificate_values(AuthoritativeCertificates) -> 69 | authoritative_certificate_values_recur(AuthoritativeCertificates, []). 70 | 71 | authoritative_certificate_values_recur([Head | Next], ValuesAcc) -> 72 | case Head of 73 | {'Certificate', DerEncoded, not_encrypted} -> 74 | UpdatedValuesAcc = [DerEncoded | ValuesAcc], 75 | authoritative_certificate_values_recur(Next, UpdatedValuesAcc); 76 | {'Certificate', _, _} -> 77 | {error, {certificate_encrypted, Head}}; 78 | <> -> 79 | UpdatedValuesAcc = [DerEncoded | ValuesAcc], 80 | authoritative_certificate_values_recur(Next, UpdatedValuesAcc); 81 | Other -> 82 | {error, {unexpected_certificate_format, Other}} 83 | end; 84 | authoritative_certificate_values_recur([], ValuesAcc) -> 85 | Values = lists:reverse(ValuesAcc), 86 | {ok, Values}. 87 | 88 | -------------------------------------------------------------------------------- /test/tls_certificate_check_cross_signing_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2021-2024 Guilherme Andrade 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a 4 | %% copy of this software and associated documentation files (the "Software"), 5 | %% to deal in the Software without restriction, including without limitation 6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | %% and/or sell copies of the Software, and to permit persons to whom the 8 | %% Software is furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | %% DEALINGS IN THE SOFTWARE. 20 | 21 | -module(tls_certificate_check_cross_signing_SUITE). 22 | -compile(export_all). 23 | 24 | %% ------------------------------------------------------------------ 25 | %% Macros 26 | %% ------------------------------------------------------------------ 27 | 28 | -define(PEMS_PATH, "../../../../test/cross_signing"). 29 | 30 | -define(REPEAT_N, 20). 31 | 32 | %% ------------------------------------------------------------------ 33 | %% Setup 34 | %% ------------------------------------------------------------------ 35 | 36 | all() -> 37 | [{group, GroupName} || {GroupName, _Options, _TestCases} <- groups()]. 38 | 39 | groups() -> 40 | [{individual_tests, [{repeat, ?REPEAT_N}, shuffle], test_names()}]. 41 | 42 | test_names() -> 43 | [good_chain_with_expired_root_test, 44 | bad_chain_with_expired_root_test, 45 | cross_signing_with_one_recognized_ca_test, 46 | cross_signing_with_one_other_recognized_ca_test]. 47 | 48 | init_per_testcase(_TestConfig, Config) -> 49 | {ok, _} = application:ensure_all_started(tls_certificate_check), 50 | Config. 51 | 52 | end_per_testcase(_TestConfig, _Config) -> 53 | ok = application:stop(tls_certificate_check). 54 | 55 | %% ------------------------------------------------------------------ 56 | %% Test Cases 57 | %% ------------------------------------------------------------------ 58 | 59 | good_chain_with_expired_root_test(_Config) -> 60 | tls_certificate_check_test_utils:connect( 61 | ?PEMS_PATH, "good_ca_store_for_expiry.pem", 62 | chain, "localhost_chain_for_expiry.pem", 63 | fun ({ok, Socket}) -> 64 | ssl:close(Socket) 65 | end). 66 | 67 | -ifdef(EXPIRED_CAs_ARE_CONSIDERED_VALID). 68 | 69 | -ifdef(FLAKY_CROSS_SIGNING_VALIDATION). 70 | bad_chain_with_expired_root_test(_Config) -> 71 | {skip, "This test fails non-deterministically on the present OTP version"}. 72 | -else. 73 | bad_chain_with_expired_root_test(_Config) -> 74 | tls_certificate_check_test_utils:connect( 75 | ?PEMS_PATH, "bad_ca_store_for_expiry.pem", 76 | chain, "localhost_chain_for_expiry.pem", 77 | fun ({ok, Socket}) -> 78 | ssl:close(Socket) 79 | end). 80 | -endif. % ifdef(FLAKY_CROSS_SIGNING_VALIDATION 81 | 82 | -else. % ifdef(EXPIRED_CAs_ARE_CONSIDERED_VALID) 83 | bad_chain_with_expired_root_test(_Config) -> 84 | tls_certificate_check_test_utils:connect( 85 | ?PEMS_PATH, "bad_ca_store_for_expiry.pem", 86 | chain, "localhost_chain_for_expiry.pem", 87 | fun ({error, {tls_alert, {certificate_expired, _}}}) -> 88 | ok 89 | end). 90 | 91 | -endif. % -ifdef(EXPIRED_CAs_ARE_CONSIDERED_VALID) 92 | 93 | cross_signing_with_one_recognized_ca_test(_Config) -> 94 | tls_certificate_check_test_utils:connect( 95 | ?PEMS_PATH, "ca_store1_for_cross_signing.pem", 96 | chain, "localhost_chain_for_cross_signing.pem", 97 | fun ({ok, Socket}) -> 98 | ssl:close(Socket) 99 | end). 100 | 101 | -ifdef(FLAKY_CROSS_SIGNING_VALIDATION). 102 | cross_signing_with_one_other_recognized_ca_test(_Config) -> 103 | {skip, "This test fails non-deterministically on the present OTP version"}. 104 | 105 | -else. 106 | cross_signing_with_one_other_recognized_ca_test(_Config) -> 107 | tls_certificate_check_test_utils:connect( 108 | ?PEMS_PATH, "ca_store2_for_cross_signing.pem", 109 | chain, "localhost_chain_for_cross_signing.pem", 110 | fun ({ok, Socket}) -> 111 | ssl:close(Socket) 112 | end). 113 | 114 | -endif. % -ifdef(FLAKY_CROSS_SIGNING_VALIDATION) 115 | -------------------------------------------------------------------------------- /test/cross_signing/Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: CA_stores/good_ca_store_for_expiry.pem 3 | all: CA_stores/bad_ca_store_for_expiry.pem 4 | all: CA_stores/ca_store1_for_cross_signing.pem 5 | all: CA_stores/ca_store2_for_cross_signing.pem 6 | all: certificate_chains/localhost_chain_for_expiry.pem 7 | all: certificate_chains/localhost_chain_for_cross_signing.pem 8 | 9 | clean: 10 | find . -mindepth 2 -maxdepth 2 -type f \( -name '*.pem' -or -name '*.csr' -or -name '*.srl' \) -delete 11 | 12 | CA_stores/good_ca_store_for_expiry.pem: CAs/new_ca.pem 13 | CA_stores/good_ca_store_for_expiry.pem: CAs/expired_ca.pem 14 | CA_stores/good_ca_store_for_expiry.pem: 15 | cat \ 16 | CAs/expired_ca.pem \ 17 | CAs/new_ca.pem \ 18 | >CA_stores/good_ca_store_for_expiry.pem 19 | 20 | CA_stores/bad_ca_store_for_expiry.pem: CAs/expired_ca.pem 21 | CA_stores/bad_ca_store_for_expiry.pem: 22 | cat \ 23 | CAs/expired_ca.pem \ 24 | >CA_stores/bad_ca_store_for_expiry.pem 25 | 26 | CA_stores/ca_store1_for_cross_signing.pem: CAs/new_ca.pem 27 | cat \ 28 | CAs/new_ca.pem \ 29 | >CA_stores/ca_store1_for_cross_signing.pem 30 | 31 | CA_stores/ca_store2_for_cross_signing.pem: CAs/third_ca.pem 32 | cat \ 33 | CAs/third_ca.pem \ 34 | >CA_stores/ca_store2_for_cross_signing.pem 35 | 36 | certificate_chains/localhost_chain_for_expiry.pem: leaf_certificates/localhost.pem 37 | certificate_chains/localhost_chain_for_expiry.pem: intermediate_certificates/regular_intermediate_cert.pem 38 | certificate_chains/localhost_chain_for_expiry.pem: intermediate_certificates/cross_signed_bad_intermediate_cert.pem 39 | cat \ 40 | leaf_certificates/localhost.pem \ 41 | intermediate_certificates/regular_intermediate_cert.pem \ 42 | intermediate_certificates/cross_signed_bad_intermediate_cert.pem \ 43 | >certificate_chains/localhost_chain_for_expiry.pem 44 | 45 | certificate_chains/localhost_chain_for_cross_signing.pem: leaf_certificates/localhost.pem 46 | certificate_chains/localhost_chain_for_cross_signing.pem: intermediate_certificates/regular_intermediate_cert.pem 47 | certificate_chains/localhost_chain_for_cross_signing.pem: intermediate_certificates/cross_signed_good_intermediate_cert.pem 48 | cat \ 49 | leaf_certificates/localhost.pem \ 50 | intermediate_certificates/regular_intermediate_cert.pem \ 51 | intermediate_certificates/cross_signed_good_intermediate_cert.pem \ 52 | >certificate_chains/localhost_chain_for_cross_signing.pem 53 | 54 | leaf_certificates/localhost.pem: intermediate_certificates/regular_intermediate_cert.pem 55 | leaf_certificates/localhost.pem: leaf_certificates/localhost.csr 56 | faketime -f '-5y' openssl x509 \ 57 | -req \ 58 | -in leaf_certificates/localhost.csr \ 59 | -CA intermediate_certificates/regular_intermediate_cert.pem \ 60 | -CAkey intermediate_certificates/regular_intermediate_cert_key.pem \ 61 | -CAcreateserial \ 62 | -out leaf_certificates/localhost.pem \ 63 | -days 20075 \ 64 | -sha256 65 | 66 | intermediate_certificates/regular_intermediate_cert.pem: CAs/new_ca.pem 67 | intermediate_certificates/regular_intermediate_cert.pem: intermediate_certificates/regular_intermediate_cert.csr 68 | faketime -f '-5y' openssl x509 \ 69 | -req \ 70 | -in intermediate_certificates/regular_intermediate_cert.csr \ 71 | -extfile intermediate_ca.ext \ 72 | -extensions v3_intermediate_ca \ 73 | -CA CAs/new_ca.pem \ 74 | -CAkey CAs/new_ca_key.pem \ 75 | -CAcreateserial \ 76 | -out intermediate_certificates/regular_intermediate_cert.pem \ 77 | -days 20075 \ 78 | -sha256 79 | 80 | intermediate_certificates/cross_signed_bad_intermediate_cert.pem: CAs/expired_ca.pem 81 | intermediate_certificates/cross_signed_bad_intermediate_cert.pem: CAs/new_ca.csr 82 | faketime -f '-5y' openssl x509 \ 83 | -req \ 84 | -in CAs/new_ca.csr \ 85 | -extfile intermediate_ca.ext \ 86 | -extensions v3_intermediate_ca \ 87 | -CA CAs/expired_ca.pem \ 88 | -CAkey CAs/expired_ca_key.pem \ 89 | -CAcreateserial \ 90 | -out intermediate_certificates/cross_signed_bad_intermediate_cert.pem \ 91 | -days 20075 \ 92 | -sha256 93 | 94 | intermediate_certificates/cross_signed_good_intermediate_cert.pem: CAs/third_ca.pem 95 | intermediate_certificates/cross_signed_good_intermediate_cert.pem: CAs/new_ca.csr 96 | faketime -f '-5y' openssl x509 \ 97 | -req \ 98 | -in CAs/new_ca.csr \ 99 | -extfile intermediate_ca.ext \ 100 | -extensions v3_intermediate_ca \ 101 | -CA CAs/third_ca.pem \ 102 | -CAkey CAs/third_ca_key.pem \ 103 | -CAcreateserial \ 104 | -out intermediate_certificates/cross_signed_good_intermediate_cert.pem \ 105 | -days 20075 \ 106 | -sha256 107 | 108 | CAs/expired_ca.pem: CAs/expired_ca_key.pem 109 | faketime -f '-5y' openssl req -x509 \ 110 | -new -nodes \ 111 | -key CAs/expired_ca_key.pem \ 112 | -sha256 \ 113 | -days 1800 \ 114 | -subj "/CN=Expired CA" \ 115 | -out CAs/expired_ca.pem 116 | 117 | .PRECIOUS: %_ca.pem # prevents removal of what is considered an intermediate file (FIXME untested on macos) 118 | %_ca.pem: %_ca_key.pem 119 | faketime -f '-5y' openssl req -x509 \ 120 | -new -nodes \ 121 | -key $*_ca_key.pem \ 122 | -sha256 \ 123 | -days 20075 \ 124 | -subj "/CN=$(*F)_ca" \ 125 | -out $@ 126 | 127 | .PRECIOUS: %_key.pem # prevents removal of what is considered an intermediate file (FIXME untested on macos) 128 | %.csr: %_key.pem 129 | openssl req \ 130 | -new \ 131 | -key $*_key.pem \ 132 | -subj "/CN=$(*F)" \ 133 | -out $@ 134 | 135 | .PRECIOUS: %_key.pem # prevents removal of what is considered an intermediate file (FIXME untested on macos) 136 | %_key.pem: 137 | openssl genrsa -out $@ 2048 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tls\_certificate\_check 2 | 3 | [![Hex downloads](https://img.shields.io/hexpm/dt/tls_certificate_check.svg)](https://hex.pm/packages/tls_certificate_check) 4 | [![License](https://img.shields.io/hexpm/l/tls_certificate_check.svg)](https://github.com/g-andrade/tls_certificate_check/blob/master/LICENSE) 5 | [![Erlang Versions](https://img.shields.io/badge/Erlang%2FOTP-24%20to%2028-blue)](https://www.erlang.org) 6 | [![CI status](https://github.com/g-andrade/tls_certificate_check/actions/workflows/ci.yml/badge.svg)](https://github.com/g-andrade/tls_certificate_check/actions/workflows/ci.yml) 7 | [![Latest version](https://img.shields.io/hexpm/v/tls_certificate_check.svg?style=flat)](https://hex.pm/packages/tls_certificate_check) 8 | [![API reference](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/tls_certificate_check/) 9 | [![Last commit](https://img.shields.io/github/last-commit/g-andrade/tls_certificate_check.svg)](https://github.com/g-andrade/tls_certificate_check/commits/master) 10 | 11 | `tls_certificate_check` is a library for Erlang/OTP and Elixir that 12 | tries to make it easier to establish [more secure HTTPS 13 | connections](https://wiki.mozilla.org/index.php?title=CA/IncludedCertificates&redirect=no) 14 | in ordinary setups. 15 | 16 | Other kinds of TLS/SSL connections may also benefit from it. 17 | 18 | It blends a CA trust store with 19 | [ssl\_verify\_fun](https://github.com/deadtrickster/ssl_verify_fun.erl) 20 | to verify remote hostnames, 21 | as well as the boilerplate to validate [misordered 22 | certificate chains](https://github.com/elixir-mint/mint/issues/95). 23 | 24 | The 25 | [OTP-trusted CAs](https://www.erlang.org/doc/man/public_key.html#cacerts_get-0) 26 | (typically provided by the OS) are used on OTP 25+ unless unavailable or opted-out[^1], 27 | in which case `tls_certificate_check` falls back to a hardcoded [Mozilla's CA certificate 28 | store](https://curl.se/docs/caextract.html), as extracted by `curl`. 29 | When on OTP 24 or older, the lib will initialize using only the latter. 30 | 31 | The trusted authorities' certificates are loaded when the application 32 | starts and made available to the API through 33 | [`persistent_term`](https://erlang.org/doc/man/persistent_term.html)[^2]. After that, they can 34 | be explicitly overridden through the API. 35 | 36 | ### How to use 37 | 38 | #### Erlang 39 | 40 | ##### 1\. Import as a dependency 41 | 42 | rebar.config 43 | 44 | ``` erlang 45 | {deps, [ 46 | % [...] 47 | {tls_certificate_check, "~> 1.31"} 48 | ]}. 49 | ``` 50 | 51 | your\_application.app.src 52 | 53 | ``` erlang 54 | {applications, [ 55 | kernel, 56 | stdlib, 57 | % [...] 58 | tls_certificate_check 59 | ]} 60 | ``` 61 | 62 | ##### 2\. Make your connections safer 63 | 64 | ``` erlang 65 | Host = "example.com", 66 | Options = tls_certificate_check:options(Host), 67 | ssl:connect(Host, 443, Options, 5000) 68 | ``` 69 | 70 | #### Elixir 71 | 72 | ##### 1\. Import as a dependency 73 | 74 | mix.exs 75 | 76 | ``` elixir 77 | defp deps do 78 | [ 79 | # [...] 80 | {:tls_certificate_check, "~> 1.31"} 81 | ] 82 | end 83 | ``` 84 | 85 | ##### 2\. Make your connections safer 86 | 87 | ``` elixir 88 | host = "example.com" 89 | options = :tls_certificate_check.options(host) 90 | host |> String.to_charlist() |> :ssl.connect(443, options, 5000) 91 | ``` 92 | 93 | ### Advanced Use 94 | 95 | #### Overriding Trusted CAs 96 | 97 | ##### Erlang 98 | 99 | ```erlang 100 | Path = certifi:cacertfile(), 101 | tls_certificate_check:override_trusted_authorities({file, Path}) 102 | ``` 103 | 104 | ##### Elixir 105 | 106 | ```elixir 107 | path = CAStore.file_path() 108 | :tls_certificate_check.override_trusted_authorities({:file, path}) 109 | ``` 110 | 111 | ### API Reference 112 | 113 | The API reference can be found on 114 | [HexDocs](https://hexdocs.pm/tls_certificate_check/). 115 | 116 | ### Tested setup 117 | 118 | - Erlang/OTP 24 or newer 119 | - rebar3 120 | 121 | ### License 122 | 123 | MIT License 124 | 125 | Copyright (c) 2020-2024 Guilherme Andrade 126 | 127 | Permission is hereby granted, free of charge, to any person obtaining a 128 | copy of this software and associated documentation files (the 129 | "Software"), to deal in the Software without restriction, including 130 | without limitation the rights to use, copy, modify, merge, publish, 131 | distribute, sublicense, and/or sell copies of the Software, and to 132 | permit persons to whom the Software is furnished to do so, subject to 133 | the following conditions: 134 | 135 | The above copyright notice and this permission notice shall be included 136 | in all copies or substantial portions of the Software. 137 | 138 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 139 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 140 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 141 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 142 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 143 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 144 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 145 | 146 | --- 147 | 148 | [^1]: the use of OTP-trusted CAs can be controlled through the `use_otp_trusted_CAs` boolean 149 | option within application env config. 150 | 151 | [^2]: the persistent term key is derived from the CA store's own contents and existing keys 152 | are not erased until the app terminates gracefully - this minimizes the risk of an impactful 153 | global garbage collection. 154 | -------------------------------------------------------------------------------- /test/common_scenarios/Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: leaf_certificates/good_certificate.pem 3 | all: leaf_certificates/good_certificate_for_localhost2.pem 4 | all: leaf_certificates/expired_certificate.pem 5 | all: leaf_certificates/future_certificate.pem 6 | all: leaf_certificates/wrong.host.pem 7 | all: leaf_certificates/self_signed.pem 8 | all: leaf_certificates/unknown_ca.pem 9 | all: certificate_chains/misordered_chain.pem 10 | all: CA_stores/foobar.pem 11 | 12 | clean: 13 | find . -mindepth 2 -maxdepth 2 -type f \( -name '*.pem' -or -name '*.csr' -or -name '*.srl' \) -delete 14 | 15 | certificate_chains/misordered_chain.pem: CAs/foobar_ca.pem 16 | certificate_chains/misordered_chain.pem: intermediate_certificates/intermediate_cert.pem 17 | certificate_chains/misordered_chain.pem: leaf_certificates/good_certificate_with_intermediate_ca.pem 18 | cat \ 19 | leaf_certificates/good_certificate_with_intermediate_ca.pem \ 20 | CAs/foobar_ca.pem \ 21 | intermediate_certificates/intermediate_cert.pem \ 22 | >certificate_chains/misordered_chain.pem 23 | 24 | leaf_certificates/good_certificate.pem: CAs/foobar_ca.pem 25 | leaf_certificates/good_certificate.pem: leaf_certificates/localhost.csr 26 | faketime -f '-5y' openssl x509 \ 27 | -req \ 28 | -in leaf_certificates/localhost.csr \ 29 | -CA CAs/foobar_ca.pem \ 30 | -CAkey CAs/foobar_ca_key.pem \ 31 | -CAcreateserial \ 32 | -out $@ \ 33 | -days 20075 \ 34 | -sha256 35 | 36 | leaf_certificates/good_certificate_for_localhost2.pem: CAs/foobar_ca.pem 37 | leaf_certificates/good_certificate_for_localhost2.pem: leaf_certificates/localhost2.csr 38 | faketime -f '-5y' openssl x509 \ 39 | -req \ 40 | -in leaf_certificates/localhost2.csr \ 41 | -CA CAs/foobar_ca.pem \ 42 | -CAkey CAs/foobar_ca_key.pem \ 43 | -CAcreateserial \ 44 | -out $@ \ 45 | -days 20075 \ 46 | -sha256 47 | 48 | leaf_certificates/expired_certificate.pem: CAs/foobar_ca.pem 49 | leaf_certificates/expired_certificate.pem: leaf_certificates/localhost.csr 50 | faketime -f '-5y' openssl x509 \ 51 | -req \ 52 | -in leaf_certificates/localhost.csr \ 53 | -CA CAs/foobar_ca.pem \ 54 | -CAkey CAs/foobar_ca_key.pem \ 55 | -CAcreateserial \ 56 | -out $@ \ 57 | -days 1800 \ 58 | -sha256 59 | 60 | leaf_certificates/future_certificate.pem: CAs/foobar_ca.pem 61 | leaf_certificates/future_certificate.pem: leaf_certificates/localhost.csr 62 | faketime -f '+50y' openssl x509 \ 63 | -req \ 64 | -in leaf_certificates/localhost.csr \ 65 | -CA CAs/foobar_ca.pem \ 66 | -CAkey CAs/foobar_ca_key.pem \ 67 | -CAcreateserial \ 68 | -out $@ \ 69 | -days 1800 \ 70 | -sha256 71 | 72 | leaf_certificates/wrong.host.pem: CAs/foobar_ca.pem 73 | leaf_certificates/wrong.host.pem: leaf_certificates/wrong.host.csr 74 | faketime -f '-5y' openssl x509 \ 75 | -req \ 76 | -in leaf_certificates/wrong.host.csr \ 77 | -CA CAs/foobar_ca.pem \ 78 | -CAkey CAs/foobar_ca_key.pem \ 79 | -CAcreateserial \ 80 | -out $@ \ 81 | -days 20075 \ 82 | -sha256 83 | 84 | leaf_certificates/self_signed.pem: leaf_certificates/self_signed_key.pem 85 | faketime -f '-5y' openssl req -x509 \ 86 | -new -nodes \ 87 | -key leaf_certificates/self_signed_key.pem \ 88 | -sha256 \ 89 | -days 20075 \ 90 | -subj '/CN=localhost' \ 91 | -out $@ 92 | 93 | leaf_certificates/unknown_ca.pem: CAs/another_ca.pem 94 | leaf_certificates/unknown_ca.pem: leaf_certificates/localhost.csr 95 | faketime -f '-5y' openssl x509 \ 96 | -req \ 97 | -in leaf_certificates/localhost.csr \ 98 | -CA CAs/another_ca.pem \ 99 | -CAkey CAs/another_ca_key.pem \ 100 | -CAcreateserial \ 101 | -out $@ \ 102 | -days 20075 \ 103 | -sha256 104 | 105 | intermediate_certificates/intermediate_cert.pem: CAs/foobar_ca.pem 106 | intermediate_certificates/intermediate_cert.pem: intermediate_certificates/intermediate_cert.csr 107 | faketime -f '-5y' openssl x509 \ 108 | -req \ 109 | -in intermediate_certificates/intermediate_cert.csr \ 110 | -extfile intermediate_ca.ext \ 111 | -extensions v3_intermediate_ca \ 112 | -CA CAs/foobar_ca.pem \ 113 | -CAkey CAs/foobar_ca_key.pem \ 114 | -CAcreateserial \ 115 | -out intermediate_certificates/intermediate_cert.pem \ 116 | -days 20075 \ 117 | -sha256 118 | 119 | leaf_certificates/good_certificate_with_intermediate_ca.pem: intermediate_certificates/intermediate_cert.pem 120 | leaf_certificates/good_certificate_with_intermediate_ca.pem: leaf_certificates/localhost.csr 121 | faketime -f '-5y' openssl x509 \ 122 | -req \ 123 | -in leaf_certificates/localhost.csr \ 124 | -CA intermediate_certificates/intermediate_cert.pem \ 125 | -CAkey intermediate_certificates/intermediate_cert_key.pem \ 126 | -CAcreateserial \ 127 | -out $@ \ 128 | -days 20075 \ 129 | -sha256 130 | 131 | CA_stores/foobar.pem: CAs/foobar_ca.pem 132 | cat \ 133 | CAs/foobar_ca.pem \ 134 | >CA_stores/foobar.pem 135 | 136 | 137 | .PRECIOUS: %_ca.pem # prevents removal of what is considered an intermediate file (FIXME untested on macos) 138 | %_ca.pem: %_ca_key.pem 139 | faketime -f '-5y' openssl req -x509 \ 140 | -new -nodes \ 141 | -key $*_ca_key.pem \ 142 | -sha256 \ 143 | -days 20075 \ 144 | -subj "/CN=$(*F)_ca" \ 145 | -out $@ 146 | 147 | .PRECIOUS: %_key.pem # prevents removal of what is considered an intermediate file (FIXME untested on macos) 148 | %.csr: %_key.pem 149 | openssl req \ 150 | -new \ 151 | -key $*_key.pem \ 152 | -subj "/CN=$(*F)" \ 153 | -out $@ 154 | 155 | .PRECIOUS: %_key.pem # prevents removal of what is considered an intermediate file (FIXME untested on macos) 156 | %_key.pem: 157 | openssl genrsa -out $@ 2048 158 | -------------------------------------------------------------------------------- /test/tls_certificate_check_override_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2023-2024 Guilherme Andrade 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a 4 | %% copy of this software and associated documentation files (the "Software"), 5 | %% to deal in the Software without restriction, including without limitation 6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | %% and/or sell copies of the Software, and to permit persons to whom the 8 | %% Software is furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | %% DEALINGS IN THE SOFTWARE. 20 | 21 | -module(tls_certificate_check_override_SUITE). 22 | -compile(export_all). 23 | 24 | -include_lib("stdlib/include/assert.hrl"). 25 | 26 | %% ------------------------------------------------------------------ 27 | %% Macros 28 | %% ------------------------------------------------------------------ 29 | 30 | -define(PEMS_PATH, "../../../../test/common_scenarios"). 31 | 32 | %% ------------------------------------------------------------------ 33 | %% Setup 34 | %% ------------------------------------------------------------------ 35 | 36 | all() -> 37 | [empty_override_test, 38 | badly_encoded_override_test, 39 | non_existent_file_test, 40 | app_stopped_test, 41 | decoded_test, 42 | certifi_test, 43 | castore_test]. 44 | 45 | init_per_testcase(TestCase, Config) -> 46 | _ = TestCase =/= app_stopped_test 47 | andalso begin {ok, _} = application:ensure_all_started(tls_certificate_check) end, 48 | Config. 49 | 50 | end_per_testcase(TestCase, _Config) -> 51 | _ = TestCase =/= app_stopped_test 52 | andalso begin ok = application:stop(tls_certificate_check) end, 53 | ok. 54 | 55 | %% ------------------------------------------------------------------ 56 | %% Test Cases 57 | %% ------------------------------------------------------------------ 58 | 59 | % 60 | % The success path is tested in `tls_certificate_check_test_utils:connect()' 61 | % 62 | 63 | empty_override_test(_Config) -> 64 | ?assertThrow({failed_to_process_authorities, no_authoritative_certificates_found}, 65 | tls_certificate_check:override_trusted_authorities({encoded, <<>>})), 66 | assert_good_conn(). 67 | 68 | badly_encoded_override_test(_Config) -> 69 | EncodedAuthorities = << 70 | "GlobalSign Root CA\n", 71 | "==================\n", 72 | "-----BEGIN CERTIFICATE-----\n", 73 | "MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx\n", 74 | "GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds\n", 75 | "b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV\n", 76 | "BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD\n", 77 | "VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa\n", 78 | "DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc\n", 79 | "THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb\n", 80 | "Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP\n", 81 | "c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX\n", 82 | "gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n", 83 | "HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF\n" 84 | >>, 85 | ?assertThrow({failed_to_process_authorities, {failed_to_decode, _}}, 86 | tls_certificate_check:override_trusted_authorities({encoded, EncodedAuthorities})), 87 | 88 | assert_good_conn(). 89 | 90 | non_existent_file_test(_Config) -> 91 | Filename = base64:encode(crypto:strong_rand_bytes(32)), 92 | ?assertThrow({read_file, _}, 93 | tls_certificate_check:override_trusted_authorities({file, Filename})), 94 | 95 | assert_good_conn(). 96 | 97 | app_stopped_test(_Config) -> 98 | ?assertThrow({application_either_not_started_or_not_ready, tls_certificate_check}, 99 | tls_certificate_check:override_trusted_authorities({encoded, <<>>})). 100 | 101 | decoded_test(_Config) -> 102 | CAs = tls_certificate_check:trusted_authorities(), 103 | ok = tls_certificate_check:override_trusted_authorities(CAs), 104 | assert_good_conn(). 105 | 106 | certifi_test(_Config) -> 107 | {ok, _} = application:ensure_all_started(certifi), 108 | try 109 | do_file_test(fun certifi:cacertfile/0) 110 | after 111 | ok = application:stop(certifi) 112 | end. 113 | 114 | castore_test(_Config) -> 115 | case application:ensure_all_started(castore) of 116 | {ok, _} -> 117 | try 118 | do_file_test(fun 'Elixir.CAStore':file_path/0) 119 | after 120 | ok = application:stop(castore) 121 | end; 122 | {error, Reason} -> 123 | {skip, {"Elixir's CAStore not available", Reason}} 124 | end. 125 | 126 | %% ------------------------------------------------------------------ 127 | %% Internal 128 | %% ------------------------------------------------------------------ 129 | 130 | do_file_test(PathFun) -> 131 | ok = tls_certificate_check:override_trusted_authorities({file, PathFun()}), 132 | assert_good_conn(). 133 | 134 | assert_good_conn() -> 135 | tls_certificate_check_test_utils:connect( 136 | ?PEMS_PATH, "foobar.pem", 137 | leaf, "good_certificate.pem", 138 | fun ({ok, Socket}) -> 139 | ssl:close(Socket) 140 | end). 141 | -------------------------------------------------------------------------------- /test/tls_certificate_check_test_utils.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2021-2024 Guilherme Andrade 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a 4 | %% copy of this software and associated documentation files (the "Software"), 5 | %% to deal in the Software without restriction, including without limitation 6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | %% and/or sell copies of the Software, and to permit persons to whom the 8 | %% Software is furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | %% DEALINGS IN THE SOFTWARE. 20 | 21 | -module(tls_certificate_check_test_utils). 22 | 23 | %% ------------------------------------------------------------------ 24 | %% API Function Exports 25 | %% ------------------------------------------------------------------ 26 | 27 | -export([connect/5, connect/6]). 28 | 29 | %% ------------------------------------------------------------------ 30 | %% API Function Definitions 31 | %% ------------------------------------------------------------------ 32 | 33 | connect(PemsPath, AuthoritiesFilename, ChainOrLeaf, CertsConf, Fun) -> 34 | connect(PemsPath, AuthoritiesFilename, ChainOrLeaf, CertsConf, Fun, 35 | _Opts = []). 36 | 37 | connect(PemsPath, AuthoritiesFilename, ChainOrLeaf, CertsConf, KeyFilename, Fun) 38 | when is_function(Fun) -> 39 | connect(PemsPath, AuthoritiesFilename, ChainOrLeaf, CertsConf, Fun, 40 | _Opts = [{key, KeyFilename}]); 41 | connect(PemsPath, AuthoritiesFilename, ChainOrLeaf, CertsConf, Fun, 42 | Opts) -> 43 | KeyConf = proplists:get_value(key, Opts, "localhost_key.pem"), 44 | AuthoritiesPath = filename:join([PemsPath, "CA_stores", AuthoritiesFilename]), 45 | ok = tls_certificate_check:override_trusted_authorities({file, AuthoritiesPath}), 46 | 47 | {ListenSocket, Port, AcceptorPid} = start_server_with_chain(PemsPath, ChainOrLeaf, 48 | CertsConf, 49 | KeyConf), 50 | try 51 | ConnectRes = connect(Opts, Port), 52 | _ = Fun(ConnectRes) 53 | after 54 | stop_ssl_acceptor(AcceptorPid), 55 | _ = ssl:close(ListenSocket) 56 | end. 57 | 58 | %% ------------------------------------------------------------------ 59 | %% Internal Function Definitions 60 | %% ------------------------------------------------------------------ 61 | 62 | connect(Opts, Port) -> 63 | Timeout = timer:seconds(5), 64 | 65 | case proplists:get_value(hostname, Opts) of 66 | undefined -> 67 | Hostname = "localhost", 68 | Options = tls_certificate_check:options(Hostname), 69 | ssl:connect(Hostname, Port, Options, Timeout); 70 | {Hostname, IpAddress} -> 71 | Options = tls_certificate_check:options(Hostname), 72 | case gen_tcp:connect(IpAddress, Port, [], Timeout) of 73 | {ok, TcpSocket} -> 74 | ssl:connect(TcpSocket, Options, Timeout); 75 | {error, _} = Error -> 76 | Error 77 | end 78 | end. 79 | 80 | start_server_with_chain(PemsPath, ChainOrLeaf, CertsConf, KeyConf) -> 81 | Options = server_options(PemsPath, ChainOrLeaf, CertsConf, KeyConf), 82 | ct:pal("server options: ~p", [Options]), 83 | 84 | {ok, ListenSocket} = ssl:listen(_Port = 0, Options), 85 | {ok, {_Address, Port}} = ssl:sockname(ListenSocket), 86 | AcceptorPid = start_ssl_acceptor(ListenSocket), 87 | {ListenSocket, Port, AcceptorPid}. 88 | 89 | server_options(PemsPath, ChainOrLeaf, CertsConf, KeyConf) -> 90 | [{ip, {127, 0, 0, 1}}, 91 | {reuseaddr, true} 92 | | certs_options(PemsPath, ChainOrLeaf, CertsConf, KeyConf)]. 93 | 94 | certs_options(PemsPath, ChainOrLeaf, CertFilename, KeyFilename) 95 | when is_list(CertFilename), is_list(KeyFilename) -> 96 | cert_options_for_paths(PemsPath, ChainOrLeaf, CertFilename, KeyFilename); 97 | certs_options(PemsPath, ChainOrLeaf, 98 | {multiple, CertFilenames}, 99 | {multiple, KeyFilenames}) -> 100 | [{sni_hosts, maps:fold( 101 | fun (Hostname, CertFilename, Acc) -> 102 | KeyFilename = maps:get(Hostname, KeyFilenames), 103 | Opts = cert_options_for_paths(PemsPath, ChainOrLeaf, CertFilename, KeyFilename), 104 | [{Hostname, Opts} | Acc] 105 | end, 106 | _Acc0 = [], 107 | CertFilenames)}]. 108 | 109 | cert_options_for_paths(PemsPath, ChainOrLeaf, CertFilename, KeyFilename) -> 110 | ChainOrLeafDir = chain_or_leaf_dir(ChainOrLeaf), 111 | CertsPath = filename:join([PemsPath, ChainOrLeafDir, CertFilename]), 112 | KeyPath = filename:join([PemsPath, "leaf_certificates", KeyFilename]), 113 | 114 | [{certfile, CertsPath}, 115 | % Ugh: http://erlang.org/pipermail/erlang-questions/2020-May/099521.html 116 | {cacertfile, CertsPath}, 117 | {keyfile, KeyPath}]. 118 | 119 | chain_or_leaf_dir(chain) -> 120 | "certificate_chains"; 121 | chain_or_leaf_dir(leaf) -> 122 | "leaf_certificates". 123 | 124 | start_ssl_acceptor(ListenSocket) -> 125 | spawn_link(fun () -> run_ssl_acceptor(ListenSocket) end). 126 | 127 | run_ssl_acceptor(ListenSocket) -> 128 | receive 129 | stop -> exit(normal) 130 | after 131 | 0 -> ok 132 | end, 133 | 134 | case ssl:transport_accept(ListenSocket, _Timeout = 100) of 135 | {ok, Transportsocket} -> 136 | _ = ssl:handshake(Transportsocket), 137 | run_ssl_acceptor(ListenSocket); 138 | {error, Reason} 139 | when Reason =:= timeout; Reason =:= closed -> 140 | run_ssl_acceptor(ListenSocket) 141 | end. 142 | 143 | stop_ssl_acceptor(AcceptorPid) -> 144 | AcceptorPid ! stop. 145 | -------------------------------------------------------------------------------- /test/tls_certificate_check_options_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2020-2024 Guilherme Andrade 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a 4 | %% copy of this software and associated documentation files (the "Software"), 5 | %% to deal in the Software without restriction, including without limitation 6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | %% and/or sell copies of the Software, and to permit persons to whom the 8 | %% Software is furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | %% DEALINGS IN THE SOFTWARE. 20 | 21 | -module(tls_certificate_check_options_SUITE). 22 | -compile(export_all). 23 | 24 | -include_lib("stdlib/include/assert.hrl"). 25 | 26 | %% ------------------------------------------------------------------ 27 | %% Macros 28 | %% ------------------------------------------------------------------ 29 | 30 | -define(PEMS_PATH, "../../../../test/common_scenarios"). 31 | 32 | -ifdef(MISMATCHED_SNI_FAILS_HANDSHAKE). 33 | -define(MISMATCHED_SNI_ERROR_REASON_PATTERN, {tls_alert, {handshake_failure, _}}). 34 | -else. 35 | -ifdef(MISMATCHED_SNI_CLOSES_CONN). 36 | -define(MISMATCHED_SNI_ERROR_REASON_PATTERN, closed). 37 | -else. 38 | -define(MISMATCHED_SNI_ERROR_REASON_PATTERN, {tls_alert, {illegal_parameter, _}}). 39 | -endif. 40 | -endif. 41 | 42 | %% ------------------------------------------------------------------ 43 | %% Setup 44 | %% ------------------------------------------------------------------ 45 | 46 | all() -> 47 | [real_certificate_test, 48 | good_certificate_test, 49 | sni_test, 50 | expired_certificate_test, 51 | future_certificate_test, 52 | wrong_host_certificate_test, 53 | self_signed_certificate_test, 54 | unknown_ca_test, 55 | misordered_chain_test]. 56 | 57 | init_per_suite(Config) -> 58 | {ok, _} = application:ensure_all_started(tls_certificate_check), 59 | Config. 60 | 61 | end_per_suite(_Config) -> 62 | ok = application:stop(tls_certificate_check). 63 | 64 | %% ------------------------------------------------------------------ 65 | %% Test Cases 66 | %% ------------------------------------------------------------------ 67 | 68 | real_certificate_test(_Config) -> 69 | {ok, _} = application:ensure_all_started(inets), 70 | try 71 | URLs = shuffle_list(["https://example.com", 72 | "https://www.google.com", 73 | "https://www.meta.com"]), 74 | real_certificate_test_recur(URLs) 75 | after 76 | application:stop(inets) 77 | end. 78 | 79 | good_certificate_test(_Config) -> 80 | tls_certificate_check_test_utils:connect( 81 | ?PEMS_PATH, "foobar.pem", 82 | leaf, "good_certificate.pem", 83 | fun ({ok, Socket}) -> 84 | ssl:close(Socket) 85 | end). 86 | 87 | sni_test(_Config) -> 88 | Certs = {multiple, #{ 89 | "localhost" => "good_certificate.pem", 90 | "localhost2" => "good_certificate_for_localhost2.pem" 91 | }}, 92 | Keys = {multiple, #{ 93 | "localhost" => "localhost_key.pem", 94 | "localhost2" => "localhost2_key.pem" 95 | }}, 96 | 97 | % Good hostname A 98 | % 99 | tls_certificate_check_test_utils:connect( 100 | ?PEMS_PATH, "foobar.pem", 101 | leaf, Certs, 102 | fun ({ok, Socket}) -> 103 | ssl:close(Socket) 104 | end, 105 | [{key, Keys}]), 106 | 107 | % Good hostname B 108 | % 109 | tls_certificate_check_test_utils:connect( 110 | ?PEMS_PATH, "foobar.pem", 111 | leaf, Certs, 112 | fun ({ok, Socket}) -> 113 | ssl:close(Socket) 114 | end, 115 | [{key, Keys}, 116 | {hostname, {"localhost2", {127, 0, 0, 1}}}]), 117 | 118 | % Bad hostname 119 | % 120 | tls_certificate_check_test_utils:connect( 121 | ?PEMS_PATH, "foobar.pem", 122 | leaf, Certs, 123 | fun ({error, ?MISMATCHED_SNI_ERROR_REASON_PATTERN}) -> 124 | ok 125 | end, 126 | [{key, Keys}, 127 | {hostname, {"localhost3", {127, 0, 0, 1}}}]). 128 | 129 | expired_certificate_test(_Config) -> 130 | tls_certificate_check_test_utils:connect( 131 | ?PEMS_PATH, "foobar.pem", 132 | leaf, "expired_certificate.pem", 133 | fun ({error, {tls_alert, {certificate_expired, _}}}) -> 134 | ok 135 | end). 136 | 137 | future_certificate_test(_Config) -> 138 | tls_certificate_check_test_utils:connect( 139 | ?PEMS_PATH, "foobar.pem", 140 | leaf, "future_certificate.pem", 141 | fun ({error, {tls_alert, {certificate_expired, _}}}) -> 142 | ok 143 | end). 144 | 145 | wrong_host_certificate_test(_Config) -> 146 | tls_certificate_check_test_utils:connect( 147 | ?PEMS_PATH, "foobar.pem", 148 | leaf, "wrong.host.pem", "wrong.host_key.pem", 149 | fun ({error, {tls_alert, {handshake_failure, _}}}) -> 150 | ok 151 | end). 152 | 153 | self_signed_certificate_test(_Config) -> 154 | tls_certificate_check_test_utils:connect( 155 | ?PEMS_PATH, "foobar.pem", 156 | leaf, "self_signed.pem", "self_signed_key.pem", 157 | fun ({error, {tls_alert, {bad_certificate, _}}}) -> 158 | ok 159 | end). 160 | 161 | unknown_ca_test(_Config) -> 162 | tls_certificate_check_test_utils:connect( 163 | ?PEMS_PATH, "foobar.pem", 164 | leaf, "unknown_ca.pem", 165 | fun ({error, {tls_alert, {unknown_ca, _}}}) -> 166 | ok 167 | end). 168 | 169 | misordered_chain_test(_Config) -> 170 | tls_certificate_check_test_utils:connect( 171 | ?PEMS_PATH, "foobar.pem", 172 | chain, "misordered_chain.pem", 173 | fun ({ok, Socket}) -> 174 | ssl:close(Socket) 175 | end). 176 | 177 | %% ------------------------------------------------------------------ 178 | %% Internal 179 | %% ------------------------------------------------------------------ 180 | 181 | real_certificate_test_recur([Url | Next]) -> 182 | ct:pal("Trying ~p", [Url]), 183 | Headers = [{"connection", "close"}], 184 | HttpOpts = [{ssl, tls_certificate_check:options(Url)}], 185 | Opts = [], 186 | 187 | case httpc:request(head, {Url, Headers}, HttpOpts, Opts) of 188 | {ok, {{_, StatusCode, _}, _, _}} 189 | when is_integer(StatusCode) -> 190 | ok; 191 | {error, Reason} -> 192 | ?assertNotMatch({error, {failed_connect, [{to_address, {_, _}}, 193 | {inet, [inet], {tls_alert, _}}]}}, 194 | Reason), 195 | ct:pal("Failed: ~p", [Reason]), 196 | real_certificate_test_recur(Next) 197 | end; 198 | real_certificate_test_recur([]) -> 199 | error('All test URLs are down (or we have no internet access)'). 200 | 201 | shuffle_list(List) -> 202 | Weighed = [{rand:uniform(), V} || V <- List], 203 | Sorted = lists:sort(Weighed), 204 | [V || {_, V} <- Sorted]. 205 | -------------------------------------------------------------------------------- /src/tls_certificate_check.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2020-2024 Guilherme Andrade 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a 4 | %% copy of this software and associated documentation files (the "Software"), 5 | %% to deal in the Software without restriction, including without limitation 6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | %% and/or sell copies of the Software, and to permit persons to whom the 8 | %% Software is furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | %% DEALINGS IN THE SOFTWARE. 20 | 21 | %% @doc Main API 22 | -module(tls_certificate_check). 23 | 24 | -ifdef(TEST). 25 | -include_lib("eunit/include/eunit.hrl"). 26 | -endif. 27 | 28 | %% ------------------------------------------------------------------ 29 | %% API Function Exports 30 | %% ------------------------------------------------------------------ 31 | 32 | -export([options/1, 33 | trusted_authorities/0, 34 | override_trusted_authorities/1]). 35 | 36 | -ignore_xref( 37 | [options/1, 38 | trusted_authorities/0, 39 | override_trusted_authorities/1]). 40 | 41 | %% ------------------------------------------------------------------ 42 | %% Macro Definitions 43 | %% ------------------------------------------------------------------ 44 | 45 | % Same as OpenSSL. 46 | % See: https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_verify_depth.html 47 | -define(DEFAULT_MAX_CERTIFICATE_CHAIN_DEPTH, 100). 48 | 49 | %% ------------------------------------------------------------------ 50 | %% Record and Type Definitions 51 | %% ------------------------------------------------------------------ 52 | 53 | -type option() :: ssl:tls_client_option(). 54 | -export_type([option/0]). 55 | 56 | -type override_source() 57 | :: {file, Path :: file:name_all()} 58 | | {encoded, binary()} 59 | | (CAs :: [public_key:der_encoded()]) 60 | . 61 | -export_type([override_source/0]). 62 | 63 | %% ------------------------------------------------------------------ 64 | %% API Function Definitions 65 | %% ------------------------------------------------------------------ 66 | 67 | %% @doc Returns the list of `ssl:connect' options 68 | %% necessary to validate the server certificate against 69 | %% a list of trusted authorities, as well as to verify 70 | %% whether the server hostname matches one in the server 71 | %% certificate. 72 | %% 73 | %%
    74 | %%
  • `Target' can be either a hostname or an HTTP URL, as `iodata()'
  • 75 | %%
76 | -spec options(Target) -> Options 77 | when Target :: Hostname | URL, 78 | Hostname :: iodata(), 79 | URL :: iodata(), 80 | Options :: [option()]. 81 | options(Target) -> 82 | try target_to_hostname(Target) of 83 | Hostname -> 84 | CAs = trusted_authorities(), 85 | CertificateVerificationFunOptions = [{check_hostname, Hostname}], 86 | CertificateVerificationFun = {fun ssl_verify_hostname:verify_fun/3, 87 | CertificateVerificationFunOptions}, 88 | 89 | % Required for OTP 23 as it fixed TLS hostname validation. 90 | % See: https://bugs.erlang.org/browse/ERL-1232 91 | Protocol = https, 92 | HostnameMatchFun = public_key:pkix_verify_hostname_match_fun(Protocol), 93 | 94 | [{verify, verify_peer}, 95 | {depth, ?DEFAULT_MAX_CERTIFICATE_CHAIN_DEPTH}, 96 | {cacerts, CAs}, 97 | {verify_fun, CertificateVerificationFun}, 98 | {partial_chain, fun tls_certificate_check_shared_state:find_trusted_authority/1}, 99 | {customize_hostname_check, [{match_fun, HostnameMatchFun}]} 100 | | maybe_sni_opts(Hostname)] 101 | catch 102 | http_target -> 103 | [] 104 | end. 105 | 106 | %% @doc Returns the list of trusted authorities. 107 | -spec trusted_authorities() -> CAs 108 | when CAs :: [public_key:der_encoded(), ...]. 109 | trusted_authorities() -> 110 | tls_certificate_check_shared_state:authoritative_certificate_values(). 111 | 112 | %% @doc Overrides the trusted authorities with a custom source. 113 | -spec override_trusted_authorities(From) -> ok 114 | when From :: override_source(). 115 | override_trusted_authorities(Source) -> 116 | case try_overriding_trusted_authorities(Source) of 117 | ok -> 118 | ok; 119 | {error, Reason} -> 120 | throw(Reason) 121 | end. 122 | 123 | %% ------------------------------------------------------------------ 124 | %% Internal Function Definitions 125 | %% ------------------------------------------------------------------ 126 | 127 | target_to_hostname(Target) -> 128 | BinaryTarget = iolist_to_binary(Target), 129 | case uri_string:parse(BinaryTarget) of 130 | #{scheme := <<"http">>} -> 131 | throw(http_target); 132 | #{host := Hostname} -> 133 | binary_to_list(Hostname); 134 | _ -> 135 | binary_to_list(BinaryTarget) 136 | end. 137 | 138 | -spec try_overriding_trusted_authorities(From) -> ok | {error, Reason} 139 | when From :: override_source(), 140 | Reason :: term(). 141 | try_overriding_trusted_authorities({file, Path} = OverrideSource) -> 142 | case file:read_file(Path) of 143 | {ok, EncodedAuthorities} -> 144 | try_overriding_trusted_authorities(_Source = {override, OverrideSource}, 145 | EncodedAuthorities); 146 | {error, Reason} -> 147 | {error, {read_file, #{path => Path, why => Reason}}} 148 | end; 149 | try_overriding_trusted_authorities({encoded, <>}) -> 150 | try_overriding_trusted_authorities(_Source = {override, encoded_binary}, 151 | EncodedAuthorities); 152 | try_overriding_trusted_authorities(Authorities) when is_list(Authorities) -> 153 | try_overriding_trusted_authorities(_Source = {override, list_of_cas}, 154 | Authorities). 155 | 156 | try_overriding_trusted_authorities(Source, UnprocessedAuthorities) -> 157 | case tls_certificate_check_shared_state:maybe_update_shared_state(Source, 158 | UnprocessedAuthorities) 159 | of 160 | noproc -> 161 | {error, {application_either_not_started_or_not_ready, tls_certificate_check}}; 162 | Other -> 163 | Other 164 | end. 165 | 166 | maybe_sni_opts(Hostname) -> 167 | case inet:parse_address(Hostname) of 168 | {ok, _IpAddress} -> 169 | % "Literal IPv4 and IPv6 addresses are not permitted in HostName" 170 | % * https://www.ietf.org/rfc/rfc4366.html#section-3.1 171 | []; 172 | {error, einval} -> 173 | % This probably doesn't cover IDNs... 174 | [{server_name_indication, Hostname}] 175 | end. 176 | 177 | %% ------------------------------------------------------------------ 178 | %% Unit Test Definitions 179 | %% ------------------------------------------------------------------ 180 | -ifdef(TEST). 181 | 182 | trusted_authorities_is_exported_test() -> 183 | {ok, _} = application:ensure_all_started(tls_certificate_check), 184 | ?assertMatch([_|_], ?MODULE:trusted_authorities()). 185 | 186 | http_target_test() -> 187 | {ok, _} = application:ensure_all_started(tls_certificate_check), 188 | ?assertEqual([], ?MODULE:options("http://example.com/")). 189 | 190 | https_target_test() -> 191 | {ok, _} = application:ensure_all_started(tls_certificate_check), 192 | ?assertMatch([_|_], ?MODULE:options("https://example.com/")). 193 | 194 | generic_tls_target_test() -> 195 | {ok, _} = application:ensure_all_started(tls_certificate_check), 196 | ?assertMatch([_|_], ?MODULE:options("example.com")). 197 | 198 | https_and_generic_tls_targets_equivalence_test() -> 199 | ?assertEqual( 200 | ?MODULE:options("example.com"), 201 | ?MODULE:options("https://example.com/") 202 | ). 203 | 204 | sni_restrictions_test() -> 205 | % Ip addresses have no SNI 206 | ?assertEqual( 207 | false, 208 | lists:keyfind(server_name_indication, 1, 209 | ?MODULE:options("93.184.216.34")) 210 | ), 211 | ?assertEqual( 212 | false, 213 | lists:keyfind(server_name_indication, 1, 214 | ?MODULE:options("2606:2800:220:1:248:1893:25c8:1946")) 215 | ), 216 | 217 | % Regular hostnames do 218 | ?assertMatch( 219 | {server_name_indication, _}, 220 | lists:keyfind(server_name_indication, 1, 221 | ?MODULE:options("example.com")) 222 | ). 223 | 224 | -endif. % -ifdef(TEST). 225 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.31.0] - 2025-12-02 9 | 10 | ### Changed 11 | 12 | - module with bundled CAs to latest as of 2025/12/02, 04:12 UTC 13 | (source: https://curl.se/ca/cacert.pem) 14 | 15 | ### Removed 16 | 17 | - [certificate authority] CommScope Public Trust ECC Root-02 18 | - [certificate authority] CommScope Public Trust ECC Root-01 19 | - [certificate authority] CommScope Public Trust RSA Root-01 20 | - [certificate authority] CommScope Public Trust RSA Root-02 21 | 22 | ## [1.30.0] - 2025-11-05 23 | 24 | ### Added 25 | 26 | - [certificate authority] OISTE Server Root ECC G1 27 | - [certificate authority] OISTE Server Root RSA G1 28 | - OTP 28.1 to CI 29 | 30 | ### Changed 31 | 32 | - module with bundled CAs to latest as of 2025/11/04, 04:12 UTC 33 | (source: https://curl.se/ca/cacert.pem) 34 | 35 | ### Fixed 36 | 37 | - throwing of `application_either_not_started_or_not_ready` when this lib is 38 | used as an _implicit_ dependency (i.e. called before it has started or 39 | finished initializing) 40 | 41 | ## [1.29.0] - 2025-08-14 42 | 43 | ### Added 44 | 45 | - OTP 28.0 to CI 46 | - OTP 27.3 to CI 47 | - [certificate authority] swisssign rsa tls root ca 2022 - 1 48 | - [certificate authority] trustasia tls ecc root ca 49 | - [certificate authority] trustasia tls rsa root ca 50 | 51 | ### Changed 52 | 53 | - module with bundled CAs to latest as of 2025/08/12, 03:12 UTC 54 | (source: https://curl.se/ca/cacert.pem) 55 | 56 | ## [1.28.0] - 2025-05-23 57 | 58 | ### Changed 59 | 60 | - module with bundled CAs to latest as of 2025/05/20, 03:12 UTC 61 | (source: https://curl.se/ca/cacert.pem) 62 | - oldest OTP version supported from 22 to 24 63 | - CI from Ubuntu 20.04 to 22.04 64 | 65 | ### Removed 66 | 67 | - [certificate authority] AAA Certificate Services 68 | - [certificate authority] baltimore cybertrust root 69 | - [certificate authority] entrust.net certification authority (2048) 70 | - [certificate authority] globalsign root ca 71 | - [certificate authority] go daddy class 2 certification authority 72 | - [certificate authority] starfield class 2 certification authority 73 | - [certificate authority] xramp global certification authority 74 | 75 | ## [1.27.0] - 2025-02-25 76 | 77 | ### Added 78 | 79 | - [certificate authority] d-trust br root ca 2 2023 80 | - [certificate authority] d-trust ev root ca 2 2023 81 | - OTP 27.2 to CI 82 | 83 | ### Changed 84 | 85 | - module with bundled CAs to latest as of 2025/02/25, 04:12 UTC 86 | (source: https://curl.se/ca/cacert.pem) 87 | 88 | ### Removed 89 | 90 | - [certificate authority] swisssign silver ca - g2 91 | 92 | ## [1.26.0] - 2025-01-09 93 | 94 | ### Changed 95 | 96 | - module with bundled CAs to latest as of 2024/12/31, 04:12 UTC 97 | (source: https://curl.se/ca/cacert.pem) 98 | 99 | ### Removed 100 | 101 | - [certificate authority] securesign rootca11 102 | - [certificate authority] security communication rootca3 103 | - [certificate authority] entrust root certification authority - g4 104 | 105 | ## [1.25.0] - 2024-12-15 106 | 107 | ### Added 108 | 109 | - OTP 27.1 to CI 110 | - [certificate authority] globaltrust 2020 111 | 112 | ### Changed 113 | 114 | - module with bundled CAs to latest as of 2024/11/26, 13:58 UTC 115 | (source: https://curl.se/ca/cacert.pem) 116 | 117 | ### Fixed 118 | 119 | - problematic lack of unix permissions for 'other' among some of the source 120 | files, affecting environments that preserve said permissions and import, 121 | process or in other ways depend on `tls_certificate_check` under a different 122 | user account & group from those that own the file (thanks 123 | https://github.com/kivra-pauoli, closes issue #52) 124 | 125 | ## [1.24.0] - 2024-09-24 126 | 127 | ### Added 128 | 129 | - [certificate authority] securesign root ca12 130 | - [certificate authority] securesign root ca14 131 | - [certificate authority] securesign root ca15 132 | - [certificate authority] twca cyber root ca 133 | 134 | ### Changed 135 | 136 | - module with bundled CAs to latest as of 2024/09/24, 03:12 UTC 137 | (source: https://curl.se/ca/cacert.pem) 138 | 139 | ## [1.23.0] - 2024-07-09 140 | 141 | ### Added 142 | 143 | - [certificate authority] FIRMAPROFESIONAL CA ROOT-A WEB 144 | - OTP 27.0 to CI 145 | 146 | ### Changed 147 | 148 | - module with bundled CAs to latest as of 2024/07/02, 03:12 UTC 149 | (source: https://curl.se/ca/cacert.pem) 150 | 151 | ### Removed 152 | 153 | - [certificate authority] globaltrust 2020 154 | 155 | ## [1.22.1] - 2024-03-16 156 | 157 | ### Changed 158 | 159 | - README 160 | 161 | ## [1.22.0] - 2024-03-16 162 | 163 | ### Added 164 | 165 | - [certificate authority] Telekom Security TLS ECC Root 2020 166 | - [certificate authority] Telekom Security TLS RSA Root 2023 167 | - OTP 26.2 to CI 168 | 169 | ### Changed 170 | 171 | - module with bundled CAs to latest as of 2024/03/11, 15:25 UTC 172 | (source: https://curl.se/ca/cacert.pem) 173 | 174 | ### Fixed 175 | 176 | - Elixir example of how to use `tls_certificate_check` with `ssl:connect/4` 177 | (thanks https://github.com/macifell) 178 | 179 | ## [1.21.0] - 2023-12-12 180 | 181 | ### Added 182 | 183 | - [certificate authority] CommScope Public Trust ECC Root-01 184 | - [certificate authority] CommScope Public Trust ECC Root-02 185 | - [certificate authority] CommScope Public Trust RSA Root-01 186 | - [certificate authority] CommScope Public Trust RSA Root-02 187 | - [certificate authority] TrustAsia Global Root CA G3 188 | - [certificate authority] TrustAsia Global Root CA G4 189 | - OTP 26.1 to CI 190 | 191 | ### Changed 192 | 193 | - module with bundled CAs to latest as of 2023/12/12, 04:12 UTC 194 | (source: https://curl.se/ca/cacert.pem) 195 | 196 | ### Removed 197 | 198 | - [certificate authority] Autoridad de Certificacion Firmaprofesional CIF A62634068 199 | - [certificate authority] security communication rootca1 200 | 201 | ## [1.20.0] - 2023-08-22 202 | 203 | ### Added 204 | 205 | - [certificate authority] Atos TrustedRoot Root CA ECC TLS 2021 206 | - [certificate authority] Atos TrustedRoot Root CA RSA TLS 2021 207 | - [certificate authority] SSL.com TLS ECC Root CA 2022 208 | - [certificate authority] SSL.com TLS RSA Root CA 2022 209 | - [certificate authority] sectigo public server authentication root e46 210 | - [certificate authority] sectigo public server authentication root r46 211 | 212 | ### Changed 213 | 214 | - module with bundled CAs to latest as of 2023/08/22, 03:12 UTC 215 | (source: https://curl.se/ca/cacert.pem) 216 | 217 | ### Removed 218 | 219 | - [certificate authority] e-tugra global root ca ecc v3 220 | - [certificate authority] e-tugra global root ca rsa v3 221 | 222 | ## [1.19.0] - 2023-05-30 223 | 224 | ### Added 225 | 226 | - OTP 26.0 to CI 227 | - [certificate authority] BJCA Global Root CA2 228 | - [certificate authority] BJCA Global Root CA1 229 | 230 | ### Changed 231 | 232 | - CI to use latest rebar3 version that's compatible with each covered OTP release 233 | - module with bundled CAs to latest as of 2023/05/30, 03:12 UTC 234 | (source: https://curl.se/ca/cacert.pem) 235 | 236 | ### Removed 237 | 238 | - [certificate authority] hongkong post root ca 1 239 | - [certificate authority] E-Tugra Certification Authority 240 | 241 | ## [1.18.1] - 2023-05-01 242 | 243 | ### Changed 244 | 245 | - import of `ssl_verify_fun` to match latest allowed 1.x version 246 | 247 | ### Fixed 248 | 249 | - failing tests and checks on macOS ventura (maybe ARM specific) 250 | 251 | ## [1.18.0] - 2023-03-20 252 | 253 | ### Added 254 | 255 | - explicit SNI, to account for TCP sockets upgraded to `ssl` 256 | with `ssl:connect/3` 257 | - OTP 25.3 to CI 258 | 259 | ### Fixed 260 | 261 | - CI deprecation warnings 262 | 263 | ## [1.17.4] - 2023-02-19 264 | 265 | ### Fixed 266 | 267 | - error starting application when OS-trusted CAs fail to load on OTP 25 268 | [present since 1.17.0] 269 | 270 | ## [1.17.3] - 2023-01-17 271 | 272 | ### Fixed 273 | 274 | - (rare?) crash after reading OS-trusted CAs 275 | 276 | ## [1.17.2] - 2023-01-12 277 | 278 | ### Fixed 279 | 280 | - listing of private modules and functions in generated reference 281 | 282 | ## [1.17.1] - 2023-01-12 283 | 284 | ### Fixed 285 | 286 | - unreleased version in change log 287 | 288 | ## [1.17.0] - 2023-01-11 289 | 290 | ### Added 291 | 292 | - ability to override trusted CAs 293 | - Windows to CI 294 | - OTP 25.2 to CI 295 | 296 | ### Changed 297 | 298 | - default CAs to the ones trusted by OTP (typically provided by the OS), when available, on OTP 25+ 299 | - shared state owner to not erase its `persistent_term`s when crashing 300 | - module with bundled CAs to latest as of 2023/01/10, 04:12 UTC 301 | (source: https://curl.se/ca/cacert.pem) 302 | 303 | ### Removed 304 | 305 | - [certificate authority] network solutions certificate authority 306 | - [certificate authority] TrustCor ECA-1 307 | - [certificate authority] TrustCor RootCert CA-1 308 | - [certificate authority] Staat der Nederlanden EV Root CA 309 | - [certificate authority] TrustCor RootCert CA-2 310 | 311 | ## [1.16.0] - 2022-10-11 312 | 313 | ### Added 314 | 315 | - OTP 25.1 to CI 316 | - [certificate authority] security communication ecc rootca1 317 | - [certificate authority] security communication rootca3 318 | 319 | ### Changed 320 | 321 | - module with bundled CAs to latest as of 2022/10/11, 03:12 UTC 322 | (source: https://curl.se/ca/cacert.pem) 323 | 324 | ## [1.15.0] - 2022-07-20 325 | 326 | ### Added 327 | 328 | - OTP 25 to CI 329 | - [certificate authority] certainly root e1 330 | - [certificate authority] digicert tls ecc p384 root g5 331 | - [certificate authority] e-tugra global root ca ecc v3 332 | - [certificate authority] certainly root r1 333 | - [certificate authority] digicert tls rsa4096 root g5 334 | - [certificate authority] e-tugra global root ca rsa v3 335 | 336 | ### Changed 337 | 338 | - module with bundled CAs to latest as of 2022/07/19, 03:12 UTC 339 | (source: https://curl.se/ca/cacert.pem) 340 | 341 | ### Removed 342 | 343 | - [certificate authority] hellenic academic and research institutions rootca 2011 344 | 345 | ### Fixed 346 | 347 | - fragile automated CHANGELOG updates 348 | - flaky test case 349 | 350 | ## [1.14.0] - 2022-04-27 351 | 352 | ### Added 353 | 354 | - [certificate authority] d-trust ev root ca 1 2020 355 | - [certificate authority] d-trust br root ca 1 2020 356 | - [certificate authority] Telia Root CA v2 357 | 358 | ### Changed 359 | 360 | - module with bundled CAs to latest as of 2022/04/26, 03:12 UTC 361 | (source: https://curl.se/ca/cacert.pem) 362 | 363 | ## [1.13.0] - 2022-03-18 364 | 365 | ### Changed 366 | 367 | - module with bundled CAs to latest as of 2022/03/18, 12:30 UTC 368 | (source: https://curl.se/ca/cacert.pem) 369 | 370 | ### Removed 371 | 372 | - [certificate authority] ec-acc 373 | 374 | ## [1.12.0] - 2022-02-02 375 | 376 | ### Added 377 | 378 | - [certificate authority] vtrus ecc root ca 379 | - [certificate authority] isrg root x2 380 | - [certificate authority] vtrus root ca 381 | - [certificate authority] HiPKI Root CA - G1 382 | - [certificate authority] Autoridad de Certificacion Firmaprofesional CIF A62634068 383 | 384 | ### Changed 385 | 386 | - module with bundled CAs to latest as of 2022/02/01, 04:12 UTC 387 | (source: https://curl.se/ca/cacert.pem) 388 | - [certificate authority] gts root r4 389 | - [certificate authority] gts root r3 390 | - [certificate authority] gts root r1 391 | - [certificate authority] gts root r2 392 | - [certificate authority] GlobalSign ECC Root CA - R4 393 | 394 | ### Removed 395 | 396 | - [certificate authority] GlobalSign Root CA - R2 397 | - [certificate authority] cybertrust global root 398 | 399 | ## [1.11.0] - 2021-10-28 400 | 401 | ### Added 402 | 403 | - [certificate authority] HARICA TLS ECC Root CA 2021 404 | - [certificate authority] HARICA TLS RSA Root CA 2021 405 | - [certificate authority] TunTrust Root CA 406 | 407 | ### Changed 408 | 409 | - module with bundled CAs to latest as of 2021/10/26, 03:12 UTC 410 | (source: https://curl.se/ca/cacert.pem) 411 | 412 | ## [1.10.0] - 2021-10-01 413 | 414 | ### Changed 415 | 416 | - module with bundled CAs to latest as of 2021/09/30, 21:42 UTC 417 | (source: https://curl.se/ca/cacert.pem) 418 | 419 | ### Removed 420 | 421 | - [certificate authority] dst root ca x3 422 | 423 | ## [1.9.0] - 2021-09-03 424 | 425 | ### Added 426 | 427 | - test coverage of certificates yet-to-be valid 428 | - test coverage of misordered certificate chains 429 | 430 | ### Changed 431 | 432 | - **partial chain validation to prepare for 433 | [DST Root CA X3 expiration](https://blog.voltone.net/post/30)** 434 | - documentation from edoc to ExDoc 435 | 436 | ### Removed 437 | 438 | - dependency on badssl.com for important test cases 439 | 440 | ## [1.8.0] - 2021-08-31 441 | 442 | ### Added 443 | 444 | - automated PR-based update of bundled CAs through GHA 445 | 446 | ### Changed 447 | 448 | - app description to tentatively improve it 449 | 450 | ## [1.7.0] - 2021-07-08 451 | 452 | ### Added 453 | 454 | - [certificate authority] certum ec-384 ca 455 | - [certificate authority] globaltrust 2020 456 | - [certificate authority] certum trusted root ca 457 | - [certificate authority] anf secure server root ca 458 | 459 | ### Changed 460 | 461 | - module with bundled CAs to latest as of 2021/07/05, 21:35 UTC 462 | (source: https://curl.se/ca/cacert.pem) 463 | 464 | ### Removed 465 | 466 | - [certificate authority] sonera class2 ca 467 | - [certificate authority] trustis fps root ca 468 | - [certificate authority] quovadis root certification authority 469 | 470 | ## [1.6.0] - 2021-05-30 471 | 472 | ### Changed 473 | 474 | - module with bundled CAs to latest as of 2021/05/25, 03:12 UTC 475 | (source: https://curl.se/ca/cacert.pem) 476 | 477 | ### Removed 478 | 479 | - [certificate authority] global chambersign root - 2008 480 | - [certificate authority] chambers of commerce root - 2008 481 | 482 | ## [1.5.0] - 2021-05-13 483 | 484 | ### Added 485 | 486 | - OTP 24 to CI targets 487 | 488 | ### Removed 489 | 490 | - compatibility with OTP 21 491 | 492 | ## [1.4.0] - 2021-04-16 493 | 494 | ### Added 495 | 496 | - [certificate authority] globalsign root e46 497 | - [certificate authority] AC RAIZ FNMT-RCM SERVIDORES SEGUROS 498 | - [certificate authority] globalsign root r46 499 | 500 | ### Changed 501 | 502 | - module with bundled CAs to latest as of 2021/04/13, 03:12 UTC 503 | (source: https://curl.se/ca/cacert.pem) 504 | 505 | ### Removed 506 | 507 | - [certificate authority] geotrust primary certification authority - g2 508 | - [certificate authority] verisign universal root certification authority 509 | - [certificate authority] Staat der Nederlanden Root CA - G3 510 | 511 | ## [1.3.0] - 2021-04-02 512 | 513 | ### Added 514 | 515 | - `tls_certificate_check:trusted_authorities/0` to API 516 | 517 | ### Changed 518 | 519 | - list of authoritative certificates, from hardcoded to one that's generated on application boot 520 | and stored on `persistent_term` 521 | - set of trusted public keys, from hardcoded to one that's generated on application boot 522 | and stored on `persistent_term` 523 | 524 | ### Removed 525 | 526 | - compatibility with OTP 19 527 | - compatibility with OTP 20 528 | - compatibility with OTP 21.0 and 21.1 529 | - priv/cacerts.pem 530 | 531 | ### Fixed 532 | 533 | - unwarranted and risky hardcoding of record values 534 | 535 | ## [1.2.0] - 2021-03-12 536 | 537 | ### Added 538 | 539 | - elements for easily updating bundled CAs 540 | - [certificate authority] NAVER Global Root Certification Authority 541 | 542 | ### Changed 543 | 544 | - module with bundled CAs to latest as of 2021/01/19, 04:12 UTC 545 | (source: https://curl.se/ca/cacert.pem) 546 | 547 | ### Removed 548 | 549 | - [dependency] `certifi` 550 | - [dependency] `parse_trans` 551 | - [certificate authority] thawte primary root ca - g2 552 | - [certificate authority] geotrust global ca 553 | - [certificate authority] geotrust primary certification authority 554 | - [certificate authority] verisign class 3 public primary certification authority - g4 555 | - [certificate authority] geotrust primary certification authority - g3 556 | - [certificate authority] thawte primary root ca 557 | - [certificate authority] thawte primary root ca - g3 558 | - [certificate authority] verisign class 3 public primary certification authority - g5 559 | - [certificate authority] geotrust universal ca 560 | - [certificate authority] geotrust universal ca 2 561 | 562 | ### Fixed 563 | 564 | - misuse of `tls_certificate_` namespace (all modules start with `tls_certificate_check` now) 565 | 566 | ## [1.1.1] - 2020-12-08 567 | 568 | ### Fixed 569 | 570 | - compilation errors on OTP 20.1+ when on top of macOS Big Sur 571 | 572 | ## [1.1.0] - 2020-12-05 573 | 574 | ### Changed 575 | 576 | - CA bundles, based on the latest mkcert.org full CA list as of Nov 13, 2020 577 | 578 | ## [1.0.2] - 2020-10-16 579 | 580 | ### Fixed 581 | 582 | - misdetection of Mix as being rebar 2 and the erronous compilation warning that followed it 583 | 584 | ## [1.0.1] - 2020-05-21 585 | 586 | ### Fixed 587 | 588 | - missing links to source code in application metadata 589 | 590 | ## [1.0.0] - 2020-05-21 591 | 592 | ### Added 593 | 594 | - `:options` function to API, for easily securing connections 595 | -------------------------------------------------------------------------------- /util/tls_certificate_check_hardcoded_authorities_updater.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2021-2024 Guilherme Andrade 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a 4 | %% copy of this software and associated documentation files (the "Software"), 5 | %% to deal in the Software without restriction, including without limitation 6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | %% and/or sell copies of the Software, and to permit persons to whom the 8 | %% Software is furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | %% DEALINGS IN THE SOFTWARE. 20 | 21 | %% @private 22 | -module(tls_certificate_check_hardcoded_authorities_updater). 23 | 24 | -include_lib("kernel/include/file.hrl"). 25 | -include_lib("public_key/include/OTP-PUB-KEY.hrl"). 26 | 27 | %% ------------------------------------------------------------------ 28 | %% API Function Exports 29 | %% ------------------------------------------------------------------ 30 | 31 | -export([main/1]). 32 | 33 | -ignore_xref( 34 | [main/1]). 35 | 36 | %% ------------------------------------------------------------------ 37 | %% Macro Definitions 38 | %% ------------------------------------------------------------------ 39 | 40 | -define(FAILURE_STATUS_CODE, 1). 41 | 42 | %% ------------------------------------------------------------------ 43 | %% API Function Definitions 44 | %% ------------------------------------------------------------------ 45 | 46 | -ifdef(HARDCODED_AUTHORITIES_UPDATER_SUPPORTED). 47 | 48 | -spec main([string(), ...]) -> no_return(). 49 | main([AuthoritiesFilePath, AuthoritiesSource, OutputModuleFilePath, ChangelogFilePath]) -> 50 | OutputModuleName = output_module_name(OutputModuleFilePath), 51 | {ok, _} = application:ensure_all_started(changelog_updater), 52 | 53 | read_authorities_date(#{authorities_file_path => AuthoritiesFilePath, 54 | authorities_source => AuthoritiesSource, 55 | output_module_file_path => OutputModuleFilePath, 56 | output_module_name => OutputModuleName, 57 | changelog_file_path => ChangelogFilePath}); 58 | main(Args) -> 59 | fail("Received ~b arg(s) instead of 4", [length(Args)]). 60 | 61 | -else. 62 | 63 | -spec main([string(), ...]) -> no_return(). 64 | main(_) -> 65 | io:format(standard_error, "[error] This script requires Erlang/OTP 23.1+", []), 66 | erlang:halt(?FAILURE_STATUS_CODE). 67 | 68 | -endif. % ifdef(HARDCODED_AUTHORITIES_UPDATER_SUPPORTED). 69 | 70 | %% ------------------------------------------------------------------ 71 | %% Internal Function Definitions 72 | %% ------------------------------------------------------------------ 73 | 74 | -ifdef(HARDCODED_AUTHORITIES_UPDATER_SUPPORTED). 75 | 76 | output_module_name(OutputModuleFilePath) -> 77 | IoData = filename:basename(OutputModuleFilePath, ".erl"), 78 | String = unicode:characters_to_list(IoData), 79 | list_to_atom(String). 80 | 81 | read_authorities_date(#{authorities_file_path := AuthoritiesFilePath} = UpdateArgs) -> 82 | case file:read_file_info(AuthoritiesFilePath, [{time, universal}]) of 83 | {ok, #file_info{mtime = ModificationDateTime}} -> 84 | ExtendedUpdateArgs = UpdateArgs#{authorities_date => ModificationDateTime}, 85 | read_encoded_authorities(ExtendedUpdateArgs); 86 | {error, Reason} -> 87 | fail("Could not read certificate authorities file: ~p (path: \"~ts\")", 88 | [Reason, AuthoritiesFilePath]) 89 | end. 90 | 91 | read_encoded_authorities(#{authorities_file_path := AuthoritiesFilePath} = UpdateArgs) -> 92 | case file:read_file(AuthoritiesFilePath) of 93 | {ok, EncodedAuthorities} -> 94 | ExtendedUpdateArgs = UpdateArgs#{encoded_authorities => EncodedAuthorities}, 95 | parse_encoded_authorities(ExtendedUpdateArgs); 96 | {error, Reason} -> 97 | fail("Could not read certificate authorities file: ~p (path: \"~ts\")", 98 | [Reason, AuthoritiesFilePath]) 99 | end. 100 | 101 | parse_encoded_authorities(#{encoded_authorities := EncodedAuthorities} = UpdateArgs) -> 102 | case tls_certificate_check_util:process_authorities(EncodedAuthorities) of 103 | {ok, AuthoritativeCertificateValues} -> 104 | ExtendedUpdateArgs = UpdateArgs#{authoritative_certificate_values 105 | => AuthoritativeCertificateValues}, 106 | maybe_produce_code(ExtendedUpdateArgs); 107 | 108 | {error, no_authoritative_certificates_found} -> 109 | #{authorities_file_path := AuthoritiesFilePath} = UpdateArgs, 110 | fail("No authoritative certificates found in \"~ts\"", [AuthoritiesFilePath]); 111 | 112 | {error, {failed_to_decode, Reason}} -> 113 | #{authorities_file_path := AuthoritiesFilePath} = UpdateArgs, 114 | fail("Could not parse authoritative certificates in \"~ts\":~n~p", 115 | [AuthoritiesFilePath, Reason]); 116 | 117 | {error, Reason} -> 118 | fail("Could not extract authoritative certificate value: ~p", [Reason]) 119 | end. 120 | 121 | maybe_produce_code(UpdateArgs) -> 122 | CurrentCodeIoData = current_code(UpdateArgs), 123 | NewCodeIoData = generate_code(UpdateArgs), 124 | case not string:equal(NewCodeIoData, CurrentCodeIoData, _IgnoreCase = false) of 125 | true -> 126 | produce_code(UpdateArgs, NewCodeIoData); 127 | false -> 128 | dismiss("No changes to generated code", []) 129 | end. 130 | 131 | produce_code(#{output_module_file_path := OutputModuleFilePath} = UpdateArgs, 132 | NewCodeIoData) 133 | -> 134 | {NumberOfAdditions, NumberOfRemovals, UpdatedChangelog} 135 | = compute_differences(UpdateArgs), 136 | 137 | case file:write_file(OutputModuleFilePath, NewCodeIoData) of 138 | ok -> 139 | write_updated_changelog(UpdateArgs, UpdatedChangelog), 140 | succeed("Authorities module updated (~b authority(ies) added, ~b removed)", 141 | [NumberOfAdditions, NumberOfRemovals]); 142 | {error, Reason} -> 143 | fail("Could not write output module: ~p (path: \"~ts\")", 144 | [Reason, OutputModuleFilePath]) 145 | end. 146 | 147 | current_code(#{output_module_file_path := OutputModuleFilePath}) -> 148 | case file:read_file(OutputModuleFilePath) of 149 | {ok, CurrentCode} -> 150 | CurrentCode; 151 | {error, enoent} -> 152 | "" 153 | end. 154 | 155 | generate_code(#{authorities_source := AuthoritiesSource, 156 | authorities_date := AuthoritiesDate, 157 | encoded_authorities := EncodedAuthorities, 158 | output_module_name := OutputModuleName}) -> 159 | 160 | {{CurrentYear, _, _}, {_, _, _}} = calendar:local_time(), 161 | CopyrightYearString = copyright_year_string(CurrentYear), 162 | % AuthoritativePKIs = authoritative_pkis(AuthoritativeCertificateValues), 163 | EncodedAuthoritiesFormattedString = format_encoded_authorities(_Indentation = " ", 164 | EncodedAuthorities), 165 | 166 | io_lib:format( 167 | "%% Copyright (c) ~s Guilherme Andrade\n" 168 | "%%\n" 169 | "%% Permission is hereby granted, free of charge, to any person obtaining a\n" 170 | "%% copy of this software and associated documentation files (the \"Software\"),\n" 171 | "%% to deal in the Software without restriction, including without limitation\n" 172 | "%% the rights to use, copy, modify, merge, publish, distribute, sublicense,\n" 173 | "%% and/or sell copies of the Software, and to permit persons to whom the\n" 174 | "%% Software is furnished to do so, subject to the following conditions:\n" 175 | "%%\n" 176 | "%% The above copyright notice and this permission notice shall be included in\n" 177 | "%% all copies or substantial portions of the Software.\n" 178 | "%%\n" 179 | "%% THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n" 180 | "%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n" 181 | "%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n" 182 | "%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n" 183 | "%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n" 184 | "%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n" 185 | "%% DEALINGS IN THE SOFTWARE.\n" 186 | "\n" 187 | "%% @private\n" 188 | "-module(~p).\n" 189 | "\n" 190 | "-on_load(maybe_update_shared_state/0).\n" 191 | "\n" 192 | "%% Automatically generated; do not edit.\n" 193 | "%%\n" 194 | "%% Source: ~ts\n" 195 | "%% Date: ~ts\n" 196 | "\n" 197 | "%% ------------------------------------------------------------------\n" 198 | "%% API Function Exports\n" 199 | "%% ------------------------------------------------------------------\n" 200 | "\n" 201 | "-export([encoded_list/0]).\n" 202 | "\n" 203 | "%% ------------------------------------------------------------------\n" 204 | "%% API Function Definitions\n" 205 | "%% ------------------------------------------------------------------\n" 206 | "\n" 207 | "-dialyzer({nowarn_function, encoded_list/0}).\n" 208 | "-spec encoded_list() -> binary().\n" 209 | "\n" 210 | "-ifdef(TEST).\n" 211 | "encoded_list() ->\n" 212 | " % We can't use `meck' to mock this because it can't mock local functions\n" 213 | " % (and maybe_update_shared_state/0 needs to call us as a local function,\n" 214 | " % necessarily, because it runs upon the module being loaded.)\n" 215 | " case file:consult(\"tls_certificate_check_hardcoded_authorities_mock_value.txt\") of\n" 216 | " {ok, [EncodedList]} -> EncodedList;\n" 217 | " {error, enoent} -> encoded_list_()\n" 218 | " end.\n" 219 | "-else.\n" 220 | "encoded_list() ->\n" 221 | " encoded_list_().\n" 222 | "-endif.\n" 223 | "\n" 224 | "%% ------------------------------------------------------------------\n" 225 | "%% Internal Function Definitions\n" 226 | "%% ------------------------------------------------------------------\n" 227 | "\n" 228 | "-spec maybe_update_shared_state() -> ok | {error, term()}.\n" 229 | "maybe_update_shared_state() ->\n" 230 | " % For code swaps / release upgrades\n" 231 | " EncodedCertificates = encoded_list(),\n" 232 | " case tls_certificate_check_shared_state:maybe_update_shared_state(_Source = hardcoded,\n" 233 | " EncodedCertificates) of\n" 234 | " noproc -> ok;\n" 235 | " Other -> Other\n" 236 | " end.\n" 237 | "\n" 238 | "encoded_list_() ->\n" 239 | "~ts", 240 | 241 | [CopyrightYearString, 242 | OutputModuleName, 243 | AuthoritiesSource, 244 | format_date(AuthoritiesDate), 245 | EncodedAuthoritiesFormattedString]). 246 | 247 | copyright_year_string(CurrentYear) 248 | when CurrentYear > 2021 -> 249 | io_lib:format("2021-~b", [CurrentYear]); 250 | copyright_year_string(CurrentYear) 251 | when CurrentYear =:= 2021 -> 252 | "2021". 253 | 254 | format_encoded_authorities(BaseIndentation, EncodedAuthorities) -> 255 | AllLines = string:split(EncodedAuthorities, "\n", all), 256 | {Lines, [<<>>]} = lists:split(length(AllLines) - 1, AllLines), 257 | NumberOfLines = length(Lines), 258 | LineIndices = lists:seq(1, NumberOfLines), 259 | 260 | lists:zipwith( 261 | fun (Line, Index) -> 262 | BinaryLine = unicode:characters_to_binary([Line, $\n]), 263 | FormattedLine = io_lib:format("~tp", [BinaryLine]), 264 | Indentation 265 | = case Index =/= 1 of 266 | true -> [BaseIndentation, " "]; 267 | false -> [BaseIndentation, "<<"] 268 | end, 269 | Punctuation 270 | = case Index =/= NumberOfLines of 271 | true -> ","; 272 | false -> ">>." 273 | end, 274 | 275 | case unicode:characters_to_list(FormattedLine) of 276 | "<<>>" -> 277 | io_lib:format("~s\"\\n\"~s\n", [Indentation, Punctuation]); 278 | 279 | "<<" ++ RestOfTheLine -> 280 | {LineData, ">>"} = lists:split(length(RestOfTheLine) - 2, RestOfTheLine), 281 | io_lib:format("~s~ts~s\n", [Indentation, LineData, Punctuation]) 282 | end 283 | end, 284 | Lines, LineIndices). 285 | 286 | format_date({{Year, Month, Day}, {Hour, Minute, _Second}}) -> 287 | io_lib:format("~4..0b/~2..0b/~2..0b, ~2..0b:~2..0b UTC", 288 | [Year, Month, Day, Hour, Minute]). 289 | 290 | compute_differences(UpdateArgs) -> 291 | #{changelog_file_path := ChangelogFilePath} = UpdateArgs, 292 | {Additions, Removals} = certificate_differences(UpdateArgs), 293 | 294 | case file:read_file(ChangelogFilePath) of 295 | {ok, Changelog} -> 296 | UpdatedChangelog = updated_changelog(UpdateArgs, Changelog, Additions, Removals), 297 | 298 | {length(Additions), length(Removals), UpdatedChangelog}; 299 | 300 | {error, Reason} -> 301 | fail("Could not read changelog: ~p (path: \"~ts\")", 302 | [Reason, ChangelogFilePath]) 303 | end. 304 | 305 | certificate_differences(#{authoritative_certificate_values 306 | := AuthoritativeCertificateValues} = UpdateArgs) -> 307 | PreviousAuthoritativeCertificateValues = previous_authoritative_certificate_values(UpdateArgs), 308 | Previous = ordsets:from_list(PreviousAuthoritativeCertificateValues), 309 | Current = ordsets:from_list(AuthoritativeCertificateValues), 310 | 311 | Additions = ordsets:subtract(Current, Previous), 312 | Removals = ordsets:subtract(Previous, Current), 313 | {ordsets:to_list(Additions), ordsets:to_list(Removals)}. 314 | 315 | previous_authoritative_certificate_values(#{output_module_name := OutputModuleName}) -> 316 | try OutputModuleName:encoded_list() of 317 | <> -> 318 | {ok, List} = tls_certificate_check_util:process_authorities(EncodedList), 319 | List 320 | catch 321 | error:undef -> 322 | [] 323 | end. 324 | 325 | updated_changelog(UpdateArgs, Changelog, Additions, Removals) -> 326 | AdditionEntries = lists:map(fun certificate_changelog_string/1, Additions), 327 | ChangeEntry = changelog_change_entry(UpdateArgs), 328 | RemovalEntries = lists:map(fun certificate_changelog_string/1, Removals), 329 | 330 | Changelog2 331 | = lists:foldl( 332 | fun (AdditionEntry, Acc) -> 333 | {ok, UpdatedAcc} = changelog_updater:insert_addition(AdditionEntry, Acc), 334 | UpdatedAcc 335 | end, 336 | Changelog, 337 | AdditionEntries), 338 | 339 | {ok, Changelog3} = changelog_updater:insert_change(ChangeEntry, Changelog2), 340 | 341 | _Changelog4 342 | = lists:foldl( 343 | fun (RemovalEntry, Acc) -> 344 | {ok, UpdatedAcc} = changelog_updater:insert_removal(RemovalEntry, Acc), 345 | UpdatedAcc 346 | end, 347 | Changelog3, 348 | RemovalEntries). 349 | 350 | changelog_change_entry(UpdateArgs) -> 351 | #{authorities_source := AuthoritiesSource, 352 | authorities_date := AuthoritiesDate} = UpdateArgs, 353 | 354 | io_lib:format( 355 | "module with bundled CAs to latest as of ~ts\n" 356 | "(source: ~ts)", 357 | [format_date(AuthoritiesDate), AuthoritiesSource]). 358 | 359 | certificate_changelog_string(EncodedCertificate) -> 360 | SubjectId = public_key:pkix_subject_id(EncodedCertificate), 361 | {_SerialNr, SubjectName} = SubjectId, 362 | {rdnSequence, SubjectNameAttributeSets} = SubjectName, 363 | 364 | % * http://erlang.org/documentation/doc-5.7.4/lib/ssl-3.10.7/doc/html/pkix_certs.html 365 | % * http://www.umich.edu/~x509/ssleay/asn1-oids.html 366 | IdAtOrganizationUnitName = {2, 5, 4, 11}, 367 | AttributesToTry = [?'id-at-commonName', IdAtOrganizationUnitName], 368 | AuthorityName = certificate_changelog_name_recur(AttributesToTry, SubjectNameAttributeSets), 369 | 370 | case string:length(AuthorityName) > 0 371 | andalso {printable, io_lib:printable_unicode_list(AuthorityName)} 372 | of 373 | {printable, true} -> 374 | io_lib:format("[certificate authority] ~ts", [AuthorityName]); 375 | {printable, false} -> 376 | fail("Authority name not printable (~p): ~p", [AuthorityName, SubjectId]); 377 | false -> 378 | fail("Authority name empty: ~p", [SubjectId]) 379 | end. 380 | 381 | -dialyzer({no_match, certificate_changelog_name_recur/2}). 382 | certificate_changelog_name_recur([Attribute | Next], SubjectNameAttributeSets) -> 383 | case certificate_changelog_name_subrecur(Attribute, SubjectNameAttributeSets) of 384 | {ok, AuthorityName} -> 385 | AuthorityName; 386 | error -> 387 | certificate_changelog_name_recur(Next, SubjectNameAttributeSets) 388 | end; 389 | certificate_changelog_name_recur([], _SubjectNameAttributeSets) -> 390 | "". 391 | 392 | -dialyzer({no_fail_call, certificate_changelog_name_subrecur/2}). 393 | certificate_changelog_name_subrecur(Attribute, [SubjectNameAttributeSet | Next]) -> 394 | % * http://erlang.org/doc/apps/public_key/public_key_records.html 395 | 396 | case lists:keyfind(Attribute, #'AttributeTypeAndValue'.type, SubjectNameAttributeSet) of 397 | #'AttributeTypeAndValue'{value = {printableString, String}} -> 398 | {ok, String}; 399 | #'AttributeTypeAndValue'{value = {universalString, String}} -> 400 | {ok, String}; 401 | #'AttributeTypeAndValue'{value = {utf8String, Binary}} -> 402 | String = unicode:characters_to_list(Binary), 403 | {ok, String}; 404 | #'AttributeTypeAndValue'{} -> 405 | certificate_changelog_name_subrecur(Attribute, Next); 406 | false -> 407 | certificate_changelog_name_subrecur(Attribute, Next) 408 | end; 409 | certificate_changelog_name_subrecur(_Attribute, []) -> 410 | error. 411 | 412 | write_updated_changelog(#{changelog_file_path := ChangelogFilePath}, UpdatedChangelog) -> 413 | case file:write_file(ChangelogFilePath, UpdatedChangelog) of 414 | ok -> 415 | ok; 416 | {error, Reason} -> 417 | fail("Could not update changelog: ~p (path: \"~ts\")", 418 | [Reason, ChangelogFilePath]) 419 | end. 420 | 421 | succeed(Fmt, Args) -> 422 | io:format(standard_error, "[updated] " ++ Fmt ++ "~n", Args), 423 | halt_(0). 424 | 425 | fail(Fmt, Args) -> 426 | io:format(standard_error, "[error] " ++ Fmt ++ "~n", Args), 427 | io:format("error~n"), 428 | halt_(?FAILURE_STATUS_CODE). 429 | 430 | dismiss(Fmt, Args) -> 431 | io:format(standard_error, "[dismissed] " ++ Fmt ++ "~n", Args), 432 | io:format("dismissed~n"), 433 | halt_(0). 434 | 435 | halt_(Status) -> 436 | % Get Dialyzer to stop complaining about functions 437 | % having "no local return" all over this module. 438 | OpaqueFunctionName = binary_to_term( term_to_binary(halt) ), 439 | erlang:OpaqueFunctionName(Status). 440 | 441 | -endif. % ifdef(HARDCODED_AUTHORITIES_UPDATER_SUPPORTED). 442 | -------------------------------------------------------------------------------- /src/tls_certificate_check_shared_state.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2021-2024 Guilherme Andrade 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a 4 | %% copy of this software and associated documentation files (the "Software"), 5 | %% to deal in the Software without restriction, including without limitation 6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | %% and/or sell copies of the Software, and to permit persons to whom the 8 | %% Software is furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | %% DEALINGS IN THE SOFTWARE. 20 | 21 | %% @private 22 | -module(tls_certificate_check_shared_state). 23 | -behaviour(gen_server). 24 | 25 | -include_lib("kernel/include/logger.hrl"). 26 | -include_lib("public_key/include/OTP-PUB-KEY.hrl"). 27 | -include_lib("public_key/include/public_key.hrl"). 28 | -include_lib("stdlib/include/ms_transform.hrl"). 29 | 30 | -ifdef(TEST). 31 | -include_lib("eunit/include/eunit.hrl"). 32 | -endif. 33 | 34 | %% ------------------------------------------------------------------ 35 | %% API Function Exports 36 | %% ------------------------------------------------------------------ 37 | 38 | -export([child_spec/0, 39 | start_link/0, 40 | authoritative_certificate_values/0, 41 | find_trusted_authority/1, 42 | maybe_update_shared_state/2]). 43 | 44 | -ignore_xref( 45 | [start_link/0]). 46 | 47 | %% Hacks 48 | -export([maybe_update_shared_state/1]). 49 | 50 | -ignore_xref( 51 | [maybe_update_shared_state/1]). 52 | 53 | %%------------------------------------------------------------------- 54 | %% OTP Process Function Exports 55 | %%------------------------------------------------------------------- 56 | 57 | -export([proc_lib_init/0]). 58 | 59 | -ignore_xref( 60 | [proc_lib_init/0]). 61 | 62 | %% ------------------------------------------------------------------ 63 | %% gen_server Function Exports 64 | %% ------------------------------------------------------------------ 65 | 66 | -export([init/1, 67 | handle_call/3, 68 | handle_cast/2, 69 | terminate/2, 70 | code_change/3]). 71 | 72 | %% ------------------------------------------------------------------ 73 | %% Internal Function Exports 74 | %% ------------------------------------------------------------------ 75 | 76 | -ifdef(TEST). 77 | -export([latest_shared_state_key/0]). 78 | 79 | -ignore_xref( 80 | [latest_shared_state_key/0]). 81 | -endif. 82 | 83 | %% ------------------------------------------------------------------ 84 | %% Macro Definitions 85 | %% ------------------------------------------------------------------ 86 | 87 | -define(APP, tls_certificate_check). 88 | -define(SERVER, ?MODULE). 89 | -define(INFO_TABLE, ?SERVER). 90 | -define(HIBERNATE_AFTER, (timer:seconds(10))). 91 | 92 | -ifdef(NO_PUBLIC_KEY_CACERTS_GET). 93 | -define(DEFAULT_USE_OTP_TRUSTED_CAs, (false)). 94 | -else. 95 | -define(DEFAULT_USE_OTP_TRUSTED_CAs, (true)). 96 | -endif. 97 | 98 | -define(SHARED_STATE_KEY_PREFIX, "__$tls_certificate_check.shared_state."). 99 | 100 | %% ------------------------------------------------------------------ 101 | %% API Type Definitions 102 | %% ------------------------------------------------------------------ 103 | 104 | -type authorities_source() :: hardcoded | otp | {override, term()}. 105 | -export_type([authorities_source/0]). 106 | 107 | %% ------------------------------------------------------------------ 108 | %% Internal Type Definitions 109 | %% ------------------------------------------------------------------ 110 | 111 | -record(state, { 112 | shared_state_initialized :: boolean() 113 | }). 114 | -type state() :: #state{}. 115 | 116 | -record(shared_state, { 117 | authoritative_certificate_values :: [public_key:der_encoded(), ...], 118 | trusted_public_keys :: #{public_key_info() := []} 119 | }). 120 | 121 | -type public_key_info() :: #'OTPSubjectPublicKeyInfo'{}. 122 | 123 | %% ------------------------------------------------------------------ 124 | %% API Function Definitions 125 | %% ------------------------------------------------------------------ 126 | 127 | -spec child_spec() -> supervisor:child_spec(). 128 | child_spec() -> 129 | #{ id => ?SERVER, 130 | start => {?MODULE, start_link, []}, 131 | shutdown => infinity % ensure `:terminate/2' has time to run unless killed 132 | }. 133 | 134 | -spec start_link() -> {ok, pid()} | {error, term()}. 135 | start_link() -> 136 | proc_lib:start_link(?MODULE, proc_lib_init, []). 137 | 138 | -spec authoritative_certificate_values() -> [public_key:der_encoded(), ...] | no_return(). 139 | authoritative_certificate_values() -> 140 | SharedState = get_latest_shared_state(), 141 | SharedState#shared_state.authoritative_certificate_values. 142 | 143 | -spec find_trusted_authority([public_key:der_encoded()]) 144 | -> {trusted_ca, public_key:der_encoded()} 145 | | unknown_ca 146 | | no_return(). 147 | find_trusted_authority(EncodedCertificates) -> 148 | SharedState = get_latest_shared_state(), 149 | TrustedPublicKeys = #{} = SharedState#shared_state.trusted_public_keys, 150 | Now = universal_time_in_certificate_format(), 151 | find_trusted_authority_recur(EncodedCertificates, Now, TrustedPublicKeys). 152 | 153 | -spec maybe_update_shared_state(authorities_source(), 154 | binary() | [public_key:der_encoded()]) 155 | -> ok | {error, term()} | noproc. 156 | maybe_update_shared_state(Source, UnprocessedAuthorities) -> 157 | try 158 | gen_server:call(?SERVER, 159 | _Req = {update_shared_state, Source, UnprocessedAuthorities}, 160 | _Timeout = infinity) 161 | catch 162 | exit:{noproc, {gen_server, call, [?SERVER | _]}} -> 163 | noproc 164 | end. 165 | 166 | %% 167 | %% Hack for hot swap 168 | %% 169 | -spec maybe_update_shared_state(binary() | [public_key:der_encoded()]) 170 | -> ok | {error, term()}. 171 | maybe_update_shared_state(UnprocessedAuthorities) -> 172 | case maybe_update_shared_state(_Source = hardcoded, UnprocessedAuthorities) of 173 | noproc -> 174 | ok; 175 | Result -> 176 | Result 177 | end. 178 | 179 | %% ------------------------------------------------------------------ 180 | %% OTP Process Function Definitions 181 | %% ------------------------------------------------------------------ 182 | 183 | -spec proc_lib_init() -> no_return(). 184 | proc_lib_init() -> 185 | % do this before registering to ensure initialization is triggered before any update 186 | EncodedHardcodedAuthorities = tls_certificate_check_hardcoded_authorities:encoded_list(), 187 | gen_server:cast(self(), {initialize_shared_state, EncodedHardcodedAuthorities}), 188 | 189 | try register(?SERVER, self()) of 190 | true -> 191 | _ = process_flag(trap_exit, true), % ensure `terminate/2' is called unless killed 192 | _ = new_info_table(), 193 | GenServerOptions = [{hibernate_after, ?HIBERNATE_AFTER}], 194 | State = #state{shared_state_initialized = false}, 195 | gen_server:enter_loop(?MODULE, GenServerOptions, State) 196 | catch 197 | error:badarg when is_atom(?SERVER) -> 198 | proc_lib:init_ack({error, {already_started, whereis(?SERVER)}}), 199 | exit(normal) 200 | end. 201 | 202 | %% ------------------------------------------------------------------ 203 | %% gen_server Function Definitions 204 | %% ------------------------------------------------------------------ 205 | 206 | -spec init(_) -> no_return(). 207 | init(_) -> 208 | error('Not to be called'). 209 | 210 | -spec handle_call(term(), {pid(), reference()}, state()) 211 | -> {reply, ok, state()} | 212 | {reply, {error, term()}, state()} | 213 | {stop, {unexpected_call, #{request := _, from := {pid(), reference()}}}, state()}. 214 | handle_call({update_shared_state, Source, UnprocessedAuthorities}, _From, State) 215 | when State#state.shared_state_initialized -> 216 | handle_shared_state_update(Source, UnprocessedAuthorities, State); 217 | handle_call(await_initialization, _From, State) 218 | when State#state.shared_state_initialized -> 219 | {reply, ok, State}; 220 | handle_call(Request, From, State) -> 221 | ErrorDetails = #{request => Request, from => From}, 222 | {stop, {unexpected_call, ErrorDetails}, State}. 223 | 224 | -spec handle_cast(term(), state()) 225 | -> {noreply, state()} | 226 | {stop, normal, state()} | 227 | {stop, {unexpected_cast, term()}, state()}. 228 | handle_cast({initialize_shared_state, EncodedHardcodedAuthorities}, State) 229 | when not State#state.shared_state_initialized -> 230 | handle_shared_state_initialization(EncodedHardcodedAuthorities, State); 231 | handle_cast(Request, State) -> 232 | {stop, {unexpected_cast, Request}, State}. 233 | 234 | -spec terminate(term(), state()) -> ok. 235 | terminate(Reason, _State) -> 236 | ets:delete_all_objects(?INFO_TABLE), 237 | erlang:yield(), 238 | _ = tls_certificate_check_util:is_termination_reason_wholesome(Reason) 239 | andalso destroy_all_shared_states(), 240 | ok. 241 | 242 | -spec code_change(term(), state() | term(), term()) 243 | -> {ok, state()} | {error, {cannot_convert_state, term()}}. 244 | code_change(_OldVsn, #state{} = State, _Extra) -> 245 | HardcodedStorePriority = source_priority(hardcoded), 246 | PrevPointer = latest_shared_state, 247 | NewPointer = {latest_shared_state, HardcodedStorePriority}, 248 | 249 | _ = case [ets:lookup(?INFO_TABLE, K) || K <- [PrevPointer, NewPointer]] of 250 | [[{_, KeyForHardcodedStore}], []] -> 251 | ?LOG_NOTICE("Migrating existing hardcoded store to new key"), 252 | ets:insert(?INFO_TABLE, {NewPointer, KeyForHardcodedStore}); 253 | [_, _] -> 254 | ok 255 | end, 256 | 257 | {ok, State}; 258 | code_change(_OldVsn, State, _Extra) -> 259 | {error, {cannot_convert_state, State}}. 260 | 261 | %% ------------------------------------------------------------------ 262 | %% Internal Function Definitions 263 | %% ------------------------------------------------------------------ 264 | 265 | new_info_table() -> 266 | Opts = [named_table, protected, {read_concurrency, true}], 267 | ets:new(?INFO_TABLE, Opts). 268 | 269 | handle_shared_state_initialization(EncodedHardcodedAuthorities, State) -> 270 | case new_shared_state(_EncodedAuthoritiesSource = hardcoded, EncodedHardcodedAuthorities) of 271 | {ok, Key, SharedState, FinalSource} -> 272 | Priority = source_priority(FinalSource), 273 | ?LOG_INFO("Loading ~b CA(s) from ~p store", 274 | [length(SharedState#shared_state.authoritative_certificate_values), 275 | FinalSource]), 276 | 277 | ets:insert(?INFO_TABLE, {{latest_shared_state_key, Priority}, Key}), 278 | proc_lib:init_ack({ok, self()}), 279 | UpdatedState = State#state{shared_state_initialized = true}, 280 | {noreply, UpdatedState}; 281 | {error, Reason} -> 282 | proc_lib:init_ack({error, Reason}), 283 | {stop, normal, State} 284 | end. 285 | 286 | handle_shared_state_update(Source, UnprocessedAuthorities, State) -> 287 | case new_shared_state(_UnprocessedAuthoritiesSource = Source, UnprocessedAuthorities) of 288 | {ok, Key, SharedState, FinalSource} -> 289 | handle_shared_state_update_(Key, SharedState, FinalSource, State); 290 | {error, _Reason} = Error -> 291 | {reply, Error, State} 292 | end. 293 | 294 | handle_shared_state_update_(Key, SharedState, FinalSource, State) -> 295 | Priority = source_priority(FinalSource), 296 | NrOfCAs = length(SharedState#shared_state.authoritative_certificate_values), 297 | maybe_log_update(Priority, Key, NrOfCAs, FinalSource), 298 | ets:insert(?INFO_TABLE, {{latest_shared_state_key, Priority}, Key}), 299 | {reply, ok, State}. 300 | 301 | source_priority({override, _}) -> 302 | 1000; 303 | source_priority(otp) -> 304 | 100; 305 | source_priority(hardcoded) -> 306 | 10. 307 | 308 | maybe_log_update(Priority, Key, NrOfCAs, FinalSource) -> 309 | case is_there_a_higher_priority_store(Priority) 310 | orelse {are_there_changes, is_key_different_from_stored(Priority, Key)} 311 | of 312 | true -> 313 | ?LOG_INFO("Update with ~b CA(s) from ~p store" 314 | " is bypassed by a higher priority store", 315 | [NrOfCAs, FinalSource]); 316 | 317 | {are_there_changes, true} -> 318 | ?LOG_NOTICE("Updating with ~b CA(s) from ~p store", 319 | [NrOfCAs, FinalSource]); 320 | 321 | {are_there_changes, false} -> 322 | ok 323 | end. 324 | 325 | is_there_a_higher_priority_store(Priority) -> 326 | MatchSpec = ets:fun2ms(fun ({{latest_shared_state_key, P}, _}) -> P > Priority end), 327 | ets:select_count(?INFO_TABLE, MatchSpec) > 0. 328 | 329 | is_key_different_from_stored(Priority, Key) -> 330 | case ets:lookup(?INFO_TABLE, {latest_shared_state_key, Priority}) of 331 | [{_, StoredKey}] -> 332 | StoredKey =/= Key; 333 | [] -> 334 | true 335 | end. 336 | 337 | new_shared_state(UnprocessedAuthoritiesSource, UnprocessedAuthorities) -> 338 | UseOtpTrustedCAs 339 | = application:get_env(tls_certificate_check, use_otp_trusted_CAs, 340 | ?DEFAULT_USE_OTP_TRUSTED_CAs) 341 | andalso (UnprocessedAuthoritiesSource =:= hardcoded), 342 | 343 | case maybe_load_authorities_trusted_by_otp(UseOtpTrustedCAs, 344 | UnprocessedAuthorities, 345 | UnprocessedAuthoritiesSource) 346 | of 347 | {ok, AuthoritativeCertificateValues, Source} -> 348 | NewSharedState 349 | = #shared_state{ 350 | authoritative_certificate_values = AuthoritativeCertificateValues, 351 | trusted_public_keys = trusted_public_keys(AuthoritativeCertificateValues) 352 | }, 353 | save_shared_state(NewSharedState, Source); 354 | {error, _Reason} = Error -> 355 | Error 356 | end. 357 | 358 | -ifdef(NO_PUBLIC_KEY_CACERTS_GET). 359 | 360 | maybe_load_authorities_trusted_by_otp(_UseOtpTrustedCAs, 361 | UnprocessedAuthorities, 362 | UnprocessedAuthoritiesSource) -> 363 | process_authorities(UnprocessedAuthorities, UnprocessedAuthoritiesSource). 364 | 365 | -else. % -ifdef(NO_PUBLIC_KEY_CACERTS_GET) 366 | 367 | maybe_load_authorities_trusted_by_otp(true = _UseOtpTrustedCAs, 368 | UnprocessedAuthorities, 369 | UnprocessedAuthoritiesSource) -> 370 | try public_key:cacerts_get() of 371 | [] -> 372 | ?LOG_WARNING("OTP trusts no CAs, falling back to hardcoded authorities"), 373 | process_authorities(UnprocessedAuthorities, UnprocessedAuthoritiesSource); 374 | CombinedAuthoritativeCertificateValues when is_list(CombinedAuthoritativeCertificateValues) -> 375 | AuthoritativeCertificateValues 376 | = [CombinedCert#cert.der || CombinedCert 377 | <- CombinedAuthoritativeCertificateValues], 378 | {ok, AuthoritativeCertificateValues, _Source = otp} 379 | catch 380 | Class:Reason when Class =/= error; Reason =/= undef -> 381 | ?LOG_WARNING("Failed to load OTP-trusted CAs: ~p:~p" 382 | ", falling back to hardcoded authorities", [Class, Reason]), 383 | process_authorities(UnprocessedAuthorities, UnprocessedAuthoritiesSource) 384 | end; 385 | maybe_load_authorities_trusted_by_otp(false = _UseOtpTrustedCAs, 386 | UnprocessedAuthorities, 387 | UnprocessedAuthoritiesSource) -> 388 | process_authorities(UnprocessedAuthorities, UnprocessedAuthoritiesSource). 389 | 390 | -endif. % -ifdef(NO_PUBLIC_KEY_CACERTS_GET) 391 | 392 | process_authorities(UnprocessedAuthorities, Source) -> 393 | case tls_certificate_check_util:process_authorities(UnprocessedAuthorities) of 394 | {ok, AuthoritativeCertificateValues} -> 395 | {ok, AuthoritativeCertificateValues, Source}; 396 | {error, Reason} -> 397 | {error, {failed_to_process_authorities, Reason}} 398 | end. 399 | 400 | trusted_public_keys(AuthoritativeCertificateValues) -> 401 | lists:foldl( 402 | fun (CertificateValue, Acc) -> 403 | DecodedCertificateValue = public_key:pkix_decode_cert(CertificateValue, otp), 404 | #'OTPCertificate'{tbsCertificate = TbsCertificate} = DecodedCertificateValue, 405 | #'OTPTBSCertificate'{subjectPublicKeyInfo = PKI} = TbsCertificate, 406 | maps:put(PKI, [], Acc) 407 | end, 408 | #{}, AuthoritativeCertificateValues). 409 | 410 | save_shared_state(SharedState, Source) -> 411 | Key = shared_state_key(SharedState), 412 | persistent_term:put(Key, SharedState), 413 | {ok, Key, SharedState, Source}. 414 | 415 | shared_state_key(SharedState) -> 416 | CanonicalSharedStateRepresentation = canonical_shared_state_representation(SharedState), 417 | CanonicalSharedStateDigest 418 | = crypto:hash(sha256, term_to_binary(CanonicalSharedStateRepresentation)), 419 | 420 | KeySuffixInteger = binary:decode_unsigned(CanonicalSharedStateDigest, big), 421 | KeySuffix = integer_to_list(KeySuffixInteger, 16), 422 | list_to_atom(?SHARED_STATE_KEY_PREFIX ++ KeySuffix). 423 | 424 | canonical_shared_state_representation(SharedState) -> 425 | TupleIndices = lists:seq(1, tuple_size(SharedState)), 426 | TupleValues = tuple_to_list(SharedState), 427 | KvPairs = lists:zip(TupleIndices, TupleValues), 428 | lists:map( 429 | fun ({1, RecordTag}) -> 430 | RecordTag; 431 | ({#shared_state.authoritative_certificate_values, AuthoritativeCertificateValues}) -> 432 | % Order matters - or rather, if the order is to change, 433 | % this should not provoke a VM-wide garbage collection 434 | % with a potentially disastrous explosion in memory 435 | % consumption. 436 | AuthoritativeCertificateValues; 437 | ({#shared_state.trusted_public_keys, TrustedPublicKeys}) -> 438 | % `term_to_binary/1' doesn't guarantee any particular encoding order; 439 | % therefore equivalent shared states could end up under different keys 440 | % (depending on VM implementation) 441 | lists:sort( maps:to_list(TrustedPublicKeys) ) 442 | end, 443 | KvPairs). 444 | 445 | destroy_all_shared_states() -> 446 | AllPersistentTermObjects = persistent_term:get(), 447 | lists:filtermap( 448 | fun ({Key, Value}) -> 449 | is_atom(Key) 450 | andalso lists:prefix(?SHARED_STATE_KEY_PREFIX, 451 | atom_to_list(Key)) 452 | andalso is_record(Value, shared_state) 453 | andalso persistent_term:erase(Key) 454 | andalso {true, Key} 455 | end, 456 | AllPersistentTermObjects). 457 | 458 | get_latest_shared_state() -> 459 | Key = latest_shared_state_key(), 460 | try 461 | persistent_term:get(Key) 462 | catch 463 | error:badarg -> 464 | throw({tls_certificate_check, 465 | #{reason => shared_state_not_found, 466 | persistent_term_key => Key}}) 467 | end. 468 | 469 | latest_shared_state_key() -> 470 | MatchSpec = ets:fun2ms( 471 | fun ({{latest_shared_state_key, Priority}, Key}) -> 472 | {-Priority, Key} 473 | end), 474 | 475 | latest_shared_state_key(MatchSpec, ensure_initialized). 476 | 477 | -spec latest_shared_state_key(ets:match_spec(), ensure_initialized | throw) -> term() | no_return(). 478 | latest_shared_state_key(MatchSpec, FailureBehaviour) -> 479 | % Regarding FailureBehaviour `ensure_initialized': 480 | % * https://github.com/g-andrade/tls_certificate_check/issues/61 481 | % * github.com/open-telemetry/opentelemetry-erlang/issues/419 482 | 483 | try ets:select(?INFO_TABLE, MatchSpec) of 484 | [] when FailureBehaviour =:= ensure_initialized -> 485 | % Workaround for when `tls_certificate_check' is an implicit 486 | % dependency that's called before it finished initializing. 487 | ok = gen_server:call(?SERVER, await_initialization), 488 | latest_shared_state_key(MatchSpec, throw); 489 | 490 | [] -> 491 | throw({application_not_ready, tls_certificate_check}); 492 | 493 | PrioritizedKeys -> 494 | [{_, HighestPriorityKey} | _] = lists:sort(PrioritizedKeys), 495 | HighestPriorityKey 496 | catch 497 | error:badarg when FailureBehaviour =:= ensure_initialized -> 498 | % Workaround for when `tls_certificate_check' is an implicit 499 | % dependency that's called before it was even started. 500 | ensure_app_started(), 501 | latest_shared_state_key(MatchSpec, throw); 502 | 503 | error:badarg -> 504 | throw({application_either_not_started_or_not_ready, tls_certificate_check}) 505 | end. 506 | 507 | -spec ensure_app_started() -> ok | no_return(). 508 | ensure_app_started() -> 509 | case application:ensure_all_started(?APP) of 510 | {ok, _} -> 511 | ok; 512 | 513 | {error, Reason} -> 514 | throw({application_failed_to_start, {tls_certificate_check, Reason}}) 515 | end. 516 | 517 | universal_time_in_certificate_format() -> 518 | % http://erlang.org/doc/apps/public_key/public_key_records.html 519 | % * {utcTime, "YYMMDDHHMMSSZ" 520 | % * {generalTime, "YYYYMMDDHHMMSSZ"} 521 | 522 | {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:universal_time(), 523 | IoData = io_lib:format("~4..0B~2..0B~2..0B" "~2..0B~2..0B~2..0BZ", 524 | [Year, Month, Day, Hour, Minute, Second]), 525 | lists:flatten(IoData). 526 | 527 | find_trusted_authority_recur([EncodedCertificate | NextEncodedCertificates], Now, TrustedPublicKeys) -> 528 | Certificate = public_key:pkix_decode_cert(EncodedCertificate, otp), 529 | #'OTPCertificate'{tbsCertificate = TbsCertificate} = Certificate, 530 | #'OTPTBSCertificate'{subjectPublicKeyInfo = PublicKeyInfo, 531 | validity = Validity} = TbsCertificate, 532 | 533 | case is_certificate_valid(Validity, Now) 534 | andalso maps:is_key(PublicKeyInfo, TrustedPublicKeys) 535 | of 536 | true -> 537 | {trusted_ca, EncodedCertificate}; 538 | false -> 539 | find_trusted_authority_recur(NextEncodedCertificates, Now, TrustedPublicKeys) 540 | end; 541 | find_trusted_authority_recur([], _Now, _TrustedPublicKeys) -> 542 | unknown_ca. 543 | 544 | is_certificate_valid(Validity, Now) -> 545 | #'Validity'{notBefore = NotBefore, notAfter = NotAfter} = Validity, 546 | compare_certificate_timestamps(NotAfter, Now) =/= lesser 547 | andalso compare_certificate_timestamps(NotBefore, Now) =/= greater. 548 | 549 | compare_certificate_timestamps({utcTime, String}, Now) -> 550 | compare_certificate_timestamps_("20" ++ String, Now); 551 | compare_certificate_timestamps({generalTime, String}, Now) -> 552 | compare_certificate_timestamps_(String, Now). 553 | 554 | compare_certificate_timestamps_([X|A], [Y|B]) -> 555 | if X < Y -> 556 | lesser; 557 | X > Y -> 558 | greater; 559 | true -> 560 | compare_certificate_timestamps_(A, B) 561 | end; 562 | compare_certificate_timestamps_([], []) -> 563 | equal. 564 | 565 | %% ------------------------------------------------------------------ 566 | %% Unit Test Definitions 567 | %% ------------------------------------------------------------------ 568 | -ifdef(TEST). 569 | -ifndef(NO_PUBLIC_KEY_CACERTS_GET). 570 | 571 | missing_otp_provided_CAs_falls_back_gracefully_test() -> 572 | _ = application:stop(tls_certificate_check), 573 | {ok, _} = application:ensure_all_started(meck), 574 | {ok, _} = application:ensure_all_started(public_key), 575 | 576 | ok = meck:new(public_key, [passthrough]), 577 | ok = meck:expect(public_key, cacerts_get, fun () -> error({badmatch, foobar}) end), 578 | try 579 | {ok, _} = application:ensure_all_started(tls_certificate_check) 580 | after 581 | ok = meck:unload(public_key) 582 | end. 583 | 584 | -endif. 585 | -endif. 586 | --------------------------------------------------------------------------------