diff --git a/API.md b/API.md new file mode 100644 index 0000000..369833c --- /dev/null +++ b/API.md @@ -0,0 +1,184 @@ +# Certificate Management API for OCSP Server + +This API allows you to add, revoke, and query certificates in the database used by the OCSP server. + +## Configuration + +To enable the API, add the following parameters to your `config.toml` file: + +```toml +# API Configuration +enable_api = true # Enable the API +api_keys = ["your-secure-api-key"] # List of valid API keys +``` + +## Generating Secure API Keys + +API keys should be random, hard to guess, and unique. Here are different methods to generate secure API keys: + +### Using OpenSSL + +The simplest way to generate a secure API key is using OpenSSL: + +```bash +# Generate a 32-byte random hexadecimal string +openssl rand -hex 32 +``` + +Example output: `a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6` + +After generating an API key, add it to the `api_keys` array in your `config.toml` file. + +## Authentication + +All API requests must include an `X-API-Key` header with a valid API key. + +## Finding Certificate Numbers + +To use this API, you need the certificate number in the correct format (with `0x` prefix and lowercase hexadecimal digits). You can extract a certificate's serial number and format it properly using this command: + +```bash +openssl x509 -in your_certificate.pem -serial -noout | awk -F= '{print "0x" tolower($2)}' +``` + +This will output the certificate number in the exact format required by the API endpoints (e.g., `0x3b6ea97e1bf7699397e2109846e4f356be982542`). + +## Endpoints + +### Health Check +``` +GET /api/health +``` +Returns "OK" if the service is available. + +### Add a Certificate +``` +POST /api/certificates +``` + +**Request Body:** +```json +{ + "cert_num": "0x123456789ABCDEF" +} +``` + +**Example Response:** +```json +{ + "cert_num": "0x123456789ABCDEF", + "status": "Valid", + "message": "Certificate added successfully" +} +``` + +### Revoke a Certificate +``` +POST /api/certificates/revoke +``` + +**Request Body:** +```json +{ + "cert_num": "0x123456789ABCDEF", + "reason": "key_compromise", + "revocation_time": "2025-03-18T12:00:00" // Optional, uses the current time if not provided +} +``` + +Valid revocation reasons are: +- `unspecified` +- `key_compromise` +- `ca_compromise` +- `affiliation_changed` +- `superseded` +- `cessation_of_operation` +- `certificate_hold` +- `privilege_withdrawn` +- `aa_compromise` + +**Example Response:** +```json +{ + "cert_num": "0x123456789ABCDEF", + "status": "Revoked", + "message": "Certificate revoked successfully" +} +``` + +### Get a Certificate's Status +``` +GET /api/certificates/{cert_num} +``` + +**Example Response:** +```json +{ + "cert_num": "0x123456789ABCDEF", + "status": "Valid", + "message": "Certificate status retrieved: Valid" +} +``` + +### List All Certificates +``` +GET /api/certificates +``` + +Optional parameters: +- `status`: Filter by status (`Valid`, `Revoked`, or `all`) + - If no `status` parameter is provided or `status=all` is used, all certificates will be returned + - Use `status=Valid` to return only valid certificates + - Use `status=Revoked` to return only revoked certificates + +**Example Response:** +```json +[ + { + "cert_num": "0x123456789ABCDEF", + "status": "Valid", + "message": "" + }, + { + "cert_num": "0x987654321FEDCBA", + "status": "Revoked", + "message": "" + } +] +``` + +## Usage Examples with cURL + +### Add a Certificate +```bash +curl -X POST http://localhost:9000/api/certificates \ + -H "Content-Type: application/json" \ + -H "X-API-Key: your-api-key" \ + -d '{"cert_num": "0x123456789ABCDEF"}' +``` + +### Revoke a Certificate +```bash +curl -X POST http://localhost:9000/api/certificates/revoke \ + -H "Content-Type: application/json" \ + -H "X-API-Key: your-api-key" \ + -d '{"cert_num": "0x123456789ABCDEF", "reason": "key_compromise"}' +``` + +### Get a Certificate's Status +```bash +curl -X GET http://localhost:9000/api/certificates/0x123456789ABCDEF \ + -H "X-API-Key: your-api-key" +``` + +### List All Valid Certificates +```bash +curl -X GET "http://localhost:9000/api/certificates?status=Valid" \ + -H "X-API-Key: your-api-key" +``` + +### List All Certificates (Explicitly) +```bash +curl -X GET "http://localhost:9000/api/certificates?status=all" \ + -H "X-API-Key: your-api-key" +``` diff --git a/Cargo.lock b/Cargo.lock index a2bc5e7..fb6e387 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,9 +93,9 @@ dependencies = [ [[package]] name = "asn1-rs" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "607495ec7113b178fbba7a6166a27f99e774359ef4823adbefd756b5b81d7970" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -223,30 +223,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "btoi" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd6407f73a9b8b6162d8a2ef999fe6afd7cc15902ebf42c5cd296addf17e0ad" -dependencies = [ - "num-traits", -] - -[[package]] -name = "bufstream" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" - [[package]] name = "bumpalo" version = "3.17.0" @@ -273,12 +249,10 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.16" +version = "1.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" dependencies = [ - "jobserver", - "libc", "shlex", ] @@ -298,15 +272,16 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] [[package]] name = "clap" -version = "4.5.32" +version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" dependencies = [ "clap_builder", "clap_derive", @@ -314,9 +289,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.32" +version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" dependencies = [ "anstream", "anstyle", @@ -342,15 +317,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" -[[package]] -name = "cmake" -version = "0.1.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] - [[package]] name = "colorchoice" version = "1.0.3" @@ -386,46 +352,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.4.2" +name = "darling" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "cfg-if", + "darling_core", + "darling_macro", ] [[package]] -name = "crossbeam-queue" -version = "0.3.12" +name = "darling_core" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ - "crossbeam-utils", + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", ] [[package]] -name = "crossbeam-utils" -version = "0.8.21" +name = "darling_macro" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "generic-array", - "typenum", + "darling_core", + "quote", + "syn", ] [[package]] @@ -450,24 +408,13 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] -[[package]] -name = "derive_utils" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccfae181bab5ab6c5478b2ccb69e4c68a02f8c3ec72f6616bfec9dbc599d2ee0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "devise" version = "0.4.2" @@ -502,13 +449,45 @@ dependencies = [ ] [[package]] -name = "digest" -version = "0.10.7" +name = "diesel" +version = "2.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "34d3950690ba3a6910126162b47e775e203006d4242a15de912bec6c0a695153" dependencies = [ - "block-buffer", - "crypto-common", + "bitflags", + "byteorder", + "chrono", + "diesel_derives", + "itoa", + "libsqlite3-sys", + "mysqlclient-sys", + "percent-encoding", + "pq-sys", + "r2d2", + "time", + "url", +] + +[[package]] +name = "diesel_derives" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93958254b70bea63b4187ff73d10180599d9d8d177071b7f91e6da4e0c0ad55" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" +dependencies = [ + "syn", ] [[package]] @@ -522,6 +501,26 @@ dependencies = [ "syn", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "dsl_auto_type" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b" +dependencies = [ + "darling", + "either", + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.15.0" @@ -545,9 +544,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.59.0", @@ -574,21 +573,25 @@ dependencies = [ ] [[package]] -name = "flate2" -version = "1.1.0" +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "crc32fast", - "libz-sys", - "miniz_oxide", + "foreign-types-shared", ] [[package]] -name = "fnv" -version = "1.0.7" +name = "foreign-types-shared" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" @@ -599,6 +602,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + [[package]] name = "futures" version = "0.3.31" @@ -677,16 +686,6 @@ dependencies = [ "windows", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -700,14 +699,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -842,14 +841,15 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -904,9 +904,9 @@ dependencies = [ [[package]] name = "icu_locid_transform_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" [[package]] name = "icu_normalizer" @@ -928,9 +928,9 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" @@ -949,9 +949,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" @@ -981,6 +981,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1004,9 +1010,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown", @@ -1019,15 +1025,6 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" -[[package]] -name = "io-enum" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d197db2f7ebf90507296df3aebaf65d69f5dce8559d8dbd82776a6cadab61bbf" -dependencies = [ - "derive_utils", -] - [[package]] name = "is-terminal" version = "0.4.16" @@ -1051,15 +1048,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "jobserver" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" version = "0.3.77" @@ -1083,12 +1071,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] -name = "libz-sys" -version = "1.1.22" +name = "libsqlite3-sys" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7" dependencies = [ - "cc", "pkg-config", "vcpkg", ] @@ -1117,9 +1104,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.26" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "loom" @@ -1136,12 +1123,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" - [[package]] name = "matchers" version = "0.1.0" @@ -1171,9 +1152,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" dependencies = [ "adler2", ] @@ -1189,6 +1170,33 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mockall" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "multer" version = "3.1.0" @@ -1209,65 +1217,13 @@ dependencies = [ ] [[package]] -name = "mysql" -version = "26.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64453aedc258ac8c720b46c8264302fad39cef6c02483f68adbad4bcd22d6fab" -dependencies = [ - "bufstream", - "bytes", - "crossbeam-queue", - "flate2", - "io-enum", - "libc", - "lru", - "mysql_common", - "named_pipe", - "pem", - "percent-encoding", - "socket2", - "twox-hash", - "url", -] - -[[package]] -name = "mysql_common" -version = "0.34.1" +name = "mysqlclient-sys" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34a9141e735d5bb02414a7ac03add09522466d4db65bdd827069f76ae0850e58" +checksum = "8f29e21174d84e2622ceb7b0146a9187d36458a3a9ee9a66c9cac22e96493ef9" dependencies = [ - "base64", - "bitflags", - "btoi", - "byteorder", - "bytes", - "cc", - "cmake", - "crc32fast", - "flate2", - "lazy_static", - "num-bigint", - "num-traits", - "rand", - "regex", - "saturating", - "serde", - "serde_json", - "sha1", - "sha2", - "subprocess", - "thiserror 1.0.69", - "uuid", - "zstd", -] - -[[package]] -name = "named_pipe" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9c443cce91fc3e12f017290db75dde490d685cdaaf508d7159d7cf41f0eb2b" -dependencies = [ - "winapi", + "pkg-config", + "vcpkg", ] [[package]] @@ -1360,18 +1316,24 @@ dependencies = [ [[package]] name = "ocsp-server" -version = "0.4.1" +version = "0.5.0" dependencies = [ + "async-trait", "chrono", "clap", "config-file", + "diesel", "hex", - "mysql", + "log", + "mockall", "ocsp", + "openssl", "pem", + "r2d2", "ring", "rocket", "serde", + "tokio", "x509-parser", "zeroize", ] @@ -1387,9 +1349,57 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.1" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-src" +version = "300.4.2+3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168ce4e058f975fe43e89d9ccf78ca668601887ae736090aacc23ae353c298e2" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] [[package]] name = "overload" @@ -1512,6 +1522,42 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pq-sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41c852911b98f5981956037b2ca976660612e548986c30af075e753107bc3400" +dependencies = [ + "libc", + "vcpkg", +] + +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -1543,6 +1589,23 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + [[package]] name = "rand" version = "0.8.5" @@ -1575,9 +1638,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ "bitflags", ] @@ -1686,6 +1749,7 @@ dependencies = [ "rocket_codegen", "rocket_http", "serde", + "serde_json", "state", "tempfile", "time", @@ -1758,9 +1822,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ "bitflags", "errno", @@ -1782,10 +1846,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] -name = "saturating" -version = "0.1.0" +name = "scheduled-thread-pool" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] [[package]] name = "scoped-tls" @@ -1840,28 +1907,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -1897,15 +1942,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1947,16 +1992,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "subprocess" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2e86926081dda636c546d8c5e641661049d7562a68f5488be4a1f7f66f6086" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "syn" version = "2.0.100" @@ -1981,17 +2016,23 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.2", "once_cell", "rustix", "windows-sys 0.59.0", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "1.0.69" @@ -2044,9 +2085,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.39" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -2059,15 +2100,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.20" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -2085,9 +2126,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.1" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", @@ -2261,18 +2302,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "twox-hash" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7b17f197b3050ba473acf9181f7b1d3b66d1cf7356c6cc57886662276e65908" - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - [[package]] name = "ubyte" version = "0.10.4" @@ -2339,12 +2368,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" - [[package]] name = "valuable" version = "0.1.1" @@ -2380,9 +2403,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] @@ -2478,18 +2501,62 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.52.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "windows-link" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] [[package]] name = "windows-sys" @@ -2641,9 +2708,9 @@ dependencies = [ [[package]] name = "wit-bindgen-rt" -version = "0.33.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] @@ -2718,18 +2785,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.23" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.23" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" dependencies = [ "proc-macro2", "quote", @@ -2798,31 +2865,3 @@ dependencies = [ "quote", "syn", ] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.14+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml index 4dc267d..a4f4573 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,10 @@ name = "ocsp-server" authors = ["DorianCoding <108593662+DorianCoding@users.noreply.github.com>"] description = "OCSP server, listening for requests to give responses." -version = "0.4.1" +version = "0.5.0" edition = "2024" -license = "GPL-3.0-only" rust-version = "1.85" +license = "GPL-3.0-only" repository = "https://github.com/DorianCoding/OCSP_server" keywords = ["ocsp", "server", "ocsp-response"] categories = ["caching", "cryptography"] @@ -17,22 +17,38 @@ exclude = [ [dependencies] clap = { version = "4.5.32", features = ["derive", "cargo"] } -chrono = { version = "~0.4.40", default-features = false, features = ["std"]} +chrono = { version = "~0.4.31", default-features = false, features = ["std", "serde"]} config-file = "~0.2.3" -hex = "~0.4.3" -mysql = { version = "~26.0.0", default-features = false, features = ["minimal"]} +serde = "~1.0.219" +diesel = { version = "2.2.8", features = ["sqlite", "r2d2", "chrono"] } +r2d2 = "0.8.10" ocsp = {git = "https://github.com/DorianCoding/ocsp-rs.git", tag="1.0.0" } -#ocsp = {git = "https://github.com/maicallist/ocsp-rs.git" } pem = "3.0.5" -#openssl-sys = { version = "~0.9.103", features = ["vendored" ]} +openssl = { version = "0.10.71", features = ["vendored"] } ring = "0.17.14" -rocket = "~0.5.1" -serde = "~1.0.219" x509-parser = "~0.17.0" +hex = "~0.4.3" zeroize = { version = "~1.8.1", features = ["std", "zeroize_derive"] } +log = "0.4.20" +async-trait = "0.1.79" +tokio = { version = "1.36.0", features = ["rt-multi-thread", "macros", "fs", "io-util", "time"] } + +[dependencies.rocket] +version = "0.5.1" +features = ["json"] +[dev-dependencies] +mockall = "0.12.1" +[features] +api=[] +mysql = ["diesel/mysql"] +postgres = ["diesel/postgres"] +default = ["api","mysql","postgres"] [profile.release] strip = "symbols" +lto = true +codegen-units = 1 +opt-level = 3 [lints.rust] unsafe_code = "deny" diff --git a/README.md b/README.md index f82ae9a..a42a762 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ---- -This software implements a OCSP responder in Rust, fetching certificate status in a Mysql/MariaDB database. Unlike the Python implementation, it **does implement its own TCP listener** on a user-selected port. +This software implements a OCSP responder in Rust, fetching certificate status in a Mysql/MariaDB database. Unlike the Python implementation, it **does implement its own TCP listener** on a user-selected port. *It will answer to any **GET or POST** requests on any URL*. ## Requirements - A CA certificate (self-signed allowed) and/or an intermediate CA that will sign leaf certificates. @@ -30,16 +30,23 @@ The config file should contain the following informations : #Config file, all fields are compulsory cachedays = 3 #Number of days a response is valid once created (only for valid certificates) dbip = "127.0.0.1" #Optional. IP to connect to MySql database. If absent, use of unix socket. -timeout = 5 #Optional timeout, default 5s +db_type = "mysql" # Can be "mysql" or "postgres" or "sqlite" dbuser = "cert" #Username to connect to MySql database -port = 9000 #Port to listen to, from 1 to 65535. Cannot use a port already used by another service (privileged ports allowed if used as root or as a service). By default 9000 +dbport = 3306 # Optional: Default 3306 for MySQL, 5432 for PostgreSQL dbname = "certs" #Name to connect to MySql data dbpassword = "certdata" #Password to connect to cert data +port = 9000 #Port to listen to, from 1 to 65535. Cannot use a port already used by another service (privileged ports allowed if used as root or as a service). By default 9000 +listen_ip = "0.0.0.0" # Optional: IP address to listen on (default: 127.0.0.1) +timeout = 5 #Optional timeout, default 5s cachefolder = "cache/" #Folder to cache data (relative or absolute, will be created if not present) itcert = "/var/public_files/it_cert.crt" #Path to intermediate certificate as PEM format +itkey = "/var/private_files/it_privkey.pem" #Path to intermediate private key, keep it secret (PKCS#8 format, only RSA keys supported so far) revocextended = true #Optional, if you want to enable EXTENDED_REVOCATION caching = true #Optional, enable caching or enable nonce response. -itkey = "/var/private_files/it_privkey.pem" #Path to intermediate private key, keep it secret (PKCS#8 format, only RSA keys supported so far) +create_table = true # Optional: Creates the table if it doesn't exist +table_name = "custom_certs" # Optional: Custom table name (default is list_certs for MySQL, ocsp_list_certs for PostgreSQL) +enable_api = true # Optional: Enable the certificate management API +api_keys = ["secure-api-key-1", "secure-api-key-2"] # Optional: List of valid API keys for authentication ``` > [!CAUTION] @@ -168,15 +175,15 @@ OCSP Response Data: > OCSP Server - OCSP responder in Rust > Copyright (C) 2023 DorianCoding -> +> > This program is free software: you can redistribute it and/or modify > it under the terms of the GNU General Public License as published by > the Free Software Foundation, under version 3 of the License only. -> +> > This program is distributed in the hope that it will be useful, > but WITHOUT ANY WARRANTY; without even the implied warranty of > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the > GNU General Public License for more details. -> +> > You should have received a copy of the GNU General Public License > along with this program. If not, see . diff --git a/config.toml b/config.toml index f15795c..108820b 100644 --- a/config.toml +++ b/config.toml @@ -1,11 +1,20 @@ #Config file, all fields are compulsory cachedays = 3 +db_type = "mysql" # Can be "mysql" or "postgres" or "sqlite" dbip = "127.0.0.1" +dbport = 3306 # Optional: Default 3306 for MySQL, 5432 for PostgreSQL port = 9000 +listen_ip = "0.0.0.0" # Optional: IP address to listen on (default: 127.0.0.1) timeout = 10 -dbuser = "cert" -dbpassword = "cert" +dbuser = "ocsp" +dbpassword = "ocsp" dbname = "certs" cachefolder = "cache/" itcert = "test_files/cert.pem" -itkey = "test_files/keyp8.pk8" +itkey = "test_files/keyp8.pk8" # Supports PKCS#8, PEM PKCS#1 (RSA) formats +revocextended = false # Optional, if you want to enable EXTENDED_REVOCATION +caching = false # Optional, enable caching or enable nonce response. +create_table = true # Optional: Creates the table if it doesn't exist +table_name = "custom_certs" # Optional: Custom table name (default is list_certs for MySQL, ocsp_list_certs for PostgreSQL) +enable_api = true # Optional: Enable the certificate management API +api_keys = ["secure-api-key-1", "secure-api-key-2"] # Optional: List of valid API keys for authentication diff --git a/shell.nix b/shell.nix index e9e5989..29bba71 100644 --- a/shell.nix +++ b/shell.nix @@ -5,12 +5,19 @@ pkgs.mkShell { rustc cargo cargo-audit + clippy rustfmt rust-analyzer + git + openssl + libmysqlclient + sqlite + postgresql + pkg-config ]; shellHook = '' - rustfmt --edition 2024 src/main.rs + rustfmt --edition 2024 src/*.rs cargo audit ''; diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..76af458 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,209 @@ +use crate::database::Database; +use crate::r#struct::{ApiKey, CertificateRequest, CertificateResponse, RevocationRequest}; +use chrono::Utc; +use log::{error, info, warn}; +use rocket::http::Status; +use rocket::request::{FromRequest, Outcome, Request}; +use rocket::serde::json::Json; +use rocket::{get, post, routes, State}; +use std::sync::Arc; + +pub fn api_routes() -> Vec { + routes![ + health_check, + add_certificate, + revoke_certificate, + get_certificate_status, + list_certificates + ] +} + +/// API key authentication guard for secure endpoints +#[rocket::async_trait] +impl<'r> FromRequest<'r> for ApiKey { + type Error = (); + + async fn from_request(request: &'r Request<'_>) -> Outcome { + // Get API key from header + let api_key = request.headers().get_one("X-API-Key"); + + match api_key { + Some(key) => { + let config = request.rocket().state::>(); + + match config { + Some(config) => { + if let Some(valid_keys) = &config.api_keys { + if valid_keys.contains(&key.to_string()) { + Outcome::Success(ApiKey(key.to_string())) + } else { + warn!("Invalid API key attempted: {}", key); + Outcome::Error((Status::Unauthorized, ())) + } + } else { + // If no API keys are configured, reject all requests + warn!("API keys not configured but API endpoint accessed"); + Outcome::Error((Status::Unauthorized, ())) + } + } + None => Outcome::Error((Status::InternalServerError, ())), + } + } + None => Outcome::Error((Status::Unauthorized, ())), + } + } +} + +#[get("/health")] +fn health_check() -> &'static str { + "OK" +} + +/// Add a new certificate to the database +#[post("/certificates", data = "")] +async fn add_certificate( + _api_key: ApiKey, + cert_request: Json, + db: &State>, +) -> Result, Status> { + let cert = cert_request.into_inner(); + + // Validate certificate number format (should start with 0x) + if !cert.cert_num.starts_with("0x") { + return Err(Status::BadRequest); + } + + match db.add_certificate(&cert.cert_num).await { + Ok(_) => { + info!("Certificate added successfully: {}", cert.cert_num); + Ok(Json(CertificateResponse { + cert_num: cert.cert_num, + status: "Valid".to_string(), + message: "Certificate added successfully".to_string(), + })) + } + Err(e) => { + error!("Failed to add certificate: {}", e); + Err(Status::InternalServerError) + } + } +} + +/// Revoke a certificate +#[post("/certificates/revoke", data = "")] +async fn revoke_certificate( + _api_key: ApiKey, + revoke_request: Json, + db: &State>, +) -> Result, Status> { + let request = revoke_request.into_inner(); + + // Validate certificate number format + if !request.cert_num.starts_with("0x") { + return Err(Status::BadRequest); + } + + // Validate revocation reason + let valid_reasons = [ + "unspecified", + "key_compromise", + "ca_compromise", + "affiliation_changed", + "superseded", + "cessation_of_operation", + "certificate_hold", + "privilege_withdrawn", + "aa_compromise", + ]; + + if !valid_reasons.contains(&request.reason.as_str()) { + return Err(Status::BadRequest); + } + + // Use current time if not provided + let revocation_time = request + .revocation_time + .unwrap_or_else(|| Utc::now().naive_utc()); + + match db + .revoke_certificate(&request.cert_num, revocation_time, &request.reason) + .await + { + Ok(_) => { + info!("Certificate revoked successfully: {}", request.cert_num); + Ok(Json(CertificateResponse { + cert_num: request.cert_num, + status: "Revoked".to_string(), + message: "Certificate revoked successfully".to_string(), + })) + } + Err(e) => { + error!("Failed to revoke certificate: {}", e); + Err(Status::InternalServerError) + } + } +} + +/// Get the status of a specific certificate +#[get("/certificates/")] +async fn get_certificate_status( + _api_key: ApiKey, + cert_num: String, + db: &State>, +) -> Result, Status> { + let cert_num = if !cert_num.starts_with("0x") { + format!("0x{}", cert_num) + } else { + cert_num + }; + + match db.get_certificate_status(&cert_num).await { + Ok(cert_info) => Ok(Json(CertificateResponse { + cert_num: cert_num.clone(), + status: cert_info.status.clone(), + message: format!("Certificate status retrieved: {}", cert_info.status), + })), + Err(_) => Err(Status::NotFound), + } +} + +/// List all certificates or filter by status +#[get("/certificates?")] +async fn list_certificates( + _api_key: ApiKey, + status: Option, + db: &State>, +) -> Result>, Status> { + let liststatus = ["Valid","revoked","All"]; + let filtered_status = match status { + Some(d) if liststatus.iter().any(|p| *p == d.as_str()) => { + if d == "All" { + Some(d) + } else { + None + } + }, + _ => { + return Err(Status::BadRequest); + } + }; + + match db.list_certificates(filtered_status).await { + Ok(certs) => { + let response = certs + .into_iter() + .map(|cert| CertificateResponse { + cert_num: cert.cert_num, + status: cert.status, + message: String::new(), + }) + .collect(); + + Ok(Json(response)) + } + Err(e) => { + error!("Failed to list certificates: {}", e); + Err(Status::InternalServerError) + } + } +} diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..1a14dee --- /dev/null +++ b/src/database.rs @@ -0,0 +1,1237 @@ +use crate::r#struct::{ + CertRecord, CertificateResponse, Certinfo, Config, DEFAULT_SQLITE_TABLE, +}; +#[cfg(feature="mysql")] +use crate::r#struct::DEFAULT_MYSQL_PORT; +#[cfg(feature="mysql")] +use crate::r#struct::DEFAULT_MYSQL_TABLE; +#[cfg(feature="postgres")] +use crate::r#struct::DEFAULT_POSTGRES_PORT; +#[cfg(feature="postgres")] +use crate::r#struct::DEFAULT_POSTGRES_TABLE; +use async_trait::async_trait; +use chrono::{Datelike, NaiveDateTime, Timelike}; +use diesel::prelude::*; +use diesel::r2d2::{ConnectionManager, Pool}; +use diesel::sql_types; +use diesel::SqliteConnection; +#[cfg(feature = "mysql")] +use diesel::MysqlConnection; +#[cfg(feature = "postgres")] +use diesel::PgConnection; +#[cfg(feature = "postgres")] +use crate::BoolResult; +use log::{debug, info, warn}; +use ocsp::common::asn1::GeneralizedTime; +use ocsp::response::{CertStatus as OcspCertStatus, CertStatusCode, CrlReason, RevokedInfo}; +use std::error::Error; +use std::path::Path; +use std::sync::Arc; +use std::time::Duration; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DatabaseType { + MySQL, + PostgreSQL, + SQLite, +} + +impl DatabaseType { + pub fn from_string(s: &str) -> Self { + match s.to_lowercase().as_str() { + "mysql" | "MySql" => DatabaseType::MySQL, + "postgres" | "postgresql" => DatabaseType::PostgreSQL, + _ => DatabaseType::SQLite, + } + } + #[cfg(any(feature = "mysql",feature="postgres"))] + fn default_port(&self) -> u16 { + match self { + DatabaseType::MySQL => DEFAULT_MYSQL_PORT, + DatabaseType::PostgreSQL => DEFAULT_POSTGRES_PORT, + DatabaseType::SQLite => 0, + } + } + + pub fn default_table_name(&self) -> &'static str { + match self { + #[cfg(feature = "mysql")] + DatabaseType::MySQL => DEFAULT_MYSQL_TABLE, + #[cfg(feature = "postgres")] + DatabaseType::PostgreSQL => DEFAULT_POSTGRES_TABLE, + _ => DEFAULT_SQLITE_TABLE, + } + } +} + +#[async_trait] +pub trait Database: Send + Sync { + async fn check_cert( + &self, + certnum: &str, + revoked: bool, + ) -> Result>; + + fn create_tables_if_needed(&self) -> Result<(), Box>; + + async fn add_certificate(&self, cert_num: &str) -> Result<(), Box>; + + async fn revoke_certificate( + &self, + cert_num: &str, + revocation_time: NaiveDateTime, + reason: &str, + ) -> Result<(), Box>; + + async fn get_certificate_status( + &self, + cert_num: &str, + ) -> Result>; + + async fn list_certificates( + &self, + status: Option, + ) -> Result, Box>; +} + +enum DatabaseConnection { + #[cfg(feature = "mysql")] + MySQL(Pool>), + #[cfg(feature = "postgres")] + PostgreSQL(Pool>), + SQLite(Pool>), +} + +pub struct DieselDatabase { + connection: DatabaseConnection, + config: Arc, + table_name: String, +} + +impl DieselDatabase { + pub fn new(config: Arc) -> Result> { + let db_type = DatabaseType::from_string(&config.db_type); + let table_name = config + .table_name + .clone() + .unwrap_or_else(|| db_type.default_table_name().to_string()); + + let connection = match db_type { + #[cfg(feature="mysql")] + DatabaseType::MySQL => { + let dbport = config.dbport.unwrap_or_else(|| db_type.default_port()); + let database_url = match &config.dbip { + Some(host) => format!( + "mysql://{}:{}@{}:{}/{}", + config.dbuser, config.dbpassword, host, dbport, config.dbname + ), + None => format!( + "mysql://{}:{}@localhost/{}", + config.dbuser, config.dbpassword, config.dbname + ), + }; + + let manager = ConnectionManager::::new(database_url); + let pool = Pool::builder() + .max_size(15) + .connection_timeout(Duration::from_secs(config.time as u64)) + .build(manager)?; + + DatabaseConnection::MySQL(pool) + } + #[cfg(feature="postgres")] + DatabaseType::PostgreSQL => { + let dbport = config.dbport.unwrap_or_else(|| db_type.default_port()); + let database_url = match &config.dbip { + Some(host) => format!( + "postgres://{}:{}@{}:{}/{}", + config.dbuser, config.dbpassword, host, dbport, config.dbname + ), + None => format!( + "postgres://{}:{}@localhost/{}", + config.dbuser, config.dbpassword, config.dbname + ), + }; + + let manager = ConnectionManager::::new(database_url); + let pool = Pool::builder() + .max_size(15) + .connection_timeout(Duration::from_secs(config.time as u64)) + .build(manager)?; + + DatabaseConnection::PostgreSQL(pool) + } + _ => { + let db_path = &config.dbname; + + if let Some(parent) = Path::new(db_path).parent() { + if !parent.exists() { + std::fs::create_dir_all(parent)?; + } + } + + let database_url = format!("sqlite://{}", db_path); + + let manager = ConnectionManager::::new(database_url); + let pool = Pool::builder() + .max_size(1) + .connection_timeout(Duration::from_secs(config.time as u64)) + .build(manager)?; + + DatabaseConnection::SQLite(pool) + } + }; + + Ok(Self { + connection, + config, + table_name, + }) + } + #[cfg(feature="mysql")] + async fn check_cert_mysql( + &self, + pool: &Pool>, + certnum: &str, + revoked: bool, + ) -> Result> { + let table_name = self.table_name.clone(); + let cert_num = certnum.to_string(); + let connection_manager = pool.clone(); + + let result = tokio::task::spawn_blocking(move || -> Result> { + let mut conn = connection_manager.get()?; + + let query = format!( + "SELECT cert_num, revocation_time, revocation_reason, status FROM {} WHERE cert_num = ?", + table_name + ); + + let results = diesel::sql_query(query) + .bind::(&cert_num) + .load::(&mut conn)?; + + if results.is_empty() { + warn!("Entry not found for cert {}", cert_num); + if !revoked { + Ok(OcspCertStatus::new(CertStatusCode::Unknown, None)) + } else { + Ok(OcspCertStatus::new( + CertStatusCode::Revoked, + Some(RevokedInfo::new( + GeneralizedTime::new(1970, 1, 1, 0, 0, 0).unwrap(), + Some(CrlReason::OcspRevokeCertHold), + )), + )) + } + } else { + let record = &results[0]; + debug!("Entry found for cert {}, status {}", cert_num, record.status); + + if record.status == "Revoked" { + let time = GeneralizedTime::now(); + + let time = if let Some(rt) = record.revocation_time { + GeneralizedTime::new( + rt.year(), + rt.month(), + rt.day(), + rt.hour(), + rt.minute(), + rt.second(), + ).unwrap_or(time) + } else { + time + }; + + let motif = record.revocation_reason.clone().unwrap_or_default(); + let motif: CrlReason = match motif.as_str() { + "key_compromise" => CrlReason::OcspRevokeKeyCompromise, + "ca_compromise" => CrlReason::OcspRevokeCaCompromise, + "affiliation_changed" => CrlReason::OcspRevokeAffChanged, + "superseded" => CrlReason::OcspRevokeSuperseded, + "cessation_of_operation" => CrlReason::OcspRevokeCessOperation, + "certificate_hold" => CrlReason::OcspRevokeCertHold, + "privilege_withdrawn" => CrlReason::OcspRevokePrivWithdrawn, + "aa_compromise" => CrlReason::OcspRevokeAaCompromise, + _ => CrlReason::OcspRevokeUnspecified, + }; + + Ok(OcspCertStatus::new( + CertStatusCode::Revoked, + Some(RevokedInfo::new(time, Some(motif))), + )) + } else { + Ok(OcspCertStatus::new(CertStatusCode::Good, None)) + } + } + }).await??; + + Ok(result) + } + #[cfg(feature="postgres")] + async fn check_cert_postgres( + &self, + pool: &Pool>, + certnum: &str, + revoked: bool, + ) -> Result> { + let table_name = self.table_name.clone(); + let cert_num = certnum.to_string(); + let connection_manager = pool.clone(); + + let result = tokio::task::spawn_blocking(move || -> Result> { + let mut conn = connection_manager.get()?; + + // Using text SQL query with explicit column names and types + let query = format!( + "SELECT cert_num, revocation_time, revocation_reason, status FROM {} WHERE cert_num = $1", + table_name + ); + + let results = diesel::sql_query(query) + .bind::(&cert_num) + .load::(&mut conn)?; + + if results.is_empty() { + warn!("Entry not found for cert {} in PostgreSQL", cert_num); + if !revoked { + Ok(OcspCertStatus::new(CertStatusCode::Unknown, None)) + } else { + Ok(OcspCertStatus::new( + CertStatusCode::Revoked, + Some(RevokedInfo::new( + GeneralizedTime::new(1970, 1, 1, 0, 0, 0).unwrap(), + Some(CrlReason::OcspRevokeCertHold), + )), + )) + } + } else { + let record = &results[0]; + debug!("Entry found for cert {}, status {}", cert_num, record.status); + + if record.status == "Revoked" { + let time = GeneralizedTime::now(); + + let time = if let Some(rt) = record.revocation_time { + GeneralizedTime::new( + rt.year(), + rt.month(), + rt.day(), + rt.hour(), + rt.minute(), + rt.second(), + ).unwrap_or(time) + } else { + time + }; + + let motif = record.revocation_reason.clone().unwrap_or_default(); + let motif: CrlReason = match motif.as_str() { + "key_compromise" => CrlReason::OcspRevokeKeyCompromise, + "ca_compromise" => CrlReason::OcspRevokeCaCompromise, + "affiliation_changed" => CrlReason::OcspRevokeAffChanged, + "superseded" => CrlReason::OcspRevokeSuperseded, + "cessation_of_operation" => CrlReason::OcspRevokeCessOperation, + "certificate_hold" => CrlReason::OcspRevokeCertHold, + "privilege_withdrawn" => CrlReason::OcspRevokePrivWithdrawn, + "aa_compromise" => CrlReason::OcspRevokeAaCompromise, + _ => CrlReason::OcspRevokeUnspecified, + }; + + Ok(OcspCertStatus::new( + CertStatusCode::Revoked, + Some(RevokedInfo::new(time, Some(motif))), + )) + } else { + Ok(OcspCertStatus::new(CertStatusCode::Good, None)) + } + } + }).await??; + + Ok(result) + } + + async fn check_cert_sqlite( + &self, + pool: &Pool>, + certnum: &str, + revoked: bool, + ) -> Result> { + let table_name = self.table_name.clone(); + let cert_num = certnum.to_string(); + let connection_manager = pool.clone(); + + let result = tokio::task::spawn_blocking(move || -> Result> { + let mut conn = connection_manager.get()?; + + // Using text SQL query with explicit column names and types + let query = format!( + "SELECT cert_num, revocation_time, revocation_reason, status FROM {} WHERE cert_num = ?", + table_name + ); + + let results = diesel::sql_query(query) + .bind::(&cert_num) + .load::(&mut conn)?; + + if results.is_empty() { + warn!("Entry not found for cert {} in SQLite", cert_num); + if !revoked { + Ok(OcspCertStatus::new(CertStatusCode::Unknown, None)) + } else { + Ok(OcspCertStatus::new( + CertStatusCode::Revoked, + Some(RevokedInfo::new( + GeneralizedTime::new(1970, 1, 1, 0, 0, 0).unwrap(), + Some(CrlReason::OcspRevokeCertHold), + )), + )) + } + } else { + let record = &results[0]; + debug!("Entry found for cert {}, status {}", cert_num, record.status); + + if record.status == "Revoked" { + let time = GeneralizedTime::now(); + + let time = if let Some(rt) = record.revocation_time { + GeneralizedTime::new( + rt.year(), + rt.month(), + rt.day(), + rt.hour(), + rt.minute(), + rt.second(), + ).unwrap_or(time) + } else { + time + }; + + let motif = record.revocation_reason.clone().unwrap_or_default(); + let motif: CrlReason = match motif.as_str() { + "key_compromise" => CrlReason::OcspRevokeKeyCompromise, + "ca_compromise" => CrlReason::OcspRevokeCaCompromise, + "affiliation_changed" => CrlReason::OcspRevokeAffChanged, + "superseded" => CrlReason::OcspRevokeSuperseded, + "cessation_of_operation" => CrlReason::OcspRevokeCessOperation, + "certificate_hold" => CrlReason::OcspRevokeCertHold, + "privilege_withdrawn" => CrlReason::OcspRevokePrivWithdrawn, + "aa_compromise" => CrlReason::OcspRevokeAaCompromise, + _ => CrlReason::OcspRevokeUnspecified, + }; + + Ok(OcspCertStatus::new( + CertStatusCode::Revoked, + Some(RevokedInfo::new(time, Some(motif))), + )) + } else { + Ok(OcspCertStatus::new(CertStatusCode::Good, None)) + } + } + }).await??; + + Ok(result) + } + #[cfg(feature="mysql")] + fn create_tables_if_needed_mysql( + &self, + pool: &Pool>, + ) -> Result<(), Box> { + if !self.config.create_table { + return Ok(()); + } + + let mut conn = pool.get()?; + let query = format!("SHOW TABLES LIKE '{}'", self.table_name); + + // For checking if table exists, we'll use execute instead of load for simpler handling + let exists: bool = diesel::sql_query(query) + .execute(&mut conn) + .map(|count| count > 0)?; + + if exists { + info!("Table {} already exists in MySQL database", self.table_name); + return Ok(()); + } + + let create_table_query = format!( + "CREATE TABLE `{}` ( + `cert_num` varchar(50) NOT NULL, + `revocation_time` datetime DEFAULT NULL, + `revocation_reason` enum('unspecified','key_compromise','ca_compromise','affiliation_changed','superseded','cessation_of_operation','certificate_hold','privilege_withdrawn','aa_compromise') DEFAULT NULL, + `status` enum('Valid','Revoked') NOT NULL DEFAULT 'Valid', + PRIMARY KEY (`cert_num`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", + self.table_name + ); + + diesel::sql_query(create_table_query).execute(&mut conn)?; + + info!( + "Table {} created successfully in MySQL database", + self.table_name + ); + Ok(()) + } + #[cfg(feature="postgres")] + fn create_tables_if_needed_postgres( + &self, + pool: &Pool>, + ) -> Result<(), Box> { + if !self.config.create_table { + return Ok(()); + } + + let mut conn = pool.get()?; + + let exists_query = format!( + "SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = '{}' + ) as exists", + self.table_name + ); + + let exists_results = diesel::sql_query(exists_query).load::(&mut conn)?; + + let exists = !exists_results.is_empty() && exists_results[0].exists; + + if exists { + info!( + "Table {} already exists in PostgreSQL database", + self.table_name + ); + return Ok(()); + } + + let types_exist_query = "SELECT EXISTS ( + SELECT FROM pg_type + WHERE typname = 'cert_status' + ) as exists"; + + let types_exist_results = + diesel::sql_query(types_exist_query).load::(&mut conn)?; + + let types_exist = !types_exist_results.is_empty() && types_exist_results[0].exists; + + if !types_exist { + diesel::sql_query("CREATE TYPE cert_status AS ENUM ('Valid', 'Revoked');") + .execute(&mut conn)?; + + diesel::sql_query( + "CREATE TYPE revocation_reason_enum AS ENUM ( + 'unspecified', + 'key_compromise', + 'ca_compromise', + 'affiliation_changed', + 'superseded', + 'cessation_of_operation', + 'certificate_hold', + 'privilege_withdrawn', + 'aa_compromise' + );", + ) + .execute(&mut conn)?; + } + + diesel::sql_query(format!( + "CREATE TABLE {} ( + cert_num VARCHAR(50) PRIMARY KEY, + revocation_time TIMESTAMP DEFAULT NULL, + revocation_reason revocation_reason_enum DEFAULT NULL, + status cert_status NOT NULL DEFAULT 'Valid' + );", + self.table_name + )) + .execute(&mut conn)?; + + info!( + "Table {} created successfully in PostgreSQL database", + self.table_name + ); + Ok(()) + } + + fn create_tables_if_needed_sqlite( + &self, + pool: &Pool>, + ) -> Result<(), Box> { + if !self.config.create_table { + return Ok(()); + } + + let mut conn = pool.get()?; + + let exists_query = format!( + "SELECT name FROM sqlite_master WHERE type='table' AND name='{}'", + self.table_name + ); + + let exists: bool = diesel::sql_query(exists_query) + .execute(&mut conn) + .map(|count| count > 0)?; + + if exists { + info!( + "Table {} already exists in SQLite database", + self.table_name + ); + return Ok(()); + } + + let create_table_query = format!( + "CREATE TABLE {} ( + cert_num TEXT PRIMARY KEY, + revocation_time TIMESTAMP DEFAULT NULL, + revocation_reason TEXT DEFAULT NULL CHECK( + revocation_reason IS NULL OR + revocation_reason IN ('unspecified', 'key_compromise', 'ca_compromise', + 'affiliation_changed', 'superseded', 'cessation_of_operation', + 'certificate_hold', 'privilege_withdrawn', 'aa_compromise') + ), + status TEXT NOT NULL DEFAULT 'Valid' CHECK(status IN ('Valid', 'Revoked')) + )", + self.table_name + ); + + diesel::sql_query(create_table_query).execute(&mut conn)?; + + info!( + "Table {} created successfully in SQLite database", + self.table_name + ); + Ok(()) + } + #[cfg(feature="mysql")] + async fn add_certificate_mysql( + &self, + pool: &Pool>, + cert_num: &str, + ) -> Result<(), Box> { + let table_name = self.table_name.clone(); + let cert_num = cert_num.to_string(); + let connection_manager = pool.clone(); + + tokio::task::spawn_blocking(move || -> Result<(), Box> { + let mut conn = connection_manager.get()?; + + // Check if certificate already exists + let query = format!( + "SELECT COUNT(*) as count FROM {} WHERE cert_num = ?", + table_name + ); + + let exists: bool = diesel::sql_query(query.clone()) + .bind::(&cert_num) + .execute(&mut conn)? + > 0; + + if exists { + return Err("Certificate already exists".into()); + } + + // Insert new certificate + let insert_query = format!( + "INSERT INTO {} (cert_num, status) VALUES (?, 'Valid')", + table_name + ); + + diesel::sql_query(insert_query) + .bind::(&cert_num) + .execute(&mut conn)?; + + Ok(()) + }) + .await? + } + #[cfg(feature="postgres")] + async fn add_certificate_postgres( + &self, + pool: &Pool>, + cert_num: &str, + ) -> Result<(), Box> { + let table_name = self.table_name.clone(); + let cert_num = cert_num.to_string(); + let connection_manager = pool.clone(); + + tokio::task::spawn_blocking(move || -> Result<(), Box> { + let mut conn = connection_manager.get()?; + + let query = format!( + "SELECT EXISTS (SELECT 1 FROM {} WHERE cert_num = $1) as exists", + table_name + ); + + let exists_results = diesel::sql_query(query) + .bind::(&cert_num) + .load::(&mut conn)?; + + let exists = !exists_results.is_empty() && exists_results[0].exists; + + if exists { + return Err("Certificate already exists".into()); + } + + let insert_query = format!( + "INSERT INTO {} (cert_num, status) VALUES ($1, 'Valid')", + table_name + ); + + diesel::sql_query(insert_query) + .bind::(&cert_num) + .execute(&mut conn)?; + + Ok(()) + }) + .await? + } + + async fn add_certificate_sqlite( + &self, + pool: &Pool>, + cert_num: &str, + ) -> Result<(), Box> { + let table_name = self.table_name.clone(); + let cert_num = cert_num.to_string(); + let connection_manager = pool.clone(); + + tokio::task::spawn_blocking(move || -> Result<(), Box> { + let mut conn = connection_manager.get()?; + + let query = format!( + "SELECT COUNT(*) as count FROM {} WHERE cert_num = ?", + table_name + ); + + let exists: bool = diesel::sql_query(query) + .bind::(&cert_num) + .execute(&mut conn)? + > 0; + + if exists { + return Err("Certificate already exists".into()); + } + + let insert_query = format!( + "INSERT INTO {} (cert_num, status) VALUES (?, 'Valid')", + table_name + ); + + diesel::sql_query(insert_query) + .bind::(&cert_num) + .execute(&mut conn)?; + + Ok(()) + }) + .await? + } + #[cfg(feature="mysql")] + async fn revoke_certificate_mysql( + &self, + pool: &Pool>, + cert_num: &str, + revocation_time: NaiveDateTime, + reason: &str, + ) -> Result<(), Box> { + let table_name = self.table_name.clone(); + let cert_num = cert_num.to_string(); + let reason = reason.to_string(); + let connection_manager = pool.clone(); + + tokio::task::spawn_blocking(move || -> Result<(), Box> { + let mut conn = connection_manager.get()?; + + // Check if certificate exists + let query = format!( + "SELECT COUNT(*) as count FROM {} WHERE cert_num = ?", + table_name + ); + + let exists: bool = diesel::sql_query(query.clone()) + .bind::(&cert_num) + .execute(&mut conn)? + > 0; + + if !exists { + return Err("Certificate does not exist".into()); + } + + // Update certificate status + let update_query = format!( + "UPDATE {} SET status = 'Revoked', revocation_time = ?, revocation_reason = ? WHERE cert_num = ?", + table_name + ); + + diesel::sql_query(update_query) + .bind::(&revocation_time) + .bind::(&reason) + .bind::(&cert_num) + .execute(&mut conn)?; + + Ok(()) + }).await? + } + #[cfg(feature="postgres")] + async fn revoke_certificate_postgres( + &self, + pool: &Pool>, + cert_num: &str, + revocation_time: NaiveDateTime, + reason: &str, + ) -> Result<(), Box> { + let table_name = self.table_name.clone(); + let cert_num = cert_num.to_string(); + let reason = reason.to_string(); + let connection_manager = pool.clone(); + + tokio::task::spawn_blocking(move || -> Result<(), Box> { + let mut conn = connection_manager.get()?; + + let query = format!( + "SELECT EXISTS (SELECT 1 FROM {} WHERE cert_num = $1) as exists", + table_name + ); + + let exists_results = diesel::sql_query(query) + .bind::(&cert_num) + .load::(&mut conn)?; + + let exists = !exists_results.is_empty() && exists_results[0].exists; + + if !exists { + return Err("Certificate does not exist".into()); + } + + // Update certificate status + // For PostgreSQL, we need to cast the text to the enum type + let update_query = format!( + "UPDATE {} SET status = 'Revoked', revocation_time = $1, revocation_reason = $2::revocation_reason_enum WHERE cert_num = $3", + table_name + ); + + diesel::sql_query(update_query) + .bind::(&revocation_time) + .bind::(&reason) + .bind::(&cert_num) + .execute(&mut conn)?; + + Ok(()) + }).await? + } + + async fn revoke_certificate_sqlite( + &self, + pool: &Pool>, + cert_num: &str, + revocation_time: NaiveDateTime, + reason: &str, + ) -> Result<(), Box> { + let table_name = self.table_name.clone(); + let cert_num = cert_num.to_string(); + let reason = reason.to_string(); + let connection_manager = pool.clone(); + + tokio::task::spawn_blocking(move || -> Result<(), Box> { + let mut conn = connection_manager.get()?; + + let query = format!( + "SELECT COUNT(*) as count FROM {} WHERE cert_num = ?", + table_name + ); + + let exists: bool = diesel::sql_query(query) + .bind::(&cert_num) + .execute(&mut conn)? + > 0; + + if !exists { + return Err("Certificate does not exist".into()); + } + + let update_query = format!( + "UPDATE {} SET status = 'Revoked', revocation_time = ?, revocation_reason = ? WHERE cert_num = ?", + table_name + ); + + diesel::sql_query(update_query) + .bind::(&revocation_time) + .bind::(&reason) + .bind::(&cert_num) + .execute(&mut conn)?; + + Ok(()) + }).await? + } + #[cfg(feature="mysql")] + async fn get_certificate_status_mysql( + &self, + pool: &Pool>, + cert_num: &str, + ) -> Result> { + let table_name = self.table_name.clone(); + let cert_num = cert_num.to_string(); + let connection_manager = pool.clone(); + + let result = tokio::task::spawn_blocking(move || -> Result> { + let mut conn = connection_manager.get()?; + + // Query certificate status + let query = format!( + "SELECT cert_num, status, revocation_time, revocation_reason FROM {} WHERE cert_num = ?", + table_name + ); + + let records = diesel::sql_query(query) + .bind::(&cert_num) + .load::(&mut conn)?; + + if records.is_empty() { + return Err("Certificate not found".into()); + } + + let record = &records[0]; + + Ok(Certinfo { + status: record.status.clone(), + revocation_time: record.revocation_time, + revocation_reason: record.revocation_reason.clone(), + }) + }).await??; + + Ok(result) + } + #[cfg(feature="postgres")] + async fn get_certificate_status_postgres( + &self, + pool: &Pool>, + cert_num: &str, + ) -> Result> { + let table_name = self.table_name.clone(); + let cert_num = cert_num.to_string(); + let connection_manager = pool.clone(); + + let result = tokio::task::spawn_blocking(move || -> Result> { + let mut conn = connection_manager.get()?; + + // Query certificate status + let query = format!( + "SELECT cert_num, status, revocation_time, revocation_reason FROM {} WHERE cert_num = $1", + table_name + ); + + let records = diesel::sql_query(query) + .bind::(&cert_num) + .load::(&mut conn)?; + + if records.is_empty() { + return Err("Certificate not found".into()); + } + + let record = &records[0]; + + Ok(Certinfo { + status: record.status.clone(), + revocation_time: record.revocation_time, + revocation_reason: record.revocation_reason.clone(), + }) + }).await??; + + Ok(result) + } + + async fn get_certificate_status_sqlite( + &self, + pool: &Pool>, + cert_num: &str, + ) -> Result> { + let table_name = self.table_name.clone(); + let cert_num = cert_num.to_string(); + let connection_manager = pool.clone(); + + let result = tokio::task::spawn_blocking(move || -> Result> { + let mut conn = connection_manager.get()?; + + let query = format!( + "SELECT cert_num, status, revocation_time, revocation_reason FROM {} WHERE cert_num = ?", + table_name + ); + + let records = diesel::sql_query(query) + .bind::(&cert_num) + .load::(&mut conn)?; + + if records.is_empty() { + return Err("Certificate not found".into()); + } + + let record = &records[0]; + + Ok(Certinfo { + status: record.status.clone(), + revocation_time: record.revocation_time, + revocation_reason: record.revocation_reason.clone(), + }) + }).await??; + + Ok(result) + } + #[cfg(feature="mysql")] + async fn list_certificates_mysql( + &self, + pool: &Pool>, + status: Option, + ) -> Result, Box> { + let table_name = self.table_name.clone(); + let status_filter = status.map(|s| s.to_string()); + let connection_manager = pool.clone(); + + let result = tokio::task::spawn_blocking( + move || -> Result, Box> { + let mut conn = connection_manager.get()?; + + // Build query based on status filter (MySQL) + let query = match &status_filter { + Some(_s) => format!( + "SELECT cert_num, status, revocation_time, revocation_reason FROM {} WHERE status = ?", + table_name + ), + None => format!("SELECT cert_num, status, revocation_time, revocation_reason FROM {}", table_name), + }; + + // Execute query with or without filter + let records: Vec = if let Some(s) = &status_filter { + diesel::sql_query(query) + .bind::(s) + .load::(&mut conn)? + } else { + diesel::sql_query(query).load::(&mut conn)? + }; + + // Convert to response format + let responses = records + .into_iter() + .map(|record| CertificateResponse { + cert_num: record.cert_num, + status: record.status, + message: String::new(), + }) + .collect(); + + Ok(responses) + }, + ) + .await??; + + Ok(result) + } + #[cfg(feature="postgres")] + async fn list_certificates_postgres( + &self, + pool: &Pool>, + status: Option, + ) -> Result, Box> { + let table_name = self.table_name.clone(); + let status_filter = status.map(|s| s.to_string()); + let connection_manager = pool.clone(); + + let result = tokio::task::spawn_blocking( + move || -> Result, Box> { + let mut conn = connection_manager.get()?; + + // Build query based on status filter (PostgreSQL) + let query = match &status_filter { + Some(_s) => format!( + "SELECT cert_num, status, revocation_time, revocation_reason FROM {} WHERE status = $1::cert_status", + table_name + ), + None => format!("SELECT cert_num, status, revocation_time, revocation_reason FROM {}", table_name), + }; + + // Execute query with or without filter + let records: Vec = if let Some(s) = &status_filter { + diesel::sql_query(query) + .bind::(s) + .load::(&mut conn)? + } else { + diesel::sql_query(query).load::(&mut conn)? + }; + + // Convert to response format + let responses = records + .into_iter() + .map(|record| CertificateResponse { + cert_num: record.cert_num, + status: record.status, + message: String::new(), + }) + .collect(); + + Ok(responses) + }, + ) + .await??; + + Ok(result) + } + + async fn list_certificates_sqlite( + &self, + pool: &Pool>, + status: Option, + ) -> Result, Box> { + let table_name = self.table_name.clone(); + let status_filter = status.map(|s| s.to_string()); + let connection_manager = pool.clone(); + + let result = tokio::task::spawn_blocking( + move || -> Result, Box> { + let mut conn = connection_manager.get()?; + + let query = match &status_filter { + Some(_s) => format!( + "SELECT cert_num, status, revocation_time, revocation_reason FROM {} WHERE status = ?", + table_name + ), + None => format!("SELECT cert_num, status, revocation_time, revocation_reason FROM {}", table_name), + }; + + let records: Vec = if let Some(s) = &status_filter { + diesel::sql_query(query) + .bind::(s) + .load::(&mut conn)? + } else { + diesel::sql_query(query).load::(&mut conn)? + }; + + let responses = records + .into_iter() + .map(|record| CertificateResponse { + cert_num: record.cert_num, + status: record.status, + message: String::new(), + }) + .collect(); + + Ok(responses) + }, + ) + .await??; + + Ok(result) + } +} + +#[async_trait] +impl Database for DieselDatabase { + async fn check_cert( + &self, + certnum: &str, + revoked: bool, + ) -> Result> { + match &self.connection { + #[cfg(feature="mysql")] + DatabaseConnection::MySQL(pool) => self.check_cert_mysql(pool, certnum, revoked).await, + #[cfg(feature="postgres")] + DatabaseConnection::PostgreSQL(pool) => { + self.check_cert_postgres(pool, certnum, revoked).await + } + DatabaseConnection::SQLite(pool) => { + self.check_cert_sqlite(pool, certnum, revoked).await + } + } + } + + fn create_tables_if_needed(&self) -> Result<(), Box> { + match &self.connection { + #[cfg(feature="mysql")] + DatabaseConnection::MySQL(pool) => self.create_tables_if_needed_mysql(pool), + #[cfg(feature="postgres")] + DatabaseConnection::PostgreSQL(pool) => self.create_tables_if_needed_postgres(pool), + DatabaseConnection::SQLite(pool) => self.create_tables_if_needed_sqlite(pool), + } + } + + async fn add_certificate(&self, cert_num: &str) -> Result<(), Box> { + match &self.connection { + #[cfg(feature="mysql")] + DatabaseConnection::MySQL(pool) => self.add_certificate_mysql(pool, cert_num).await, + #[cfg(feature="postgres")] + DatabaseConnection::PostgreSQL(pool) => { + self.add_certificate_postgres(pool, cert_num).await + } + DatabaseConnection::SQLite(pool) => self.add_certificate_sqlite(pool, cert_num).await, + } + } + + async fn revoke_certificate( + &self, + cert_num: &str, + revocation_time: NaiveDateTime, + reason: &str, + ) -> Result<(), Box> { + let now = chrono::Utc::now().naive_utc(); + if revocation_time > now { + return Err("Revocation time cannot be in the future".into()); + } + + match &self.connection { + #[cfg(feature="mysql")] + DatabaseConnection::MySQL(pool) => { + self.revoke_certificate_mysql(pool, cert_num, revocation_time, reason) + .await + } + #[cfg(feature="postgres")] + DatabaseConnection::PostgreSQL(pool) => { + self.revoke_certificate_postgres(pool, cert_num, revocation_time, reason) + .await + } + DatabaseConnection::SQLite(pool) => { + self.revoke_certificate_sqlite(pool, cert_num, revocation_time, reason) + .await + } + } + } + + async fn get_certificate_status( + &self, + cert_num: &str, + ) -> Result> { + match &self.connection { + #[cfg(feature="mysql")] + DatabaseConnection::MySQL(pool) => { + self.get_certificate_status_mysql(pool, cert_num).await + } + #[cfg(feature="postgres")] + DatabaseConnection::PostgreSQL(pool) => { + self.get_certificate_status_postgres(pool, cert_num).await + } + DatabaseConnection::SQLite(pool) => { + self.get_certificate_status_sqlite(pool, cert_num).await + } + } + } + + async fn list_certificates( + &self, + status: Option, + ) -> Result, Box> { + match &self.connection { + #[cfg(feature="mysql")] + DatabaseConnection::MySQL(pool) => self.list_certificates_mysql(pool, status).await, + #[cfg(feature="postgres")] + DatabaseConnection::PostgreSQL(pool) => { + self.list_certificates_postgres(pool, status).await + } + DatabaseConnection::SQLite(pool) => self.list_certificates_sqlite(pool, status).await, + } + } +} + +pub fn create_database( + config: Arc, +) -> Result, Box> { + let db = DieselDatabase::new(config)?; + Ok(Box::new(db)) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 268c17d..e5a293c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,11 @@ -#[macro_use] extern crate rocket; + use chrono::{self, NaiveDateTime, Timelike}; use chrono::{DateTime, Datelike, FixedOffset}; -use clap::Parser; +use clap::{CommandFactory, Parser}; use config_file::FromConfigFile; -use mysql::prelude::Queryable; -use mysql::*; +use core::str; +use log::{debug, error, info, trace, warn}; use ocsp::common::asn1::Bytes; use ocsp::common::ocsp::{OcspExt, OcspExtI}; use ocsp::request::OcspRequest; @@ -14,31 +14,61 @@ use ocsp::{ err::OcspError, oid::{ALGO_SHA256_WITH_RSA_ENCRYPTION_DOT, OCSP_RESPONSE_BASIC_DOT}, response::{ - BasicResponse, CertStatus as OcspCertStatus, CertStatus, CertStatusCode, CrlReason, - OcspRespStatus, OcspResponse, OneResp, ResponderId, ResponseBytes, ResponseData, - RevokedInfo, + BasicResponse, CertStatus, CertStatusCode, OcspRespStatus, OcspResponse, OneResp, + ResponderId, ResponseBytes, ResponseData, }, }; +use openssl::pkey::PKey; +use openssl::rsa::Rsa; use pem::parse; use ring::digest::SHA1_FOR_LEGACY_USE_ONLY; use ring::{rand, signature}; use rocket::State; use rocket::http::ContentType; use rocket::{Data, data::ToByteUnit}; -use r#struct::*; +use rocket::{get, launch, post, routes}; use std::error::Error; -use std::fs; -use std::io; +use std::fs::{self, File}; +use std::io::{self, Read}; use std::net::SocketAddr; -use std::path::Path; -use std::time::Duration; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use r#struct::*; use x509_parser::oid_registry::OID_X509_EXT_AUTHORITY_KEY_IDENTIFIER; use x509_parser::prelude::ParsedExtension; use zeroize::Zeroize; +//#[cfg(feature = "api")] +mod api; +mod database; mod r#struct; +#[cfg(test)] +#[path = "./tests/test.rs"] +mod test; +use database::Database; +#[derive(Parser, Debug)] +#[clap( + author = crate_authors!("\n"), + before_help = "OCSP server, listening for requests to give responses.", + after_help = "This script is maintained on Github.", + help_template = "\ + {name} {version} + Authors: {author-section} + {before-help} + About: {about-with-newline} + {usage-heading} {usage} + + {all-args}{after-help} + " +)] +#[command(version, author, about, long_about = None)] +struct Cli { + #[arg(default_value = "config.toml")] + config_path: PathBuf, +} + fn signresponse( issuer_hash: &[u8], - private_key: &ring::rsa::KeyPair, + private_key: &ring::signature::RsaKeyPair, response: Vec, extensions: Option>, cert: Option>, @@ -61,95 +91,17 @@ fn signresponse( let bytes = ResponseBytes::new_basic(resp_type, basic)?; Ok(bytes) } + fn signvalidresponse(bytes: ResponseBytes) -> Result, OcspError> { let ocsp = OcspResponse::new_success(bytes); ocsp.to_der() } + fn signnonvalidresponse(motif: OcspRespStatus) -> Result, OcspError> { let ocsp = OcspResponse::new_non_success(motif)?; ocsp.to_der() } -fn checkcert( - config: &State, - certnum: &str, - revoked: bool, -) -> Result { - // Let's select payments from database. Type inference should do the trick here. - let opts = OptsBuilder::new() - .user(Some(config.dbuser.as_str())) - .read_timeout(Some(Duration::new(config.time as u64, 0))) - .db_name(Some(config.dbname.as_str())) - .pass(Some(config.dbpassword.as_str())); - let opts = match &config.dbip { - Some(string) => opts.ip_or_hostname(Some(string)), - None => opts - .prefer_socket(true) - .socket(Some("/run/mysqld/mysqld.sock")), - }; - let mut conn = Conn::new(opts)?; - let status = conn.exec_map( - "SELECT status, revocation_time, revocation_reason FROM list_certs WHERE cert_num=?", - (String::from(certnum).into_bytes(),), - |(status, revocation_time, revocation_reason)| Certinfo { - status, - revocation_time, - revocation_reason, - }, - )?; - if status.is_empty() { - warn!("Entry not found for cert {}", certnum); - if !revoked { - Ok(OcspCertStatus::new(CertStatusCode::Unknown, None)) - } else { - Ok(OcspCertStatus::new( - CertStatusCode::Revoked, - Some(RevokedInfo::new( - GeneralizedTime::new(1970, 1, 1, 0, 0, 0).unwrap(), - Some(CrlReason::OcspRevokeCertHold), - )), - )) - } - } else { - let statut = status[0].clone(); - debug!("Entry found for cert {}, status {}", certnum, statut.status); - if statut.status == "Revoked" { - let time = GeneralizedTime::now(); - let date = &statut.revocation_time; - let timenew = match date { - Some(mysql::Value::Date(year, month, day, hour, min, sec, _ms)) => { - GeneralizedTime::new( - i32::from(*year), - u32::from(*month), - u32::from(*day), - u32::from(*hour), - u32::from(*min), - u32::from(*sec), - ) - } - _ => Ok(time), - }; - let time = timenew.unwrap_or(time); - let motif = statut.revocation_reason.unwrap_or_default(); - let motif: CrlReason = match motif.as_str() { - "key_compromise" => CrlReason::OcspRevokeKeyCompromise, - "ca_compromise" => CrlReason::OcspRevokeCaCompromise, - "affiliation_changed" => CrlReason::OcspRevokeAffChanged, - "superseded" => CrlReason::OcspRevokeSuperseded, - "cessation_of_operation" => CrlReason::OcspRevokeCessOperation, - "certificate_hold" => CrlReason::OcspRevokeCertHold, - "privilege_withdrawn" => CrlReason::OcspRevokePrivWithdrawn, - "aa_compromise" => CrlReason::OcspRevokeAaCompromise, - _ => CrlReason::OcspRevokeUnspecified, - }; - Ok(OcspCertStatus::new( - CertStatusCode::Revoked, - Some(RevokedInfo::new(time, Some(motif))), - )) - } else { - Ok(OcspCertStatus::new(CertStatusCode::Good, None)) - } - } -} + fn createocspresponse( cert: CertId, cert_status: CertStatus, @@ -183,7 +135,8 @@ fn createocspresponse( one_resp_ext: extension, }) } -fn checkcache(state: &State, certname: &str) -> io::Result>> { + +fn checkcache(state: &State>, certname: &str) -> io::Result>> { let paths = fs::read_dir(&state.cachefolder)?; for path in paths { let path = path?.path(); @@ -193,7 +146,7 @@ fn checkcache(state: &State, certname: &str) -> io::Result = filename.split(&certname).collect(); + let elem: Vec<&str> = filename.split(certname).collect(); if elem.len() != 2 { warn!("Invalid filename to check cache: {}", filename); continue; @@ -226,8 +179,9 @@ fn checkcache(state: &State, certname: &str) -> io::Result, + state: &State>, certnum: &str, maxdate: DateTime, response: &[u8], @@ -241,17 +195,20 @@ fn addtocache( let path = Path::new(&long); fs::write(path, response) } + #[post("/<_..>", data = "")] async fn upload2( - config: &State, + config: &State>, + db: &State>, data: Data<'_>, address: SocketAddr, ) -> io::Result<(ContentType, Vec)> { - upload(config, data, address).await + upload(config, db, data, address).await } #[get("/<_..>", data = "")] async fn upload( - state: &State, + state: &State>, + db: &State>, data: Data<'_>, address: SocketAddr, ) -> io::Result<(ContentType, Vec)> { @@ -280,7 +237,7 @@ async fn upload( ocsp::common::ocsp::OcspExt::Nonce { nonce } => Some(nonce.len()), _ => None, }) - .last() + .next_back() }) { Some(1..128) | None => (), _ => { @@ -352,7 +309,8 @@ async fn upload( }; extensions.push(revoked); }; - //Compare that signing certificate is signed by the issuer or the issuer itself https://www.rfc-editor.org/rfc/rfc6960 + + // Compare that signing certificate is signed by the issuer or the issuer itself https://www.rfc-editor.org/rfc/rfc6960 let status = match ( cert.issuer_key_hash == state.issuer_hash.0, state.issuer_hash.1 == cert.issuer_key_hash, @@ -365,10 +323,11 @@ async fn upload( } else { trace!("Certificate is the issuer."); } - match checkcert(state, &num, state.revocextended) { + + match db.check_cert(&num, state.revocextended).await { Ok(status) => status, - Err(default) => { - error!("Cannot connect to database: {}", default.to_string()); + Err(err) => { + error!("Cannot query database: {}", err); return Ok(( custom, signnonvalidresponse(OcspRespStatus::TryLater).unwrap(), @@ -396,11 +355,7 @@ async fn upload( CertStatus::new(CertStatusCode::Unknown, None) } }; - /* let mut extension = OcspExtI { - id: i2b_oid(ocsp::common::asn1::Oid::new_from_dot(OCSP_EXT_EXTENDED_REVOKE_DOT)), - ext: ocsp::common::ocsp::OcspExt::CrlRef { url: None, num: Some(OCSP_EXT_EXTENDED_REVOKE_HEX.to_vec()), time: None } - }; - let resp = createocspresponse(cert, status, Some(state.cachedays), None, None, Some(vec![extension])); */ + let resp = createocspresponse(cert, status, Some(state.cachedays), None, None, None); if resp.is_err() { error!("Error creating OCSP response."); @@ -423,7 +378,7 @@ async fn upload( Some(extensions) }; let needthecert: Option> = if needthecert { - Some(vec![state.cert.clone()]) + Some(vec![state.cert.to_vec()]) } else { None }; @@ -456,9 +411,9 @@ async fn upload( let response = response.unwrap(); if possible { let date = chrono::Local::now(); - let date = date.checked_add_days(chrono::Days::new(state.cachedays.into())); //TODO: Implement - if date.is_some() { - match addtocache(state, &certnum, date.unwrap().fixed_offset(), &response) { + let date = date.checked_add_days(chrono::Days::new(u64::from(state.cachedays))); + if let Some(date) = date { + match addtocache(state, &certnum, date.fixed_offset(), &response) { Ok(_) => (), Err(_) => { warn!("Cannot write to cache"); @@ -469,58 +424,112 @@ async fn upload( info!("Send response for {} to {}", &certnum, address.ip()); Ok((custom, response)) } -fn getprivatekey(data: T) -> Result + +fn getprivatekey(data: T) -> Result where T: AsRef<[u8]>, { - ring::rsa::KeyPair::from_pkcs8(data.as_ref()) + if let Ok(key_pair) = ring::signature::RsaKeyPair::from_pkcs8(data.as_ref()) { + return Ok(key_pair); + } + + match str::from_utf8(data.as_ref()) { + Ok(s) => { + //PEM format so we try to parse + match convert_rsa_pem_to_pkcs8(s) { + Ok(k) => match ring::signature::RsaKeyPair::from_pkcs8(&k) { + Ok(key_pair) => Ok(key_pair), + Err(e) => Err(format!("Error creating KeyPair from PEM PKCS#8: {}", e)), + }, + Err(_) => Err("Error creating KeyPair from PEM.".to_string()), + } + } + Err(_) => { + //Binary so we hope it's DER key, else fail + match ring::signature::RsaKeyPair::from_der(data.as_ref()) { + Ok(key_pair) => Ok(key_pair), + Err(e) => Err(format!("Error creating KeyPair from DER: {}", e)), + } + } + } +} +fn convert_rsa_pem_to_pkcs8( + pem_str: &str, +) -> Result, Box> { + let pem = pem::parse(pem_str.as_bytes())?; + let rsa = Rsa::private_key_from_der(pem.contents())?; + let pkey = PKey::from_rsa(rsa)?; + Ok(pkey.private_key_to_pkcs8()?) } -fn pem_to_der(pem_str: &str) -> Vec { +fn pem_to_der(pem_str: &str) -> Result, String> { match parse(pem_str.as_bytes()) { - Ok(pem) => pem.contents().to_vec(), - Err(e) => { - eprintln!("Error parsing PEM: {}", e); - panic!("Invalid PEM format") - } + Ok(pem) => Ok(pem.contents().to_vec()), + Err(e) => Err(format!("Error parsing PEM: {}", e)), } } + #[launch] -fn rocket() -> _ { +fn rocket() -> rocket::Rocket { let cli = Cli::parse(); let config_path = &cli.config_path; if !Path::new(config_path).exists() { - panic!("Config file not found at: {}", config_path); + eprintln!("Error: Config file not found at: {}", config_path); + eprintln!("\nUsage information:"); + let mut cli_command = Cli::command(); + if let Err(err) = cli_command.print_help() { + eprintln!("Could not display help: {}", err); + } + std::process::exit(1); } let config = match Fileconfig::from_config_file(config_path) { Ok(config) => config, Err(e) => { - panic!("Error reading config file at {}: {}", config_path, e); + eprintln!("Error: Reading config file at {}: {}", config_path, e); + eprintln!("\nUsage information:"); + let mut cli_command = Cli::command(); + if let Err(err) = cli_command.print_help() { + eprintln!("Could not display help: {}", err); + } + std::process::exit(1); } }; - - let file = fs::read_to_string(&config.itcert).expect("Intermediate cert is not found"); - let file2 = pem_to_der(&file); - let certpem = x509_parser::pem::parse_x509_pem(file.as_bytes()) - .expect("Invalid intermediate certificate.") - .1; - let certpem = certpem - .parse_x509() - .expect("Invalid intermediate certificate."); - let isocsp = certpem + let cert_raw = match File::open(&config.itcert) { + Ok(mut f) => { + let mut data = String::new(); + f.read_to_string(&mut data) + .expect("Cannot read intermediate cert"); + data + } + Err(e) => { + panic!("Intermediate cert is not found. Error is {}", e); + } + }; + let (certder, cert) = match x509_parser::pem::parse_x509_pem(cert_raw.as_bytes()) { + Ok(e) => (pem_to_der(&cert_raw).expect("Cannot parse intermediate certificate"), e), + Err(_) => { + panic!("Cannot parse intermediate certificate") + } + }; + let cert = cert.1.parse_x509().unwrap(); + let isocsp = cert .extended_key_usage() .unwrap() .is_some_and(|f| f.value.ocsp_signing || f.value.any); - if !isocsp { + let isca = cert.is_ca(); + if !isocsp && !isca { + panic!( + "Your certificate does not have OCSP signing extended key usage and is not a CA, it cannot sign response. Exiting" + ); + } else if !isocsp { eprintln!( "Your certificate does not have OCSP signing extended key usage. If it is not the issuer, the application won't sign the response." ) } - //let subjectkey = format!("{:x}", issuerkey).to_uppercase().replace(":", ""); - let parsed = certpem + let parsed = cert .get_extension_unique(&OID_X509_EXT_AUTHORITY_KEY_IDENTIFIER) .unwrap() .unwrap() @@ -531,26 +540,58 @@ fn rocket() -> _ { panic!("Error getting key"); } }; - //For an unknown reason, subject key identifier is not equal to SHA1 hash key so it is used instead. + + // For an unknown reason, subject key identifier is not equal to SHA1 hash key so it is used instead. let authoritykey = format!("{:x}", issuerkey).to_uppercase().replace(":", ""); - let certpempublickey = &certpem.public_key().subject_public_key.data; + let certpempublickey = &cert.public_key().subject_public_key.data; let sha1key = ring::digest::digest(&SHA1_FOR_LEGACY_USE_ONLY, certpempublickey); - //let issuer_name_hash = certpem.subject_name_hash(); - let mut key = fs::read(&config.itkey).unwrap(); - let rsakey = getprivatekey(&key).unwrap(); + + // Read private key and zero it after use + let mut key = fs::read(&config.itkey).unwrap_or_else(|e| { + panic!("Error reading key file: {}", e); + }); + + let rsakey = match getprivatekey(&key) { + Ok(key_pair) => key_pair, + Err(e) => { + eprintln!("Error loading private key: {}", e); + eprintln!("Supported formats: PKCS#8, PEM PKCS#1 (RSA)"); + panic!("Key loading failed"); + } + }; key.zeroize(); + + // Get HTTP port let port = config.port.unwrap_or(DEFAULT_PORT); - let config = Config { + + // Get listen IP address + let listen_ip = config + .listen_ip + .clone() + .unwrap_or_else(|| DEFAULT_LISTEN_IP.to_string()); + + + // Determine database type and default port + let db_type = config + .db_type + .clone() + .unwrap_or_else(|| "mysql".to_string()); + #[cfg(any(feature = "mysql",feature="postgres"))] + let dbport = match db_type.as_str() { + "postgres" | "postgresql" => config.dbport.or(Some(DEFAULT_POSTGRES_PORT)), + _ => config.dbport.or(Some(DEFAULT_MYSQL_PORT)), + }; + + // Create configuration + let config = Arc::new(Config { issuer_hash: ( sha1key.as_ref().to_vec(), - //hex::decode(subjectkey).unwrap(), hex::decode(authoritykey).unwrap(), isocsp, ), revocextended: config.revocextended.unwrap_or(false), - cert: file2, + cert: certder, time: config.timeout.unwrap_or(DEFAULT_TIMEOUT), - //issuer_name_hash, rsakey, cachefolder: config.cachefolder.clone(), caching: config.caching.unwrap_or(true), @@ -559,94 +600,57 @@ fn rocket() -> _ { dbuser: config.dbuser.clone(), dbpassword: config.dbpassword.clone(), dbname: config.dbname.clone(), + db_type, + #[cfg(any(feature = "mysql",feature="postgres"))] + dbport, + create_table: config.create_table.unwrap_or(false), + table_name: config.table_name.clone(), + api_keys: config.api_keys.clone(), + enable_api: config.enable_api.unwrap_or(false), + listen_ip, + }); + + // Create database connection and tables if needed + let db = match database::create_database(config.clone()) { + Ok(db) => { + if let Err(e) = db.create_tables_if_needed() { + eprintln!("Error creating tables: {}", e); + } + db + } + Err(e) => { + panic!("Failed to initialize database: {}", e); + } }; + + // Create cache folder if it doesn't exist let path = Path::new(config.cachefolder.as_str()); if !path.exists() { fs::create_dir_all(path).expect("Cannot create cache folder"); } - rocket::build() - .configure(rocket::Config::figment().merge(("port", port))) + + // Set up Rocket to listen on the configured IP address and port + let figment = rocket::Config::figment() + .merge(("port", port)) + .merge(("address", config.listen_ip.clone())); + + // Create rocket instance with routes + let mut rocket_builder = rocket::build() + .configure(figment) .mount("/", routes![upload]) .mount("/", routes![upload2]) - .manage(config) -} -#[cfg(test)] -mod tests { - use ring::rand::SecureRandom; - use ring::signature::KeyPair; - use zeroize::Zeroize; - use std::{fs, time::Instant}; - use std::path::Path; - use crate::rocket; - use clap::Parser; - use crate::{getprivatekey, Fileconfig}; - use ring::{rand, signature}; - use config_file::FromConfigFile; - use crate::Cli; -#[test] -fn testresponse() { - println!("Generating key, may take a while..."); - let rng = rand::SystemRandom::new(); - let pkcs8_bytes = signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap(); - - // Normally the application would store the PKCS#8 file persistently. Later - // it would read the PKCS#8 file from persistent storage to use it. - - let key_pair = signature::Ed25519KeyPair::from_pkcs8(pkcs8_bytes.as_ref()).unwrap(); - - println!("Done."); - let mut tosign = [0u8; 3000]; - println!("Generating random"); - rng.fill(&mut tosign).unwrap(); - println!("Done!"); - let time = Instant::now(); - for i in 0..100 { - if i % 10 == 0 { - rng.fill(&mut tosign).unwrap(); + .manage(config.clone()) + .manage(db as Box); + if cfg!(feature = "api") { + if config.enable_api { + info!("API functionality is enabled"); + if config.api_keys.is_none() || config.api_keys.as_ref().unwrap().is_empty() { + warn!("API is enabled but no API keys are configured - this is insecure and will lead to failure."); + } + rocket_builder = rocket_builder.mount("/api", api::api_routes()); + } else { + info!("API functionality is disabled"); } - let sig = key_pair.sign(&tosign); - // Normally an application would extract the bytes of the signature and - // send them in a protocol message to the peer(s). Here we just get the - // public key key directly from the key pair. - let peer_public_key_bytes = key_pair.public_key().as_ref(); - - // Verify the signature of the message using the public key. Normally the - // verifier of the message would parse the inputs to this code out of the - // protocol message(s) sent by the signer. - let peer_public_key = - signature::UnparsedPublicKey::new(&signature::ED25519, peer_public_key_bytes); - peer_public_key.verify(&tosign, sig.as_ref()).unwrap(); } - println!("Elapsed time : {:.6} ms", time.elapsed().as_millis()); -} -#[test] -#[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: KeyRejected(\"InvalidEncoding\")" -)] -fn checkconfigfake() { - let cli = Cli::parse(); - - let config_path = &cli.config_path; - - if !Path::new(config_path).exists() { - panic!("Config file not found at: {}", config_path); - } - - let mut config = match Fileconfig::from_config_file(config_path) { - Ok(config) => config, - Err(e) => { - panic!("Error reading config file at {}: {}", config_path, e); - } - }; - config.itkey = String::from("test_files/key.pem"); - //For an unknown reason, subject key identifier is not equal to SHA1 hash key so it is used instead. - //let issuer_name_hash = certpem.subject_name_hash(); - let mut key = fs::read(&config.itkey).unwrap(); - let _rsakey = getprivatekey(&key).unwrap(); - key.zeroize(); -} -#[test] -fn checkconfig() { - rocket(); + rocket_builder } -} \ No newline at end of file diff --git a/src/struct.rs b/src/struct.rs index c13b0aa..1bbe6c5 100644 --- a/src/struct.rs +++ b/src/struct.rs @@ -1,7 +1,13 @@ -use clap::{Parser, crate_authors}; +use chrono::NaiveDateTime; +//use chrono::NaiveDateTime; +use clap::{crate_authors, Parser}; +use diesel::QueryableByName; use ocsp::common::asn1::Bytes; +//use ocsp::common::asn1::Bytes; +use serde::{Deserialize, Serialize}; +//use x509_parser::prelude::X509Certificate; use zeroize::Zeroize; -use serde::Deserialize; + #[derive(Parser, Debug)] #[clap( author = crate_authors!("\n"), @@ -25,15 +31,24 @@ pub(crate) struct Cli { pub(crate) const CACHEFORMAT: &str = "%Y-%m-%d-%H-%M-%S"; pub(crate) const DEFAULT_PORT: u16 = 9000; +pub(crate) const DEFAULT_LISTEN_IP: &str = "localhost"; pub(crate) const DEFAULT_TIMEOUT: u8 = 5; -// In a real application, this would likely be more complex. +#[cfg(feature = "mysql")] +pub(crate) const DEFAULT_MYSQL_PORT: u16 = 3306; +#[cfg(feature = "postgres")] +pub(crate) const DEFAULT_POSTGRES_PORT: u16 = 5432; +#[cfg(feature = "mysql")] +pub(crate) const DEFAULT_MYSQL_TABLE: &str = DEFAULT_SQLITE_TABLE; +#[cfg(feature = "postgres")] +pub(crate) const DEFAULT_POSTGRES_TABLE: &str = DEFAULT_SQLITE_TABLE; +pub(crate) const DEFAULT_SQLITE_TABLE: &str = "list_certs"; + #[derive(Debug)] pub(crate) struct Config { pub(crate) issuer_hash: (Vec, Vec, bool), pub(crate) cert: Bytes, pub(crate) revocextended: bool, pub(crate) time: u8, - //issuer_name_hash: u32, pub(crate) rsakey: ring::signature::RsaKeyPair, pub(crate) cachedays: u16, pub(crate) caching: bool, @@ -41,8 +56,20 @@ pub(crate) struct Config { pub(crate) dbuser: String, pub(crate) dbpassword: String, pub(crate) dbname: String, + pub(crate) db_type: String, + #[cfg(any(feature = "mysql",feature="postgres"))] + pub(crate) dbport: Option, + pub(crate) create_table: bool, pub(crate) cachefolder: String, + pub(crate) table_name: Option, + pub(crate) api_keys: Option>, + pub(crate) enable_api: bool, + pub(crate) listen_ip: String, } + +// Don't implement Default for Config, because we can't easily create a dummy RsaKeyPair. +// We'll use explicit constructors in tests instead. + impl Drop for Config { fn drop(&mut self) { self.dbip.zeroize(); @@ -51,6 +78,7 @@ impl Drop for Config { self.dbname.zeroize(); } } + #[derive(Deserialize)] pub(crate) struct Fileconfig { pub(crate) cachedays: u16, @@ -58,14 +86,23 @@ pub(crate) struct Fileconfig { pub(crate) revocextended: Option, pub(crate) dbip: Option, pub(crate) port: Option, + pub(crate) listen_ip: Option, pub(crate) timeout: Option, pub(crate) dbuser: String, pub(crate) dbpassword: String, pub(crate) dbname: String, + pub(crate) db_type: Option, + #[allow(dead_code)] + pub(crate) dbport: Option, + pub(crate) create_table: Option, pub(crate) cachefolder: String, pub(crate) itkey: String, pub(crate) itcert: String, + pub(crate) table_name: Option, + pub(crate) api_keys: Option>, + pub(crate) enable_api: Option, } + impl Drop for Fileconfig { fn drop(&mut self) { self.dbip.zeroize(); @@ -74,9 +111,57 @@ impl Drop for Fileconfig { self.dbname.zeroize(); } } + #[derive(Debug, PartialEq, Clone)] +#[allow(dead_code)] pub(crate) struct Certinfo { pub(crate) status: String, - pub(crate) revocation_time: Option, + pub(crate) revocation_time: Option, + pub(crate) revocation_reason: Option, +} + +#[derive(Debug, Clone, PartialEq, QueryableByName)] +pub(crate) struct CertRecord { + #[diesel(sql_type = diesel::sql_types::Text)] + pub(crate) cert_num: String, + + #[diesel(sql_type = diesel::sql_types::Nullable)] + pub(crate) revocation_time: Option, + + #[diesel(sql_type = diesel::sql_types::Nullable)] pub(crate) revocation_reason: Option, -} \ No newline at end of file + + #[diesel(sql_type = diesel::sql_types::Text)] + pub(crate) status: String, +} +#[cfg(feature = "postgres")] +#[derive(Debug, QueryableByName)] +pub(crate) struct BoolResult { + #[diesel(sql_type = diesel::sql_types::Bool)] + pub(crate) exists: bool, +} + +// API Authentication +#[derive(Debug)] +#[allow(dead_code)] +pub(crate) struct ApiKey(pub(crate) String); + +// API Request and Response Structures +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct CertificateRequest { + pub cert_num: String, +} +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct RevocationRequest { + pub(crate) cert_num: String, + pub(crate) reason: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) revocation_time: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct CertificateResponse { + pub(crate) cert_num: String, + pub(crate) status: String, + pub(crate) message: String, +} diff --git a/src/tests/test.rs b/src/tests/test.rs index f721f2e..e83c6bb 100644 --- a/src/tests/test.rs +++ b/src/tests/test.rs @@ -1,10 +1,18 @@ -use crate::rocket; +use crate::database::DatabaseType; +use crate::{rocket, DEFAULT_SQLITE_TABLE}; +#[cfg(feature = "mysql")] +use crate::DEFAULT_MYSQL_TABLE; +#[cfg(feature = "postgres")] +use crate::DEFAULT_POSTGRES_TABLE; use crate::{Cli, Fileconfig, getprivatekey}; use clap::Parser; use config_file::FromConfigFile; +use mockall::*; use ring::{rand, signature}; +use rocket::async_trait; use std::{fs, path::Path}; use zeroize::Zeroize; +use crate::Database; #[test] fn testresponse() { use ring::rand::SecureRandom; @@ -46,7 +54,7 @@ fn testresponse() { } #[test] #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: KeyRejected(\"InvalidEncoding\")" + expected = "Error creating KeyPair from PEM." )] fn checkconfigfake() { let cli = Cli::parse(); @@ -54,13 +62,13 @@ fn checkconfigfake() { let config_path = &cli.config_path; if !Path::new(config_path).exists() { - panic!("Config file not found at: {}", config_path); + panic!("Config file not found at: {}", config_path.display()); } let mut config = match Fileconfig::from_config_file(config_path) { Ok(config) => config, Err(e) => { - panic!("Error reading config file at {}: {}", config_path, e); + panic!("Error reading config file at {}: {}", config_path.display(), e); } }; config.itkey = String::from("test_files/key.pem"); @@ -74,3 +82,87 @@ fn checkconfigfake() { fn checkconfig() { rocket(); } +mock! { + pub Database {} + + #[async_trait] + impl Database for Database { + async fn check_cert( + &self, + certnum: &str, + revoked: bool, + ) -> Result>; + + fn create_tables_if_needed(&self) -> Result<(), Box>; + + async fn add_certificate(&self, cert_num: &str) -> Result<(), Box>; + + async fn revoke_certificate( + &self, + cert_num: &str, + revocation_time: chrono::NaiveDateTime, + reason: &str, + ) -> Result<(), Box>; + + async fn get_certificate_status(&self, cert_num: &str) -> Result>; + + async fn list_certificates( + &self, + status: Option, + ) -> Result, Box>; + } +} + +#[test] +fn test_database_type_from_string() { + assert!(matches!( + DatabaseType::from_string("mysql"), + DatabaseType::MySQL + )); + assert!(matches!( + DatabaseType::from_string("MySQL"), + DatabaseType::MySQL + )); + assert!(matches!( + DatabaseType::from_string("postgresql"), + DatabaseType::PostgreSQL + )); + assert!(matches!( + DatabaseType::from_string("postgres"), + DatabaseType::PostgreSQL + )); + assert!(matches!( + DatabaseType::from_string("PostgreSQL"), + DatabaseType::PostgreSQL + )); + assert!(matches!( + DatabaseType::from_string("sqlite"), + DatabaseType::SQLite + )); + assert!(matches!( + DatabaseType::from_string("SQLite"), + DatabaseType::SQLite + )); + assert!(matches!( + DatabaseType::from_string("unknown"), + DatabaseType::SQLite + )); +} + +#[test] +fn test_default_table_names() { + #[cfg(feature="mysql")] + assert_eq!( + DatabaseType::MySQL.default_table_name(), + DEFAULT_MYSQL_TABLE + ); + #[cfg(feature="postgres")] + assert_eq!( + DatabaseType::PostgreSQL.default_table_name(), + DEFAULT_POSTGRES_TABLE + ); + assert_eq!( + DatabaseType::SQLite.default_table_name(), + DEFAULT_SQLITE_TABLE + ); +}