acme-micro-0.14.0/.cargo_vcs_info.json0000644000000001361046102023000131540ustar { "git": { "sha1": "6bc440169db57ff29a2de04c95a41ccdfe9bf887" }, "path_in_vcs": "" }acme-micro-0.14.0/.github/workflows/rust.yml000064400000000000000000000004731046102023000170430ustar 00000000000000name: Rust on: push: branches: [ master ] pull_request: branches: [ master ] env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose acme-micro-0.14.0/.gitignore000064400000000000000000000000371046102023000137120ustar 00000000000000/target **/*.rs.bk *.key *.crt acme-micro-0.14.0/Cargo.lock0000644000000536761046102023000111500ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "acme-micro" version = "0.14.0" dependencies = [ "anyhow", "base64", "chrono", "doc-comment", "env_logger", "log", "openssl", "regex", "serde", "serde_json", "tiny_http", "ureq", ] [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "ascii" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-link", ] [[package]] name = "chunked_transfer" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "doc-comment" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" [[package]] name = "env_filter" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", ] [[package]] name = "env_logger" version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "env_filter", "log", ] [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "futures-core" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-task" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-task", "pin-project-lite", "slab", ] [[package]] name = "getrandom" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "http" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", "itoa", ] [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "iana-time-zone" version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ "cfg-if", "futures-util", "once_cell", "wasm-bindgen", ] [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openssl" version = "0.10.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" 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.6.0+3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" dependencies = [ "cc", ] [[package]] name = "openssl-sys" version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", "openssl-src", "pkg-config", "vcpkg", ] [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", "getrandom", "libc", "untrusted", "windows-sys", ] [[package]] name = "rustls" version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "log", "once_cell", "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pki-types" version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", "serde", "serde_core", "zmij", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tiny_http" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" dependencies = [ "ascii", "chunked_transfer", "httpdate", "log", ] [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ "base64", "flate2", "log", "percent-encoding", "rustls", "rustls-pki-types", "ureq-proto", "utf8-zero", "webpki-roots", ] [[package]] name = "ureq-proto" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" dependencies = [ "base64", "http", "httparse", "log", ] [[package]] name = "utf8-zero" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] [[package]] name = "webpki-roots" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] [[package]] name = "windows-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" acme-micro-0.14.0/Cargo.toml0000644000000031501046102023000111510ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "acme-micro" version = "0.14.0" authors = [ "Martin Algesten ", "kpcyrd ", ] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Library for requesting certificates from an ACME provider (acme-lib fork)." readme = "README.md" keywords = [ "letsencrypt", "acme", ] categories = [ "web-programming", "api-bindings", ] license = "MIT" repository = "https://github.com/kpcyrd/acme-micro" [features] vendored = ["openssl/vendored"] [lib] name = "acme_micro" path = "src/lib.rs" [dependencies.anyhow] version = "1.0" [dependencies.base64] version = "0.22" [dependencies.chrono] version = "0.4" [dependencies.log] version = "0.4" [dependencies.openssl] version = "0.10" [dependencies.serde] version = "1" features = ["derive"] [dependencies.serde_json] version = "1" [dependencies.ureq] version = "3" [dev-dependencies.doc-comment] version = "0.3" [dev-dependencies.env_logger] version = "0.11" default-features = false [dev-dependencies.regex] version = "1.4" [dev-dependencies.tiny_http] version = "0.12" acme-micro-0.14.0/Cargo.toml.orig000064400000000000000000000013701046102023000146120ustar 00000000000000[package] name = "acme-micro" description = "Library for requesting certificates from an ACME provider (acme-lib fork)." license = "MIT" repository = "https://github.com/kpcyrd/acme-micro" readme = "README.md" version = "0.14.0" authors = [ "Martin Algesten ", "kpcyrd ", ] keywords = ["letsencrypt", "acme"] categories = ["web-programming", "api-bindings"] edition = "2021" [features] vendored = ["openssl/vendored"] [dependencies] anyhow = "1.0" base64 = "0.22" chrono = "0.4" log = "0.4" openssl = "0.10" serde = { version = "1", features = ["derive"] } serde_json = "1" ureq = "3" [dev-dependencies] doc-comment = "0.3" env_logger = { version = "0.11", default-features = false } regex = "1.4" tiny_http = "0.12" acme-micro-0.14.0/LICENSE.txt000064400000000000000000000020661046102023000135510ustar 00000000000000## License (MIT) Copyright (c) 2020 Martin Algesten Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. acme-micro-0.14.0/README.md000064400000000000000000000125071046102023000132060ustar 00000000000000# acme-micro acme-micro is a fork of [acme-lib](https://github.com/algesten/acme-lib) and allows accessing ACME (Automatic Certificate Management Environment) services such as [Let's Encrypt](https://letsencrypt.org/). Uses ACME v2 to issue/renew certificates. ## Example ```rust use acme_micro::{Error, Certificate, Directory, DirectoryUrl}; use acme_micro::create_p384_key; use std::time::Duration; fn request_cert() -> Result { // Use DirectoryUrl::LetsEncrypStaging for dev/testing. let url = DirectoryUrl::LetsEncrypt; // Create a directory entrypoint. let dir = Directory::from_url(url)?; // Your contact addresses, note the `mailto:` let contact = vec!["mailto:foo@bar.com".to_string()]; // Generate a private key and register an account with your ACME provider. // You should write it to disk any use `load_account` afterwards. let acc = dir.register_account(contact.clone())?; // Example of how to load an account from string: let privkey = acc.acme_private_key_pem()?; let acc = dir.load_account(&privkey, contact)?; // Order a new TLS certificate for a domain. let mut ord_new = acc.new_order("mydomain.io", &[])?; // If the ownership of the domain(s) have already been // authorized in a previous order, you might be able to // skip validation. The ACME API provider decides. let ord_csr = loop { // are we done? if let Some(ord_csr) = ord_new.confirm_validations() { break ord_csr; } // Get the possible authorizations (for a single domain // this will only be one element). let auths = ord_new.authorizations()?; // For HTTP, the challenge is a text file that needs to // be placed in your web server's root: // // /var/www/.well-known/acme-challenge/ // // The important thing is that it's accessible over the // web for the domain(s) you are trying to get a // certificate for: // // http://mydomain.io/.well-known/acme-challenge/ let chall = auths[0].http_challenge().unwrap(); // The token is the filename. let token = chall.http_token(); let path = format!(".well-known/acme-challenge/{}", token); // The proof is the contents of the file let proof = chall.http_proof()?; // Here you must do "something" to place // the file/contents in the correct place. // update_my_web_server(&path, &proof); // After the file is accessible from the web, the calls // this to tell the ACME API to start checking the // existence of the proof. // // The order at ACME will change status to either // confirm ownership of the domain, or fail due to the // not finding the proof. To see the change, we poll // the API with 5000 milliseconds wait between. chall.validate(Duration::from_millis(5000))?; // Update the state against the ACME API. ord_new.refresh()?; }; // Ownership is proven. Create a private key for // the certificate. These are provided for convenience, you // can provide your own keypair instead if you want. let pkey_pri = create_p384_key()?; // Submit the CSR. This causes the ACME provider to enter a // state of "processing" that must be polled until the // certificate is either issued or rejected. Again we poll // for the status change. let ord_cert = ord_csr.finalize_pkey(pkey_pri, Duration::from_millis(5000))?; // Finally download the certificate. let cert = ord_cert.download_cert()?; println!("{:?}", cert); Ok(cert) } ``` ### Domain ownership Most website TLS certificates tries to prove ownership/control over the domain they are issued for. For ACME, this means proving you control either a web server answering HTTP requests to the domain, or the DNS server answering name lookups against the domain. To use this library, there are points in the flow where you would need to modify either the web server or DNS server before progressing to get the certificate. See [`http_challenge`] and [`dns_challenge`]. #### Multiple domains When creating a new order, it's possible to provide multiple alt-names that will also be part of the certificate. The ACME API requires you to prove ownership of each such domain. See [`authorizations`]. [`http_challenge`]: order/struct.Auth.html#method.http_challenge [`dns_challenge`]: order/struct.Auth.html#method.dns_challenge [`authorizations`]: order/struct.NewOrder.html#method.authorizations ### Rate limits The ACME API provider Let's Encrypt uses [rate limits] to ensure the API i not being abused. It might be tempting to put the `delay_millis` really low in some of this libraries' polling calls, but balance this against the real risk of having access cut off. [rate limits]: https://letsencrypt.org/docs/rate-limits/ #### Use staging for dev! Especially take care to use the Let`s Encrypt staging environment for development where the rate limits are more relaxed. See [`DirectoryUrl::LetsEncryptStaging`]. [`DirectoryUrl::LetsEncryptStaging`]: enum.DirectoryUrl.html#variant.LetsEncryptStaging ### Implementation details The library tries to pull in as few dependencies as possible. (For now) that means using synchronous I/O and blocking cals. This doesn't rule out a futures based version later. It is written by following the [ACME draft spec 18](https://tools.ietf.org/html/draft-ietf-acme-acme-18), and relies heavily on the [openssl](https://docs.rs/openssl/) crate to make JWK/JWT and sign requests to the API. License: MIT acme-micro-0.14.0/src/acc/akey.rs000064400000000000000000000024061046102023000145400ustar 00000000000000use crate::cert::ec_group_p256; use crate::error::*; use openssl::ec::EcKey; use openssl::pkey; #[derive(Clone, Debug)] pub(crate) struct AcmeKey { private_key: EcKey, /// set once we contacted the ACME API to figure out the key id key_id: Option, } impl AcmeKey { pub(crate) fn new() -> Result { let pri_key = EcKey::generate(ec_group_p256()).context("EcKey")?; Ok(Self::from_key(pri_key)) } pub(crate) fn from_pem(pem: &[u8]) -> Result { let pri_key = EcKey::private_key_from_pem(pem).context("Failed to read PEM")?; Ok(Self::from_key(pri_key)) } fn from_key(private_key: EcKey) -> AcmeKey { AcmeKey { private_key, key_id: None, } } pub(crate) fn to_pem(&self) -> Result> { let pem = self .private_key .private_key_to_pem() .context("private_key_to_pem")?; Ok(pem) } pub(crate) fn private_key(&self) -> &EcKey { &self.private_key } pub(crate) fn key_id(&self) -> &str { self.key_id.as_ref().unwrap() } pub(crate) fn set_key_id(&mut self, kid: String) { self.key_id = Some(kid) } } acme-micro-0.14.0/src/acc/mod.rs000064400000000000000000000112611046102023000143650ustar 00000000000000// use std::sync::Arc; use crate::api::{ApiAccount, ApiDirectory, ApiIdentifier, ApiOrder, ApiRevocation}; use crate::cert::Certificate; use crate::error::*; use crate::order::{NewOrder, Order}; use crate::req::req_expect_header; use crate::trans::Transport; use crate::util::{base64url, read_json}; mod akey; pub(crate) use self::akey::AcmeKey; #[derive(Clone, Debug)] pub(crate) struct AccountInner { pub transport: Transport, pub api_account: ApiAccount, pub api_directory: ApiDirectory, } /// Account with an ACME provider. /// /// Accounts are created using [`Directory::account`] and consist of a contact /// email address and a private key for signing requests to the ACME API. /// /// acme-lib uses elliptic curve P-256 for accessing the account. This /// does not affect which key algorithms that can be used for the /// issued certificates. /// /// The advantage of using elliptic curve cryptography is that the signed /// requests against the ACME lib are kept small and that the public key /// can be derived from the private. /// /// [`Directory::account`]: struct.Directory.html#method.account #[derive(Clone)] pub struct Account { inner: Arc, } impl Account { pub(crate) fn new( transport: Transport, api_account: ApiAccount, api_directory: ApiDirectory, ) -> Self { Account { inner: Arc::new(AccountInner { transport, api_account, api_directory, }), } } /// Private key for this account. /// /// The key is an elliptic curve private key. pub fn acme_private_key_pem(&self) -> Result { let pem = String::from_utf8(self.inner.transport.acme_key().to_pem()?)?; Ok(pem) } /// Create a new order to issue a certificate for this account. /// /// Each order has a required `primary_name` (which will be set as the certificates `CN`) /// and a variable number of `alt_names`. /// /// This library doesn't constrain the number of `alt_names`, but it is limited by the ACME /// API provider. Let's Encrypt sets a max of [100 names] per certificate. /// /// Every call creates a new order with the ACME API provider, even when the domain /// names supplied are exactly the same. /// /// [100 names]: https://letsencrypt.org/docs/rate-limits/ pub fn new_order(&self, primary_name: &str, alt_names: &[&str]) -> Result { // construct the identifiers let prim_arr = [primary_name]; let domains = prim_arr.iter().chain(alt_names); let order = ApiOrder { identifiers: domains .map(|s| ApiIdentifier { _type: "dns".into(), value: s.to_string(), }) .collect(), ..Default::default() }; let new_order_url = &self.inner.api_directory.newOrder; let res = self.inner.transport.call(new_order_url, &order)?; let order_url = req_expect_header(&res, "location")?; let api_order: ApiOrder = read_json(res)?; let order = Order::new(&self.inner, api_order, order_url); Ok(NewOrder { order }) } /// Revoke a certificate for the reason given. pub fn revoke_certificate(&self, cert: &Certificate, reason: RevocationReason) -> Result<()> { // convert to base64url of the DER (which is not PEM). let certificate = base64url(&cert.certificate_der()?); let revoc = ApiRevocation { certificate, reason: reason as usize, }; let url = &self.inner.api_directory.revokeCert; self.inner.transport.call(url, &revoc)?; Ok(()) } /// Access the underlying JSON object for debugging. pub fn api_account(&self) -> &ApiAccount { &self.inner.api_account } } /// Enumeration of reasons for revocation. /// /// The reason codes are taken from [rfc5280](https://tools.ietf.org/html/rfc5280#section-5.3.1). pub enum RevocationReason { Unspecified = 0, KeyCompromise = 1, CACompromise = 2, AffiliationChanged = 3, Superseded = 4, CessationOfOperation = 5, CertificateHold = 6, // value 7 is not used RemoveFromCRL = 8, PrivilegeWithdrawn = 9, AACompromise = 10, } #[cfg(test)] mod test { use crate::*; #[test] fn test_create_order() -> Result<()> { let server = crate::test::with_directory_server(); let url = DirectoryUrl::Other(&server.dir_url); let dir = Directory::from_url(url)?; let acc = dir.register_account(vec!["mailto:foo@bar.com".to_string()])?; let _ = acc.new_order("acmetest.example.com", &[])?; Ok(()) } } acme-micro-0.14.0/src/api.rs000064400000000000000000000255701046102023000136410ustar 00000000000000//! Low level API JSON objects. //! //! Unstable and not to be used directly. Provided to aid debugging. #![allow(non_snake_case)] #![allow(non_camel_case_types)] use crate::error::*; use serde::{ ser::{SerializeMap, Serializer}, Deserialize, Serialize, }; /// Serializes to `""` pub struct ApiEmptyString; impl Serialize for ApiEmptyString { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str("") } } /// Serializes to `{}` pub struct ApiEmptyObject; impl Serialize for ApiEmptyObject { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let m = serializer.serialize_map(Some(0))?; m.end() } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct ApiProblem { #[serde(rename = "type")] pub _type: String, #[serde(skip_serializing_if = "Option::is_none")] pub detail: Option, #[serde(skip_serializing_if = "Option::is_none")] pub subproblems: Option>, } impl ApiProblem { pub fn is_bad_nonce(&self) -> bool { self._type == "urn:ietf:params:acme:error:badNonce" } pub fn is_jwt_verification_error(&self) -> bool { (self._type == "urn:acme:error:malformed" || self._type == "urn:ietf:params:acme:error:malformed") && self .detail .as_ref() .map(|s| s == "JWS verification error") .unwrap_or(false) } } impl std::fmt::Display for ApiProblem { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { if let Some(detail) = &self.detail { write!(f, "{}: {}", self._type, detail) } else { write!(f, "{}", self._type) } } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct ApiSubproblem { #[serde(rename = "type")] pub _type: String, pub detail: Option, pub identifier: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct ApiDirectory { pub newNonce: String, pub newAccount: String, pub newOrder: String, #[serde(skip_serializing_if = "Option::is_none")] pub newAuthz: Option, pub revokeCert: String, pub keyChange: String, #[serde(skip_serializing_if = "Option::is_none")] pub meta: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct ApiDirectoryMeta { #[serde(skip_serializing_if = "Option::is_none")] pub termsOfService: Option, #[serde(skip_serializing_if = "Option::is_none")] pub website: Option, #[serde(skip_serializing_if = "Option::is_none")] pub caaIdentities: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub externalAccountRequired: Option, } impl ApiDirectoryMeta { pub fn externalAccountRequired(&self) -> bool { self.externalAccountRequired.unwrap_or(false) } } // { // "status": "valid", // "contact": [ // "mailto:cert-admin@example.com", // "mailto:admin@example.com" // ], // "termsOfServiceAgreed": true, // "orders": "https://example.com/acme/acct/evOfKhNU60wg/orders" // } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct ApiAccount { #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, #[serde(default)] pub contact: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub termsOfServiceAgreed: Option, #[serde(skip_serializing_if = "Option::is_none")] pub orders: Option, } impl ApiAccount { pub fn is_status_valid(&self) -> bool { self.status.as_ref().map(|s| s.as_ref()) == Some("valid") } pub fn is_status_deactivated(&self) -> bool { self.status.as_ref().map(|s| s.as_ref()) == Some("deactivated") } pub fn is_status_revoked(&self) -> bool { self.status.as_ref().map(|s| s.as_ref()) == Some("revoked") } pub fn termsOfServiceAgreed(&self) -> bool { self.termsOfServiceAgreed.unwrap_or(false) } } // { // "status": "pending", // "expires": "2019-01-09T08:26:43.570360537Z", // "identifiers": [ // { // "type": "dns", // "value": "acmetest.algesten.se" // } // ], // "authorizations": [ // "https://example.com/acme/authz/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs" // ], // "finalize": "https://example.com/acme/finalize/7738992/18234324" // } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct ApiOrder { #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, #[serde(skip_serializing_if = "Option::is_none")] pub expires: Option, pub identifiers: Vec, pub notBefore: Option, pub notAfter: Option, pub error: Option, pub authorizations: Option>, pub finalize: String, pub certificate: Option, } impl ApiOrder { /// As long as there are outstanding authorizations. pub fn is_status_pending(&self) -> bool { self.status.as_ref().map(|s| s.as_ref()) == Some("pending") } /// When all authorizations are finished, and we need to call /// "finalize". pub fn is_status_ready(&self) -> bool { self.status.as_ref().map(|s| s.as_ref()) == Some("ready") } /// On "finalize" the server is processing to sign CSR. pub fn is_status_processing(&self) -> bool { self.status.as_ref().map(|s| s.as_ref()) == Some("processing") } /// Once the certificate is issued and can be downloaded. pub fn is_status_valid(&self) -> bool { self.status.as_ref().map(|s| s.as_ref()) == Some("valid") } /// If the order failed and can't be used again. pub fn is_status_invalid(&self) -> bool { self.status.as_ref().map(|s| s.as_ref()) == Some("invalid") } /// Return all domains pub fn domains(&self) -> Vec<&str> { self.identifiers.iter().map(|i| i.value.as_ref()).collect() } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ApiIdentifier { #[serde(rename = "type")] pub _type: String, pub value: String, } impl ApiIdentifier { pub fn is_type_dns(&self) -> bool { self._type == "dns" } } // { // "identifier": { // "type": "dns", // "value": "acmetest.algesten.se" // }, // "status": "pending", // "expires": "2019-01-09T08:26:43Z", // "challenges": [ // { // "type": "http-01", // "status": "pending", // "url": "https://example.com/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789597", // "token": "MUi-gqeOJdRkSb_YR2eaMxQBqf6al8dgt_dOttSWb0w" // }, // { // "type": "tls-alpn-01", // "status": "pending", // "url": "https://example.com/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789598", // "token": "WCdRWkCy4THTD_j5IH4ISAzr59lFIg5wzYmKxuOJ1lU" // }, // { // "type": "dns-01", // "status": "pending", // "url": "https://example.com/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789599", // "token": "RRo2ZcXAEqxKvMH8RGcATjSK1KknLEUmauwfQ5i3gG8" // } // ] // } // on incorrect challenge, something like: // // "challenges": [ // { // "type": "dns-01", // "status": "invalid", // "error": { // "type": "urn:ietf:params:acme:error:dns", // "detail": "DNS problem: NXDOMAIN looking up TXT for _acme-challenge.martintest.foobar.com", // "status": 400 // }, // "url": "https://example.com/acme/challenge/afyChhlFB8GLLmIqEnqqcXzX0Ss3GBw6oUlKAGDG6lY/221695600", // "token": "YsNqBWZnyYjDun3aUC2CkCopOaqZRrI5hp3tUjxPLQU" // }, // "Incorrect TXT record \"caOh44dp9eqXNRkd0sYrKVF8dBl0L8h8-kFpIBje-2c\" found at _acme-challenge.martintest.foobar.com #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ApiAuth { pub identifier: ApiIdentifier, pub status: Option, pub expires: Option, pub challenges: Vec, pub wildcard: Option, } impl ApiAuth { pub fn is_status_pending(&self) -> bool { self.status.as_ref().map(|s| s.as_ref()) == Some("pending") } pub fn is_status_valid(&self) -> bool { self.status.as_ref().map(|s| s.as_ref()) == Some("valid") } pub fn is_status_invalid(&self) -> bool { self.status.as_ref().map(|s| s.as_ref()) == Some("invalid") } pub fn is_status_deactivated(&self) -> bool { self.status.as_ref().map(|s| s.as_ref()) == Some("deactivated") } pub fn is_status_expired(&self) -> bool { self.status.as_ref().map(|s| s.as_ref()) == Some("expired") } pub fn is_status_revoked(&self) -> bool { self.status.as_ref().map(|s| s.as_ref()) == Some("revoked") } pub fn wildcard(&self) -> bool { self.wildcard.unwrap_or(false) } pub fn http_challenge(&self) -> Option<&ApiChallenge> { self.challenges.iter().find(|c| c._type == "http-01") } pub fn dns_challenge(&self) -> Option<&ApiChallenge> { self.challenges.iter().find(|c| c._type == "dns-01") } pub fn tls_alpn_challenge(&self) -> Option<&ApiChallenge> { self.challenges.iter().find(|c| c._type == "tls-alpn-01") } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ApiChallenge { pub url: String, #[serde(rename = "type")] pub _type: String, pub status: String, pub token: String, pub validated: Option, pub error: Option, } // { // "type": "http-01", // "status": "pending", // "url": "https://acme-staging-v02.api.letsencrypt.org/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789597", // "token": "MUi-gqeOJdRkSb_YR2eaMxQBqf6al8dgt_dOttSWb0w" // } impl ApiChallenge { pub fn is_status_pending(&self) -> bool { &self.status == "pending" } pub fn is_status_processing(&self) -> bool { &self.status == "processing" } pub fn is_status_valid(&self) -> bool { &self.status == "valid" } pub fn is_status_invalid(&self) -> bool { &self.status == "invalid" } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ApiFinalize { pub csr: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ApiRevocation { pub certificate: String, pub reason: usize, } #[cfg(test)] mod test { use super::*; #[test] fn test_api_empty_string() { let x = serde_json::to_string(&ApiEmptyString).unwrap(); assert_eq!("\"\"", x); } #[test] fn test_api_empty_object() { let x = serde_json::to_string(&ApiEmptyObject).unwrap(); assert_eq!("{}", x); } } acme-micro-0.14.0/src/cert.rs000064400000000000000000000124531046102023000140210ustar 00000000000000use crate::error::*; use chrono::NaiveDateTime; use openssl::{ ec::{Asn1Flag, EcGroup, EcKey}, hash::MessageDigest, nid::Nid, pkey::{self, PKey}, rsa::Rsa, stack::Stack, x509::{extension::SubjectAlternativeName, X509Req, X509ReqBuilder, X509}, }; use std::sync::OnceLock; pub(crate) fn ec_group_p256() -> &'static EcGroup { static EC_GROUP_P256: OnceLock = OnceLock::new(); EC_GROUP_P256.get_or_init(|| ec_group(Nid::X9_62_PRIME256V1)) } pub(crate) fn ec_group_p384() -> &'static EcGroup { static EC_GROUP_P384: OnceLock = OnceLock::new(); EC_GROUP_P384.get_or_init(|| ec_group(Nid::SECP384R1)) } fn ec_group(nid: Nid) -> EcGroup { let mut g = EcGroup::from_curve_name(nid).expect("EcGroup"); // this is required for openssl 1.0.x (but not 1.1.x) g.set_asn1_flag(Asn1Flag::NAMED_CURVE); g } /// Make an RSA private key (from which we can derive a public key). /// /// This library does not check the number of bits used to create the key pair. /// For Let's Encrypt, the bits must be between 2048 and 4096. pub fn create_rsa_key(bits: u32) -> Result> { let pri_key_rsa = Rsa::generate(bits)?; let pkey = PKey::from_rsa(pri_key_rsa)?; Ok(pkey) } /// Make a P-256 private key (from which we can derive a public key). pub fn create_p256_key() -> Result> { let pri_key_ec = EcKey::generate(ec_group_p256())?; let pkey = PKey::from_ec_key(pri_key_ec)?; Ok(pkey) } /// Make a P-384 private key pair (from which we can derive a public key). pub fn create_p384_key() -> Result> { let pri_key_ec = EcKey::generate(ec_group_p384())?; let pkey = PKey::from_ec_key(pri_key_ec)?; Ok(pkey) } pub(crate) fn create_csr(pkey: &PKey, domains: &[&str]) -> Result { // // the csr builder let mut req_bld = X509ReqBuilder::new()?; // set private/public key in builder req_bld.set_pubkey(pkey)?; // set all domains as alt names let mut stack = Stack::new()?; let ctx = req_bld.x509v3_context(None); let mut an = SubjectAlternativeName::new(); for d in domains { an.dns(d); } let ext = an.build(&ctx).context("SubjectAlternativeName::build")?; stack.push(ext).expect("Stack::push"); req_bld.add_extensions(&stack)?; // sign it req_bld.sign(pkey, MessageDigest::sha256())?; // the csr Ok(req_bld.build()) } /// Encapsulated certificate and private key. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Certificate { private_key: String, certificate: String, } impl Certificate { pub(crate) fn new(private_key: String, certificate: String) -> Self { Certificate { private_key, certificate, } } pub fn parse(private_key: String, certificate: String) -> Result { // validate certificate X509::from_pem(certificate.as_bytes())?; // validate private key PKey::private_key_from_pem(private_key.as_bytes())?; Ok(Certificate { private_key, certificate, }) } /// The PEM encoded private key. pub fn private_key(&self) -> &str { &self.private_key } /// The private key as DER. pub fn private_key_der(&self) -> Result> { let pkey = PKey::private_key_from_pem(self.private_key.as_bytes())?; let der = pkey.private_key_to_der()?; Ok(der) } /// The PEM encoded issued certificate. pub fn certificate(&self) -> &str { &self.certificate } /// The issued certificate as DER. pub fn certificate_der(&self) -> Result> { let x509 = X509::from_pem(self.certificate.as_bytes())?; let der = x509.to_der()?; Ok(der) } /// Inspect the certificate to count the number of (whole) valid days left. /// /// It's up to the ACME API provider to decide how long an issued certificate is valid. /// Let's Encrypt sets the validity to 90 days. This function reports 89 days for newly /// issued cert, since it counts _whole_ days. /// /// It is possible to get negative days for an expired certificate. pub fn valid_days_left(&self) -> Result { // the cert used in the tests is not valid to load as x509 if cfg!(test) { return Ok(89); } // load as x509 let x509 = X509::from_pem(self.certificate.as_bytes())?; // convert asn1 time to expiry date let not_after = format!("{}", x509.not_after()); // Display trait produces this format: "Apr 19 08:48:46 2019 GMT" let expires = parse_date(¬_after)?; let now = chrono::Utc::now(); let dur = expires.signed_duration_since(now); Ok(dur.num_days()) } } fn parse_date(s: &str) -> Result> { debug!("Parse date/time: {}", s); // OpenSSL formats ASN1_TIME as "May 3 07:40:15 2019 GMT" let dt = NaiveDateTime::parse_from_str(s, "%b %d %H:%M:%S %Y %Z")?; Ok(dt.and_utc()) } #[cfg(test)] mod test { use super::*; #[test] fn test_parse_date() { let x = parse_date("May 3 07:40:15 2019 GMT") .context("input date parsing failed") .unwrap(); assert_eq!(x.format("%F %T").to_string(), "2019-05-03 07:40:15"); } } acme-micro-0.14.0/src/dir.rs000064400000000000000000000122401046102023000136340ustar 00000000000000// use std::sync::Arc; use crate::{ acc::AcmeKey, api::{ApiAccount, ApiDirectory}, error::*, req::{req_expect_header, req_get, req_handle_error}, trans::{NoncePool, Transport}, util::read_json, Account, }; const LETSENCRYPT: &str = "https://acme-v02.api.letsencrypt.org/directory"; const LETSENCRYPT_STAGING: &str = "https://acme-staging-v02.api.letsencrypt.org/directory"; /// Enumeration of known ACME API directories. #[derive(Debug, Clone)] pub enum DirectoryUrl<'a> { /// The main Let's Encrypt directory. Not appropriate for testing and dev. LetsEncrypt, /// The staging Let's Encrypt directory. Use for testing and dev. Doesn't issue /// "valid" certificates. The root signing certificate is not supposed /// to be in any trust chains. LetsEncryptStaging, /// Provide an arbitrary director URL to connect to. Other(&'a str), } impl<'a> DirectoryUrl<'a> { fn to_url(&self) -> &str { match self { DirectoryUrl::LetsEncrypt => LETSENCRYPT, DirectoryUrl::LetsEncryptStaging => LETSENCRYPT_STAGING, DirectoryUrl::Other(s) => s, } } } /// Entry point for accessing an ACME API. #[derive(Clone)] pub struct Directory { nonce_pool: Arc, api_directory: ApiDirectory, } impl Directory { /// Create a directory over a persistence implementation and directory url. pub fn from_url(url: DirectoryUrl) -> Result { let dir_url = url.to_url(); let res = req_handle_error(req_get(dir_url))?; let api_directory: ApiDirectory = read_json(res)?; let nonce_pool = Arc::new(NoncePool::new(&api_directory.newNonce)); Ok(Directory { nonce_pool, api_directory, }) } pub fn register_account(&self, contact: Vec) -> Result { let acme_key = AcmeKey::new()?; self.upsert_account(acme_key, contact) } pub fn load_account(&self, pem: &str, contact: Vec) -> Result { let acme_key = AcmeKey::from_pem(pem.as_bytes())?; self.upsert_account(acme_key, contact) } fn upsert_account(&self, acme_key: AcmeKey, contact: Vec) -> Result { // Prepare making a call to newAccount. This is fine to do both for // new keys and existing. For existing the spec says to return a 200 // with the Location header set to the key id (kid). let acc = ApiAccount { contact, termsOfServiceAgreed: Some(true), ..Default::default() }; let mut transport = Transport::new(&self.nonce_pool, acme_key); let res = transport.call_jwk(&self.api_directory.newAccount, &acc)?; let kid = req_expect_header(&res, "location")?; debug!("Key id is: {}", kid); let api_account: ApiAccount = read_json(res)?; // fill in the server returned key id transport.set_key_id(kid); // The finished account Ok(Account::new( transport, api_account, self.api_directory.clone(), )) } /// Access the underlying JSON object for debugging. pub fn api_directory(&self) -> &ApiDirectory { &self.api_directory } } #[cfg(test)] mod test { use super::*; #[test] fn test_create_directory() -> Result<()> { let server = crate::test::with_directory_server(); let url = DirectoryUrl::Other(&server.dir_url); let _ = Directory::from_url(url)?; Ok(()) } #[test] fn test_create_acount() -> Result<()> { let server = crate::test::with_directory_server(); let url = DirectoryUrl::Other(&server.dir_url); let dir = Directory::from_url(url)?; let _ = dir.register_account(vec!["mailto:foo@bar.com".to_string()])?; Ok(()) } // #[test] // fn test_the_whole_hog() -> Result<()> { // std::env::set_var("RUST_LOG", "acme_micro=trace"); // let _ = env_logger::try_init(); // use crate::cert::create_p384_key; // let url = DirectoryUrl::LetsEncryptStaging; // let persist = FilePersist::new("."); // let dir = Directory::from_url(persist, url)?; // let acc = dir.account("foo@bar.com")?; // let mut ord = acc.new_order("myspecialsite.com", &[])?; // let ord = loop { // if let Some(ord) = ord.confirm_validations() { // break ord; // } // let auths = ord.authorizations()?; // let chall = auths[0].dns_challenge(); // info!("Proof: {}", chall.dns_proof()); // use std::thread; // use std::time::Duration; // thread::sleep(Duration::from_millis(60_000)); // chall.validate(5000)?; // ord.refresh()?; // }; // let (pkey_pri, pkey_pub) = create_p384_key(); // let ord = ord.finalize_pkey(pkey_pri, pkey_pub, 5000)?; // let cert = ord.download_and_save_cert()?; // println!( // "{}{}{}", // cert.private_key(), // cert.certificate(), // cert.valid_days_left() // ); // Ok(()) // } } acme-micro-0.14.0/src/error.rs000064400000000000000000000003331046102023000142070ustar 00000000000000use crate::api::ApiProblem; pub use anyhow::{anyhow, bail, Context, Error, Result}; pub use log::{debug, trace}; impl From for Error { fn from(x: ApiProblem) -> Error { anyhow!("{}", x) } } acme-micro-0.14.0/src/jwt.rs000064400000000000000000000050601046102023000136640ustar 00000000000000use crate::acc::AcmeKey; use crate::cert::ec_group_p256; use crate::util::base64url; use crate::Result; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; #[derive(Debug, Serialize, Deserialize, Default)] pub(crate) struct JwsProtected { alg: String, url: String, nonce: String, #[serde(skip_serializing_if = "Option::is_none")] jwk: Option, #[serde(skip_serializing_if = "Option::is_none")] kid: Option, } impl JwsProtected { pub(crate) fn new_jwk(jwk: Jwk, url: &str, nonce: String) -> Self { JwsProtected { alg: "ES256".into(), url: url.into(), nonce, jwk: Some(jwk), ..Default::default() } } pub(crate) fn new_kid(kid: &str, url: &str, nonce: String) -> Self { JwsProtected { alg: "ES256".into(), url: url.into(), nonce, kid: Some(kid.into()), ..Default::default() } } } #[derive(Debug, Serialize, Deserialize, Clone)] pub(crate) struct Jwk { alg: String, crv: String, kty: String, #[serde(rename = "use")] _use: String, x: String, y: String, } #[derive(Debug, Serialize, Deserialize, Clone)] // LEXICAL ORDER OF FIELDS MATTER! pub(crate) struct JwkThumb { crv: String, kty: String, x: String, y: String, } impl TryFrom<&AcmeKey> for Jwk { type Error = crate::Error; fn try_from(a: &AcmeKey) -> Result { let mut ctx = openssl::bn::BigNumContext::new()?; let mut x = openssl::bn::BigNum::new()?; let mut y = openssl::bn::BigNum::new()?; a.private_key().public_key().affine_coordinates_gfp( ec_group_p256(), &mut x, &mut y, &mut ctx, )?; Ok(Jwk { alg: "ES256".into(), kty: "EC".into(), crv: "P-256".into(), _use: "sig".into(), x: base64url(&x.to_vec()), y: base64url(&y.to_vec()), }) } } impl From<&Jwk> for JwkThumb { fn from(a: &Jwk) -> Self { JwkThumb { crv: a.crv.clone(), kty: a.kty.clone(), x: a.x.clone(), y: a.y.clone(), } } } #[derive(Debug, Serialize, Deserialize)] pub(crate) struct Jws { protected: String, payload: String, signature: String, } impl Jws { pub(crate) fn new(protected: String, payload: String, signature: String) -> Self { Jws { protected, payload, signature, } } } acme-micro-0.14.0/src/lib.rs000064400000000000000000000143761046102023000136400ustar 00000000000000#![warn(clippy::all)] //! acme-lib is a library for accessing ACME (Automatic Certificate Management Environment) //! services such as [Let's Encrypt](https://letsencrypt.org/). //! //! Uses ACME v2 to issue/renew certificates. //! //! # Example //! //! ```no_run //! use acme_micro::{Error, Certificate, Directory, DirectoryUrl}; //! use acme_micro::create_p384_key; //! use std::time::Duration; //! //! fn request_cert() -> Result { //! //! // Use DirectoryUrl::LetsEncrypStaging for dev/testing. //! let url = DirectoryUrl::LetsEncrypt; //! //! // Create a directory entrypoint. //! let dir = Directory::from_url(url)?; //! //! // Your contact addresses, note the `mailto:` //! let contact = vec!["mailto:foo@bar.com".to_string()]; //! //! // Generate a private key and register an account with your ACME provider. //! // You should write it to disk any use `load_account` afterwards. //! let acc = dir.register_account(contact.clone())?; //! //! // Example of how to load an account from string: //! let privkey = acc.acme_private_key_pem()?; //! let acc = dir.load_account(&privkey, contact)?; //! //! // Order a new TLS certificate for a domain. //! let mut ord_new = acc.new_order("mydomain.io", &[])?; //! //! // If the ownership of the domain(s) have already been //! // authorized in a previous order, you might be able to //! // skip validation. The ACME API provider decides. //! let ord_csr = loop { //! // are we done? //! if let Some(ord_csr) = ord_new.confirm_validations() { //! break ord_csr; //! } //! //! // Get the possible authorizations (for a single domain //! // this will only be one element). //! let auths = ord_new.authorizations()?; //! //! // For HTTP, the challenge is a text file that needs to //! // be placed in your web server's root: //! // //! // /var/www/.well-known/acme-challenge/ //! // //! // The important thing is that it's accessible over the //! // web for the domain(s) you are trying to get a //! // certificate for: //! // //! // http://mydomain.io/.well-known/acme-challenge/ //! let chall = auths[0].http_challenge().unwrap(); //! //! // The token is the filename. //! let token = chall.http_token(); //! let path = format!(".well-known/acme-challenge/{}", token); //! //! // The proof is the contents of the file //! let proof = chall.http_proof()?; //! //! // Here you must do "something" to place //! // the file/contents in the correct place. //! // update_my_web_server(&path, &proof); //! //! // After the file is accessible from the web, the calls //! // this to tell the ACME API to start checking the //! // existence of the proof. //! // //! // The order at ACME will change status to either //! // confirm ownership of the domain, or fail due to the //! // not finding the proof. To see the change, we poll //! // the API with 5000 milliseconds wait between. //! chall.validate(Duration::from_millis(5000))?; //! //! // Update the state against the ACME API. //! ord_new.refresh()?; //! }; //! //! // Ownership is proven. Create a private key for //! // the certificate. These are provided for convenience, you //! // can provide your own keypair instead if you want. //! let pkey_pri = create_p384_key()?; //! //! // Submit the CSR. This causes the ACME provider to enter a //! // state of "processing" that must be polled until the //! // certificate is either issued or rejected. Again we poll //! // for the status change. //! let ord_cert = //! ord_csr.finalize_pkey(pkey_pri, Duration::from_millis(5000))?; //! //! // Now download the certificate. Also stores the cert in //! // the persistence. //! let cert = ord_cert.download_cert()?; //! println!("{:?}", cert); //! //! Ok(cert) //! } //! ``` //! //! ## Domain ownership //! //! Most website TLS certificates tries to prove ownership/control over the domain they //! are issued for. For ACME, this means proving you control either a web server answering //! HTTP requests to the domain, or the DNS server answering name lookups against the domain. //! //! To use this library, there are points in the flow where you would need to modify either //! the web server or DNS server before progressing to get the certificate. //! //! See [`http_challenge`] and [`dns_challenge`]. //! //! ### Multiple domains //! //! When creating a new order, it's possible to provide multiple alt-names that will also //! be part of the certificate. The ACME API requires you to prove ownership of each such //! domain. See [`authorizations`]. //! //! [`http_challenge`]: order/struct.Auth.html#method.http_challenge //! [`dns_challenge`]: order/struct.Auth.html#method.dns_challenge //! [`authorizations`]: order/struct.NewOrder.html#method.authorizations //! //! ## Rate limits //! //! The ACME API provider Let's Encrypt uses [rate limits] to ensure the API i not being //! abused. It might be tempting to put the `delay` really low in some of this //! libraries' polling calls, but balance this against the real risk of having access //! cut off. //! //! [rate limits]: https://letsencrypt.org/docs/rate-limits/ //! //! ### Use staging for dev! //! //! Especially take care to use the Let`s Encrypt staging environment for development //! where the rate limits are more relaxed. //! //! See [`DirectoryUrl::LetsEncryptStaging`]. //! //! [`DirectoryUrl::LetsEncryptStaging`]: enum.DirectoryUrl.html#variant.LetsEncryptStaging //! //! ## Implementation details //! //! The library tries to pull in as few dependencies as possible. (For now) that means using //! synchronous I/O and blocking cals. This doesn't rule out a futures based version later. //! //! It is written by following the //! [ACME draft spec 18](https://tools.ietf.org/html/draft-ietf-acme-acme-18), and relies //! heavily on the [openssl](https://docs.rs/openssl/) crate to make JWK/JWT and sign requests //! to the API. //! #[cfg(doctest)] doc_comment::doctest!("../README.md"); mod acc; mod cert; mod dir; mod error; mod jwt; mod req; mod trans; mod util; pub mod api; pub mod order; #[cfg(test)] mod test; pub use crate::acc::{Account, RevocationReason}; pub use crate::cert::{create_p256_key, create_p384_key, create_rsa_key, Certificate}; pub use crate::dir::{Directory, DirectoryUrl}; pub use crate::error::{Error, Result}; acme-micro-0.14.0/src/order/auth.rs000064400000000000000000000237041046102023000151410ustar 00000000000000// use openssl::sha::sha256; use std::sync::Arc; use std::thread; use std::{convert::TryInto, time::Duration}; use crate::acc::AccountInner; use crate::acc::AcmeKey; use crate::api::{ApiAuth, ApiChallenge, ApiEmptyObject, ApiEmptyString}; use crate::error::*; use crate::jwt::*; use crate::util::{base64url, read_json}; /// An authorization ([ownership proof]) for a domain name. /// /// Each authorization for an order much be progressed to a valid state before the ACME API /// will issue a certificate. /// /// Authorizations may or may not be required depending on previous orders against the same /// ACME account. The ACME API decides if the authorization is needed. /// /// Currently there are two ways of providing the authorization. /// /// * In a text file served using [HTTP] from a web server of the domain being authorized. /// * A `TXT` [DNS] record under the domain being authorized. /// /// [ownership proof]: ../index.html#domain-ownership /// [HTTP]: #method.http_challenge /// [DNS]: #method.dns_challenge #[derive(Debug)] pub struct Auth { inner: Arc, api_auth: ApiAuth, auth_url: String, } impl Auth { pub(crate) fn new(inner: &Arc, api_auth: ApiAuth, auth_url: &str) -> Self { Auth { inner: inner.clone(), api_auth, auth_url: auth_url.into(), } } /// Domain name for this authorization. pub fn domain_name(&self) -> &str { &self.api_auth.identifier.value } /// Whether we actually need to do the authorization. This might not be needed if we have /// proven ownership of the domain recently in a previous order. pub fn need_challenge(&self) -> bool { !self.api_auth.is_status_valid() } /// Get the http challenge. /// /// The http challenge must be placed so it is accessible under: /// /// ```text /// http:///.well-known/acme-challenge/ /// ``` /// /// The challenge will be accessed over HTTP (not HTTPS), for obvious reasons. /// /// ```no_run /// use acme_micro::order::Auth; /// use acme_micro::Error; /// use std::fs::File; /// use std::io::Write; /// use std::time::Duration; /// /// fn web_authorize(auth: &Auth) -> Result<(), Error> { /// let challenge = auth.http_challenge().unwrap(); /// // Assuming our web server's root is under /var/www /// let path = { /// let token = challenge.http_token(); /// format!("/var/www/.well-known/acme-challenge/{}", token) /// }; /// let mut file = File::create(&path)?; /// file.write_all(challenge.http_proof()?.as_bytes())?; /// challenge.validate(Duration::from_millis(5000))?; /// Ok(()) /// } /// ``` pub fn http_challenge(&self) -> Option> { self.api_auth .http_challenge() .map(|c| Challenge::new(&self.inner, c.clone(), &self.auth_url)) } /// Get the dns challenge. /// /// The dns challenge is a `TXT` record that must put created under: /// /// ```text /// _acme-challenge.. TXT /// ``` /// /// The contains the signed token proving this account update it. /// /// ```no_run /// use acme_micro::order::Auth; /// use acme_micro::Error; /// use std::time::Duration; /// /// fn dns_authorize(auth: &Auth) -> Result<(), Error> { /// let challenge = auth.dns_challenge().unwrap(); /// let record = format!("_acme-challenge.{}.", auth.domain_name()); /// // route_53_set_record(&record, "TXT", challenge.dns_proof()); /// challenge.validate(Duration::from_millis(5000))?; /// Ok(()) /// } /// ``` /// /// The dns proof is not the same as the http proof. pub fn dns_challenge(&self) -> Option> { self.api_auth .dns_challenge() .map(|c| Challenge::new(&self.inner, c.clone(), &self.auth_url)) } /// Get the TLS ALPN challenge. /// /// The TLS ALPN challenge is a certificate that must be served when a /// request is made for the ALPN protocol "tls-alpn-01". The certificate /// must contain a single dNSName SAN containing the domain being /// validated, as well as an ACME extension containing the SHA256 of the /// key authorization. pub fn tls_alpn_challenge(&self) -> Option> { self.api_auth .tls_alpn_challenge() .map(|c| Challenge::new(&self.inner, c.clone(), &self.auth_url)) } /// Access the underlying JSON object for debugging. We don't /// refresh the authorization when the corresponding challenge is validated, /// so there will be no changes to see here. pub fn api_auth(&self) -> &ApiAuth { &self.api_auth } } /// Marker type for http challenges. #[doc(hidden)] pub struct Http; /// Marker type for dns challenges. #[doc(hidden)] pub struct Dns; /// Marker type for tls alpn challenges. #[doc(hidden)] pub struct TlsAlpn; /// A DNS, HTTP, or TLS-ALPN challenge as obtained from the [`Auth`]. /// /// [`Auth`]: struct.Auth.html pub struct Challenge { inner: Arc, api_challenge: ApiChallenge, auth_url: String, _ph: std::marker::PhantomData, } impl Challenge { /// The `token` is a unique identifier of the challenge. It is the file name in the /// http challenge like so: /// /// ```text /// http:///.well-known/acme-challenge/ /// ``` pub fn http_token(&self) -> &str { &self.api_challenge.token } /// The `proof` is some text content that is placed in the file named by `token`. pub fn http_proof(&self) -> Result { let acme_key = self.inner.transport.acme_key(); let proof = key_authorization(&self.api_challenge.token, acme_key, false)?; Ok(proof) } } impl Challenge { /// The `proof` is the `TXT` record placed under: /// /// ```text /// _acme-challenge.. TXT /// ``` pub fn dns_proof(&self) -> Result { let acme_key = self.inner.transport.acme_key(); let proof = key_authorization(&self.api_challenge.token, acme_key, true)?; Ok(proof) } } impl Challenge { /// The `proof` is the contents of the ACME extension to be placed in the /// certificate used for validation. pub fn tls_alpn_proof(&self) -> Result<[u8; 32]> { let acme_key = self.inner.transport.acme_key(); let proof = key_authorization(&self.api_challenge.token, acme_key, false)?; Ok(sha256(proof.as_bytes())) } } impl Challenge { fn new(inner: &Arc, api_challenge: ApiChallenge, auth_url: &str) -> Self { Challenge { inner: inner.clone(), api_challenge, auth_url: auth_url.into(), _ph: std::marker::PhantomData, } } /// Check whether this challlenge really need validation. It might already been /// done in a previous order for the same account. pub fn need_validate(&self) -> bool { self.api_challenge.is_status_pending() } /// Tell the ACME API to attempt validating the proof of this challenge. /// /// The user must first update the DNS record or HTTP web server depending /// on the type challenge being validated. pub fn validate(&self, delay: Duration) -> Result<()> { let url_chall = &self.api_challenge.url; let res = self.inner.transport.call(url_chall, &ApiEmptyObject)?; let _: ApiChallenge = read_json(res)?; let auth = wait_for_auth_status(&self.inner, &self.auth_url, delay)?; if !auth.is_status_valid() { let error = auth .challenges .iter() .filter_map(|c| c.error.as_ref()) .next(); let reason = if let Some(error) = error { format!( "Failed: {}", error.detail.clone().unwrap_or_else(|| error._type.clone()) ) } else { "Validation failed and no error found".into() }; bail!("Validation failed: {:?}", reason); } Ok(()) } /// Access the underlying JSON object for debugging. pub fn api_challenge(&self) -> &ApiChallenge { &self.api_challenge } } fn key_authorization(token: &str, key: &AcmeKey, extra_sha256: bool) -> Result { let jwk: Jwk = key.try_into()?; let jwk_thumb: JwkThumb = (&jwk).into(); let jwk_json = serde_json::to_string(&jwk_thumb)?; let digest = base64url(&sha256(jwk_json.as_bytes())); let key_auth = format!("{}.{}", token, digest); let res = if extra_sha256 { base64url(&sha256(key_auth.as_bytes())) } else { key_auth }; Ok(res) } fn wait_for_auth_status( inner: &Arc, auth_url: &str, delay: Duration, ) -> Result { let auth = loop { let res = inner.transport.call(auth_url, &ApiEmptyString)?; let auth: ApiAuth = read_json(res)?; if !auth.is_status_pending() { break auth; } thread::sleep(delay); }; Ok(auth) } #[cfg(test)] mod test { use crate::*; #[test] fn test_get_challenges() -> Result<()> { let server = crate::test::with_directory_server(); let url = DirectoryUrl::Other(&server.dir_url); let dir = Directory::from_url(url)?; let acc = dir.register_account(vec!["mailto:foo@bar.com".to_string()])?; let ord = acc.new_order("acmetest.example.com", &[])?; let authz = ord.authorizations()?; assert!(authz.len() == 1); let auth = &authz[0]; { let http = auth.http_challenge().unwrap(); assert!(http.need_validate()); } { let dns = auth.dns_challenge().unwrap(); assert!(dns.need_validate()); } Ok(()) } } acme-micro-0.14.0/src/order/mod.rs000064400000000000000000000304241046102023000147540ustar 00000000000000//! Order life cycle. //! //! An order goes through a life cycle of different states that require various actions by //! the user. To ensure the user only use appropriate actions, this library have simple façade //! structs that wraps the actual [`ApiOrder`]. //! //! 1. First prove ownership: //! * [`NewOrder`] -> [`Auth`]* -> [`Challenge`] //! 2. Then submit CSR and download the cert. //! * [`NewOrder`] -> [`CsrOrder`] -> [`CertOrder`] //! //! \* Possibly multiple auths. //! //! [`ApiOrder`]: ../api/struct.ApiOrder.html //! [`NewOrder`]: struct.NewOrder.html //! [`Auth`]: struct.Auth.html //! [`Challenge`]: struct.Challenge.html //! [`CsrOrder`]: struct.CsrOrder.html //! [`CertOrder`]: struct.CertOrder.html use crate::{ acc::AccountInner, api::{ApiAuth, ApiEmptyString, ApiFinalize, ApiOrder}, cert::{create_csr, Certificate}, error::*, util::{base64url, read_json}, }; use openssl::pkey::{self, PKey}; use std::{sync::Arc, thread, time::Duration}; use ureq::{http, Body}; mod auth; pub use self::auth::{Auth, Challenge}; /// The order wrapped with an outer façade. pub(crate) struct Order { inner: Arc, api_order: ApiOrder, url: String, } impl Order { pub(crate) fn new(inner: &Arc, api_order: ApiOrder, url: String) -> Self { Order { inner: inner.clone(), api_order, url, } } } /// Helper to refresh an order status (POST-as-GET). pub(crate) fn refresh_order( inner: &Arc, url: String, want_status: &'static str, ) -> Result { let res = inner.transport.call(&url, &ApiEmptyString)?; // our test rig requires the order to be in `want_status`. // api_order_of is different for test compilation let api_order = api_order_of(res, want_status)?; Ok(Order { inner: inner.clone(), api_order, url, }) } #[cfg(not(test))] fn api_order_of(res: http::Response, _want_status: &str) -> Result { read_json(res) } #[cfg(test)] // our test rig requires the order to be in `want_status` fn api_order_of(mut res: http::Response, want_status: &str) -> Result { let s = res.body_mut().read_to_string().map_err(|e| e.into_io())?; #[allow(clippy::trivial_regex)] let re = regex::Regex::new("").unwrap(); let b = re.replace_all(&s, want_status).to_string(); let api_order: ApiOrder = serde_json::from_str(&b)?; Ok(api_order) } /// A new order created by [`Account::new_order`]. /// /// An order is created using one or many domains (a primary `CN` and possible multiple /// alt names). All domains in the order must have authorizations ([confirmed ownership]) /// before the order can progress to submitting a [CSR]. /// /// This order façade provides calls to provide such authorizations and to progress the order /// when ready. /// /// The ACME API provider might "remember" for a time that you already own a domain, which /// means you might not need to prove the ownership every time. Use appropriate methods to /// first check whether you really need to handle authorizations. /// /// [`Account::new_order`]: ../struct.Account.html#method.new_order /// [confirmed ownership]: ../index.html#domain-ownership /// [CSR]: https://en.wikipedia.org/wiki/Certificate_signing_request pub struct NewOrder { pub(crate) order: Order, } impl NewOrder { /// Tell if the domains in this order have been authorized. /// /// This doesn't do any calls against the API. You must manually call [`refresh`]. /// /// In ACME API terms, the order can either be `ready` or `valid`, which both would /// mean we have passed the authorization stage. /// /// [`refresh`]: struct.NewOrder.html#method.refresh pub fn is_validated(&self) -> bool { self.order.api_order.is_status_ready() || self.order.api_order.is_status_valid() } /// If the order [`is_validated`] progress it to a [`CsrOrder`]. /// /// This doesn't do any calls against the API. You must manually call [`refresh`]. /// /// [`is_validated`]: struct.NewOrder.html#method.is_validated /// [`CsrOrder`]: struct.CsrOrder.html pub fn confirm_validations(&self) -> Option { if self.is_validated() { Some(CsrOrder { order: Order::new( &self.order.inner, self.order.api_order.clone(), self.order.url.clone(), ), }) } else { None } } /// Refresh the order state against the ACME API. /// /// The specification calls this a "POST-as-GET" against the order URL. pub fn refresh(&mut self) -> Result<()> { let order = refresh_order(&self.order.inner, self.order.url.clone(), "ready")?; self.order = order; Ok(()) } /// Provide the authorizations. The number of authorizations will be the same as /// the number of domains requests, i.e. at least one (the primary CN), but possibly /// more (for alt names). /// /// If the order includes new domain names that have not been authorized before, this /// list might contain a mix of already valid and not yet valid auths. pub fn authorizations(&self) -> Result> { let mut result = vec![]; if let Some(authorizations) = &self.order.api_order.authorizations { for auth_url in authorizations { let res = self.order.inner.transport.call(auth_url, &ApiEmptyString)?; let api_auth: ApiAuth = read_json(res)?; result.push(Auth::new(&self.order.inner, api_auth, auth_url)); } } Ok(result) } /// Access the underlying JSON object for debugging. pub fn api_order(&self) -> &ApiOrder { &self.order.api_order } } /// An order that is ready for a [CSR] submission. /// /// To submit the CSR is called "finalizing" the order. /// /// To finalize, the user supplies a private key (from which a public key is derived). This /// library provides [functions to create private keys], but the user can opt for creating them /// in some other way. /// /// This library makes no attempt at validating which key algorithms are used. Unsupported /// algorithms will show as an error when finalizing the order. It is up to the ACME API /// provider to decide which key algorithms to support. /// /// Right now Let's Encrypt [supports]: /// /// * RSA keys from 2048 to 4096 bits in length /// * P-256 and P-384 ECDSA keys /// /// [CSR]: https://en.wikipedia.org/wiki/Certificate_signing_request /// [functions to create key pairs]: ../index.html#functions /// [supports]: https://letsencrypt.org/docs/integration-guide/#supported-key-algorithms pub struct CsrOrder { pub(crate) order: Order, } impl CsrOrder { /// Finalize the order by providing a private key as PEM. /// /// Once the CSR has been submitted, the order goes into a `processing` status, /// where we must poll until the status changes. The `delay` is the /// amount of time to wait between each poll attempt. /// /// This is a convenience wrapper that in turn calls the lower level [`finalize_pkey`]. /// /// [`finalize_pkey`]: struct.CsrOrder.html#method.finalize_pkey pub fn finalize(self, private_key_pem: &str, delay: Duration) -> Result { let pkey_pri = PKey::private_key_from_pem(private_key_pem.as_bytes()) .context("Error reading private key PEM")?; self.finalize_pkey(pkey_pri, delay) } /// Lower level finalize call that works directly with the openssl crate structures. /// /// Creates the CSR for the domains in the order and submit it to the ACME API. /// /// Once the CSR has been submitted, the order goes into a `processing` status, /// where we must poll until the status changes. The `delay` is the /// amount of time to wait between each poll attempt. pub fn finalize_pkey( self, private_key: PKey, delay: Duration, ) -> Result { // // the domains that we have authorized let domains = self.order.api_order.domains(); // csr from private key and authorized domains. let csr = create_csr(&private_key, &domains)?; // this is not the same as PEM. let csr_der = csr.to_der()?; let csr_enc = base64url(&csr_der); let finalize = ApiFinalize { csr: csr_enc }; let inner = self.order.inner; let order_url = self.order.url; let finalize_url = &self.order.api_order.finalize; // if the CSR is invalid, we will get a 4xx code back that // bombs out from this retry_call. inner.transport.call(finalize_url, &finalize)?; // wait for the status to not be processing. // valid -> cert is issued // invalid -> the whole thing is off let order = wait_for_order_status(&inner, &order_url, delay)?; if !order.api_order.is_status_valid() { bail!("Order is in status: {:?}", order.api_order.status); } Ok(CertOrder { private_key, order }) } /// Access the underlying JSON object for debugging. pub fn api_order(&self) -> &ApiOrder { &self.order.api_order } } fn wait_for_order_status(inner: &Arc, url: &str, delay: Duration) -> Result { loop { let order = refresh_order(inner, url.to_string(), "valid")?; if !order.api_order.is_status_processing() { return Ok(order); } thread::sleep(delay); } } /// Order for an issued certificate that is ready to download. pub struct CertOrder { private_key: PKey, order: Order, } impl CertOrder { /// Request download of the issued certificate. pub fn download_cert(self) -> Result { // let url = self .order .api_order .certificate .ok_or_else(|| anyhow::anyhow!("certificate url"))?; let inner = self.order.inner; let mut res = inner.transport.call(&url, &ApiEmptyString)?; // save key and cert into persistence let pkey_pem_bytes = self.private_key.private_key_to_pem_pkcs8()?; let pkey_pem = String::from_utf8_lossy(&pkey_pem_bytes); let cert = res.body_mut().read_to_string().map_err(|e| e.into_io())?; Ok(Certificate::new(pkey_pem.to_string(), cert)) } /// Access the underlying JSON object for debugging. pub fn api_order(&self) -> &ApiOrder { &self.order.api_order } } #[cfg(test)] mod test { use super::*; use crate::*; #[test] fn test_get_authorizations() -> Result<()> { let server = crate::test::with_directory_server(); let url = DirectoryUrl::Other(&server.dir_url); let dir = Directory::from_url(url)?; let acc = dir.register_account(vec!["mailto:foo@bar.com".to_string()])?; let ord = acc.new_order("acmetest.example.com", &[])?; let _ = ord.authorizations()?; Ok(()) } #[test] fn test_finalize() -> Result<()> { let server = crate::test::with_directory_server(); let url = DirectoryUrl::Other(&server.dir_url); let dir = Directory::from_url(url)?; let acc = dir.register_account(vec!["mailto:foo@bar.com".to_string()])?; let ord = acc.new_order("acmetest.example.com", &[])?; // shortcut auth let ord = CsrOrder { order: ord.order }; let pkey = cert::create_p256_key()?; let _ord = ord.finalize_pkey(pkey, Duration::from_millis(1))?; Ok(()) } #[test] fn test_download_and_save_cert() -> Result<()> { let server = crate::test::with_directory_server(); let url = DirectoryUrl::Other(&server.dir_url); let dir = Directory::from_url(url)?; let acc = dir.register_account(vec!["mailto:foo@bar.com".to_string()])?; let ord = acc.new_order("acmetest.example.com", &[])?; // shortcut auth let ord = CsrOrder { order: ord.order }; let pkey = cert::create_p256_key()?; let ord = ord.finalize_pkey(pkey, Duration::from_millis(1))?; let cert = ord.download_cert()?; assert_eq!("CERT HERE", cert.certificate()); assert!(!cert.private_key().is_empty()); assert_eq!(cert.valid_days_left()?, 89); Ok(()) } } acme-micro-0.14.0/src/req.rs000064400000000000000000000057201046102023000136520ustar 00000000000000use crate::api::ApiProblem; use crate::error::*; use ureq::{http, Body}; pub(crate) type ReqResult = std::result::Result; const TIMEOUT_DURATION: std::time::Duration = std::time::Duration::from_secs(30); pub(crate) fn req_get(url: &str) -> Result, ureq::Error> { let req = ureq::get(url) .config() .timeout_global(Some(TIMEOUT_DURATION)) .http_status_as_error(false) .build(); trace!("{:?}", req); req.call() } pub(crate) fn req_head(url: &str) -> Result, ureq::Error> { let req = ureq::head(url) .config() .timeout_global(Some(TIMEOUT_DURATION)) .http_status_as_error(false) .build(); trace!("{:?}", req); req.call() } pub(crate) fn req_post(url: &str, body: &str) -> Result, ureq::Error> { let req = ureq::post(url) .header("content-type", "application/jose+json") .config() .timeout_global(Some(TIMEOUT_DURATION)) .http_status_as_error(false) .build(); trace!("{:?} {}", req, body); req.send(body) } pub(crate) fn req_handle_error( res: Result, ureq::Error>, ) -> ReqResult> { let res = res.map_err(|err| ApiProblem { _type: "httpReqError".into(), detail: Some(err.to_string()), subproblems: None, })?; if res.status().is_success() { return Ok(res); } let problem = if res.body().mime_type() == Some("application/problem+json") { // if we were sent a problem+json, deserialize it let body = req_safe_read_body(res); serde_json::from_str(&body).unwrap_or_else(|err| ApiProblem { _type: "problemJsonFail".into(), detail: Some(format!( "Failed to deserialize application/problem+json ({err}) body: {body}", )), subproblems: None, }) } else { // some other problem let status = format!( "{} {}", res.status(), res.status().canonical_reason().unwrap_or_default() ); let body = req_safe_read_body(res); let detail = format!("{} body: {}", status, body); ApiProblem { _type: "httpReqError".into(), detail: Some(detail), subproblems: None, } }; Err(problem) } pub(crate) fn req_expect_header(res: &http::Response, name: &str) -> ReqResult { res.headers() .get(name) .map(|v| v.to_str().unwrap_or_default().to_string()) .ok_or_else(|| ApiProblem { _type: format!("Missing header: {}", name), detail: None, subproblems: None, }) } pub(crate) fn req_safe_read_body(mut res: http::Response) -> String { // letsencrypt sometimes closes the TLS abruptly causing io error // even though we did capture the body. res.body_mut().read_to_string().unwrap_or_default() } acme-micro-0.14.0/src/test/mod.rs000064400000000000000000000161201046102023000146150ustar 00000000000000#![allow(clippy::trivial_regex)] use std::net::TcpListener; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread; static RE_URL: std::sync::OnceLock = std::sync::OnceLock::new(); fn re_url() -> &'static regex::Regex { RE_URL.get_or_init(|| regex::Regex::new("").unwrap()) } pub struct TestServer { pub dir_url: String, shutdown: Arc, thread_handle: Option>, } impl Drop for TestServer { fn drop(&mut self) { self.shutdown.store(true, Ordering::SeqCst); if let Some(handle) = self.thread_handle.take() { handle.join().ok(); } } } fn get_directory(url: &str) -> String { const BODY: &str = r#"{ "keyChange": "/acme/key-change", "newAccount": "/acme/new-acct", "newNonce": "/acme/new-nonce", "newOrder": "/acme/new-order", "revokeCert": "/acme/revoke-cert", "meta": { "caaIdentities": [ "testdir.org" ] } }"#; re_url().replace_all(BODY, url).to_string() } fn head_new_nonce() -> (u16, Vec<(&'static str, &'static str)>, String) { ( 204, vec![( "Replay-Nonce", "8_uBBV3N2DBRJczhoiB46ugJKUkUHxGzVe6xIMpjHFM", )], String::new(), ) } fn post_new_acct(url: &str) -> (u16, Vec<(String, String)>, String) { const BODY: &str = r#"{ "id": 7728515, "key": { "use": "sig", "kty": "EC", "crv": "P-256", "alg": "ES256", "x": "ttpobTRK2bw7ttGBESRO7Nb23mbIRfnRZwunL1W6wRI", "y": "h2Z00J37_2qRKH0-flrHEsH0xbit915Tyvd2v_CAOSk" }, "contact": [ "mailto:foo@bar.com" ], "initialIp": "90.171.37.12", "createdAt": "2018-12-31T17:15:40.399104457Z", "status": "valid" }"#; let location = re_url() .replace_all("/acme/acct/7728515", url) .to_string(); ( 201, vec![("Location".to_string(), location)], BODY.to_string(), ) } fn post_new_order(url: &str) -> (u16, Vec<(String, String)>, String) { const BODY: &str = r#"{ "status": "pending", "expires": "2019-01-09T08:26:43.570360537Z", "identifiers": [ { "type": "dns", "value": "acmetest.example.com" } ], "authorizations": [ "/acme/authz/YTqpYUthlVfwBncUufE8IRWLMSRqcSs" ], "finalize": "/acme/finalize/7738992/18234324" }"#; let location = re_url() .replace_all("/acme/order/YTqpYUthlVfwBncUufE8", url) .to_string(); let body = re_url().replace_all(BODY, url).to_string(); (201, vec![("Location".to_string(), location)], body) } fn post_get_order(url: &str) -> (u16, Vec<(String, String)>, String) { const BODY: &str = r#"{ "status": "", "expires": "2019-01-09T08:26:43.570360537Z", "identifiers": [ { "type": "dns", "value": "acmetest.example.com" } ], "authorizations": [ "/acme/authz/YTqpYUthlVfwBncUufE8IRWLMSRqcSs" ], "finalize": "/acme/finalize/7738992/18234324", "certificate": "/acme/cert/fae41c070f967713109028" }"#; let body = re_url().replace_all(BODY, url).to_string(); (200, vec![], body) } fn post_authz(url: &str) -> (u16, Vec<(String, String)>, String) { const BODY: &str = r#"{ "identifier": { "type": "dns", "value": "acmetest.algesten.se" }, "status": "pending", "expires": "2019-01-09T08:26:43Z", "challenges": [ { "type": "http-01", "status": "pending", "url": "/acme/challenge/YTqpYUthlVfwBncUufE8IRWLMSRqcSs/216789597", "token": "MUi-gqeOJdRkSb_YR2eaMxQBqf6al8dgt_dOttSWb0w" }, { "type": "tls-alpn-01", "status": "pending", "url": "/acme/challenge/YTqpYUthlVfwBncUufE8IRWLMSRqcSs/216789598", "token": "WCdRWkCy4THTD_j5IH4ISAzr59lFIg5wzYmKxuOJ1lU" }, { "type": "dns-01", "status": "pending", "url": "/acme/challenge/YTqpYUthlVfwBncUufE8IRWLMSRqcSs/216789599", "token": "RRo2ZcXAEqxKvMH8RGcATjSK1KknLEUmauwfQ5i3gG8" } ] }"#; let body = re_url().replace_all(BODY, url).to_string(); (201, vec![], body) } fn post_finalize(_url: &str) -> (u16, Vec<(String, String)>, String) { (200, vec![], String::new()) } fn post_certificate(_url: &str) -> (u16, Vec<(String, String)>, String) { (200, vec![], "CERT HERE".to_string()) } fn route_request(method: &str, path: &str, url: &str) -> (u16, Vec<(String, String)>, String) { match (method, path) { ("GET", "/directory") => (200, vec![], get_directory(url)), ("HEAD", "/acme/new-nonce") => { let (status, headers, body) = head_new_nonce(); ( status, headers .into_iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect(), body, ) } ("POST", "/acme/new-acct") => post_new_acct(url), ("POST", "/acme/new-order") => post_new_order(url), ("POST", "/acme/order/YTqpYUthlVfwBncUufE8") => post_get_order(url), ("POST", "/acme/authz/YTqpYUthlVfwBncUufE8IRWLMSRqcSs") => post_authz(url), ("POST", "/acme/finalize/7738992/18234324") => post_finalize(url), ("POST", "/acme/cert/fae41c070f967713109028") => post_certificate(url), _ => (404, vec![], String::new()), } } pub fn with_directory_server() -> TestServer { let tcp = TcpListener::bind("127.0.0.1:0").unwrap(); let port = tcp.local_addr().unwrap().port(); let url = format!("http://127.0.0.1:{}", port); let dir_url = format!("{}/directory", url); let shutdown = Arc::new(AtomicBool::new(false)); let shutdown_clone = shutdown.clone(); let handle = thread::spawn(move || { let server = tiny_http::Server::from_listener(tcp, None).unwrap(); while !shutdown_clone.load(Ordering::SeqCst) { let request = match server.recv_timeout(std::time::Duration::from_millis(100)) { Ok(Some(req)) => req, Ok(None) => continue, Err(_) => break, }; let method = request.method().as_str(); let path = request.url(); let (status, headers, body) = route_request(method, path, &url); let mut response = tiny_http::Response::from_string(body).with_status_code(status); for (key, value) in headers { if let Ok(header) = tiny_http::Header::from_bytes(key.as_bytes(), value.as_bytes()) { response.add_header(header); } } let _ = request.respond(response); } }); TestServer { dir_url, shutdown, thread_handle: Some(handle), } } #[test] pub fn test_make_directory() { let server = with_directory_server(); let res = ureq::get(&server.dir_url).call(); assert!(res.is_ok()); } acme-micro-0.14.0/src/trans.rs000064400000000000000000000135171046102023000142150ustar 00000000000000use openssl::ecdsa::EcdsaSig; use openssl::sha::sha256; use serde::Serialize; use std::collections::VecDeque; use std::{ convert::TryInto, sync::{Arc, Mutex}, }; use ureq::{http, Body}; use crate::acc::AcmeKey; use crate::error::*; use crate::jwt::*; use crate::req::{req_expect_header, req_handle_error, req_head, req_post}; use crate::util::base64url; /// JWS payload and nonce handling for requests to the API. /// /// Setup is: /// /// 1. `Transport::new()` /// 2. `call_jwk()` against newAccount url /// 3. `set_key_id` from the returned `Location` header. /// 4. `call()` for all calls after that. #[derive(Clone, Debug)] pub(crate) struct Transport { acme_key: AcmeKey, nonce_pool: Arc, } impl Transport { pub fn new(nonce_pool: &Arc, acme_key: AcmeKey) -> Self { Transport { acme_key, nonce_pool: nonce_pool.clone(), } } /// Update the key id once it is known (part of setting up the transport). pub fn set_key_id(&mut self, kid: String) { self.acme_key.set_key_id(kid); } /// The key used in the transport pub fn acme_key(&self) -> &AcmeKey { &self.acme_key } /// Make call using the full jwk. Only for the first newAccount request. pub fn call_jwk( &self, url: &str, body: &T, ) -> Result> { self.do_call(url, body, jws_with_jwk) } /// Make call using the key id pub fn call(&self, url: &str, body: &T) -> Result> { self.do_call(url, body, jws_with_kid) } fn do_call Result>( &self, url: &str, body: &T, make_body: F, ) -> Result> { // The ACME API may at any point invalidate all nonces. If we detect such an // error, we loop until the server accepts the nonce. loop { // Either get a new nonce, or reuse one from a previous request. let nonce = self.nonce_pool.get_nonce()?; // Sign the body. let body = make_body(url, nonce, &self.acme_key, body)?; debug!("Call endpoint {}", url); // Post it to the URL let response = req_post(url, &body); // Regardless of the request being a success or not, there might be // a nonce in the response. self.nonce_pool.extract_nonce(&response); // Turn errors into ApiProblem. let result = req_handle_error(response); if let Err(problem) = &result { if problem.is_bad_nonce() { // retry the request with a new nonce. debug!("Retrying on bad nonce"); continue; } // it seems we sometimes make bad JWTs. Why?! if problem.is_jwt_verification_error() { debug!("Retrying on: {}", problem); continue; } } return Ok(result?); } } } /// Shared pool of nonces. #[derive(Default, Debug)] pub(crate) struct NoncePool { nonce_url: String, pool: Mutex>, } impl NoncePool { pub fn new(nonce_url: &str) -> Self { NoncePool { nonce_url: nonce_url.into(), ..Default::default() } } fn extract_nonce(&self, res: &Result, ureq::Error>) { let Ok(res) = res else { return }; if let Some(nonce) = res.headers().get("replay-nonce") { trace!("Extract nonce"); let mut pool = self.pool.lock().unwrap(); let Ok(nonce) = nonce.to_str() else { return }; pool.push_back(nonce.to_string()); if pool.len() > 10 { pool.pop_front(); } } } fn get_nonce(&self) -> Result { { let mut pool = self.pool.lock().unwrap(); if let Some(nonce) = pool.pop_front() { trace!("Use previous nonce"); return Ok(nonce); } } debug!("Request new nonce"); let res = req_head(&self.nonce_url)?; Ok(req_expect_header(&res, "replay-nonce")?) } } fn jws_with_kid( url: &str, nonce: String, key: &AcmeKey, payload: &T, ) -> Result { let protected = JwsProtected::new_kid(key.key_id(), url, nonce); jws_with(protected, key, payload) } fn jws_with_jwk( url: &str, nonce: String, key: &AcmeKey, payload: &T, ) -> Result { let jwk: Jwk = key.try_into()?; let protected = JwsProtected::new_jwk(jwk, url, nonce); jws_with(protected, key, payload) } fn jws_with( protected: JwsProtected, key: &AcmeKey, payload: &T, ) -> Result { let protected = { let pro_json = serde_json::to_string(&protected)?; base64url(pro_json.as_bytes()) }; let payload = { let pay_json = serde_json::to_string(payload)?; if pay_json == "\"\"" { // This is a special case produced by ApiEmptyString and should // not be further base64url encoded. "".to_string() } else { base64url(pay_json.as_bytes()) } }; let to_sign = format!("{}.{}", protected, payload); let digest = sha256(to_sign.as_bytes()); let sig = EcdsaSig::sign(&digest, key.private_key())?; let r = sig.r().to_vec(); let s = sig.s().to_vec(); let mut v = Vec::with_capacity(r.len() + s.len()); v.extend_from_slice(&r); v.extend_from_slice(&s); let signature = base64url(&v); let jws = Jws::new(protected, payload, signature); Ok(serde_json::to_string(&jws)?) } acme-micro-0.14.0/src/util.rs000064400000000000000000000007271046102023000140420ustar 00000000000000use base64::Engine; use serde::de::DeserializeOwned; use ureq::{http, Body}; use crate::error::*; use crate::req::req_safe_read_body; pub(crate) fn base64url>(input: &T) -> String { base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(input) } pub(crate) fn read_json(res: http::Response) -> Result { let res_body = req_safe_read_body(res); debug!("{}", res_body); Ok(serde_json::from_str(&res_body)?) }