Python-Acme client acme-client-0.5.3: Yet another Let's Encrypt client and library written in Rust.

icon
Latest Release: acme-client-0.5.3

LIB

  • Use latest version of error-chain crate (0.12).
Source code(tar.gz)
Source code(zip)

acme-client

Build Status License Crates.io docs.rs.io

Easy to use Let's Encrypt compatible ACME client to issue, renew and revoke TLS certificates.

Contents

Installation

By default acme-client crate comes with a handy CLI. You can install acme-client with: cargo install acme-client or you can download pre-built acme-client binary for Linux in the releases page.

Usage

acme-client is using the OpenSSL library to generate all required keys and certificate signing request. You don't need to run any openssl command. You can use your already generated keys and CSR if you want and you don't need any root access while running acme-client.

acme-client is using simple HTTP validation to pass Let's Encrypt's DNS validation challenge. You need a working HTTP server to host the challenge file.

Sign a certificate

acme-client sign -D example.org -P /var/www -k domain.key -o domain.crt

This command will generate a user key, domain key and X509 certificate signing request. It will register a new user account and identify the domain ownership by putting the required challenge token into /var/www/.well-known/acme-challenge/. If everything goes well, it will save the domain private key into domain.key and the signed certificate into domain.crt.

You can also use the --email option to provide a contact adress on registration.

Using your own keys and CSR

You can use your own RSA keys for user registration and domain. For example:

acme-client sign \
  --user-key user.key \
  --domain-key domain.key \
  --domain-csr domain.csr \
  -P /var/www \
  -o domain.crt

This will not generate any key and it will use provided keys to sign the certificate. It will also get domain names from provided CSR file.

Using DNS validation

You can use --dns flag to trigger DNS validation instead of HTTP. This option requires user to generate a TXT record for domain. An example DNS validation:

$ acme-client sign --dns -D onur.im -E [email protected] \
    -k /tmp/onur.im.key -o /tmp/onur.im.crt
Please create a TXT record for _acme-challenge.onur.im: fDdTmWl4RMuGqj9acJiTC13hF6dVOZUNm3FujCIz3jc
Press enter to continue

Revoking a signed certificate

acme-client can also revoke a signed certificate. You need to use your user key and a signed certificate to revoke.

acme-client revoke --user-key user.key --signed-crt signed.crt

Options

You can get a list of all available options with acme-client sign --help and acme-client revoke --help:

$ acme-client sign --help
acme-client-sign
Signs a certificate

USAGE:
    acme-client sign [FLAGS] [OPTIONS]

FLAGS:
    -d, --dns        Use DNS challenge instead of HTTP. This option requires
                     user to generate a TXT record for domain.
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
    -A, --directory <DIRECTORY>
            Set ACME directory URL [default: https://acme-v01.api.letsencrypt.org/directory]
    -D, --domain <DOMAIN>...
            Domain name to obtain certificate. You can use more than one domain name.
    -P, --public-dir <PUBLIC_DIR>
            Directory to save ACME simple HTTP challenge. This option is
            required unless --dns option is being used.
    -U, --user-key <USER_KEY_PATH>
            Path to load user private key to use it in account registration.
            This is optional and acme-client will generate one if it's not supplied.
    -C, --csr <DOMAIN_CSR>
            Path to load domain certificate signing request. acme-client can also use CSR to get domain names.
            This is optional and acme-client will generate one if it's not supplied.
    -K, --domain-key <DOMAIN_KEY_PATH>
            Path to load private domain key.
            This is optional and acme-client will generate one if it's not supplied.
    -E, --email <EMAIL>
            Contact email address (optional).
    -c, --save-chained-crt <SAVE_CHAINED_CERTIFICATE>
            Chain signed certificate with Let's Encrypt Authority X3
            (IdenTrust cross-signed) intermediate certificate and save to given path.
    -r, --save-csr <SAVE_DOMAIN_CSR>
            Path to save domain certificate signing request generated by acme-client.
    -k, --save-domain-key <SAVE_DOMAIN_KEY>
            Path to save domain private key generated by acme-client.
    -i, --save-intermediate-crt <SAVE_INTERMEDIATE_CERTIFICATE>
            Path to save intermediate certificate.
    -o, --save-crt <SAVE_SIGNED_CERTIFICATE>
            Path to save signed certificate. Default is STDOUT.
    -u, --save-user-key <SAVE_USER_KEY>
            Path to save private user key.
$ acme-client revoke --help
acme-client-revoke
Revokes a signed certificate

USAGE:
    acme-client revoke --user-key <USER_KEY> --signed-crt <SIGNED_CRT>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
    -C, --signed-crt <SIGNED_CRT>    Path to signed domain certificate to revoke.
    -K, --user-key <USER_KEY>        User or domain private key path.

There is also genkey and gencsr subcommands to generate RSA private key and CSR. You can use multiple -v flags for verbose output.

Library

You can read entire API documentation in docs.rs. You can use acme-client library by adding following lines to your Cargo.toml:

[dependencies]
acme-client = "0.5"

By default acme-client will build CLI. You can disable this with:

[dependencies.acme-client]
version = "0.5"
default-features = false

API overview

To successfully sign a SSL certificate for a domain name, you need to identify ownership of your domain. You can also identify and sign certificate for multiple domain names and explicitly use your own private keys and certificate signing request (CSR), otherwise this library will generate them. Basic usage of acme-client:

use acme_client::Directory;

let directory = Directory::lets_encrypt()?;
let account = directory.account_registration().register()?;

// Create a identifier authorization for example.com
let authorization = account.authorization("example.com")?;

// Validate ownership of example.com with http challenge
let http_challenge = authorization.get_http_challenge().ok_or("HTTP challenge not found")?;
http_challenge.save_key_authorization("/var/www")?;
http_challenge.validate()?;

let cert = account.certificate_signer(&["example.com"]).sign_certificate()?;
cert.save_signed_certificate("certificate.pem")?;
cert.save_private_key("certificate.key")?;

acme-client supports signing a certificate for multiple domain names with SAN. You need to validate ownership of each domain name:

use acme_client::Directory;

let directory = Directory::lets_encrypt()?;
let account = directory.account_registration().register()?;

let domains = ["example.com", "example.org"];

for domain in domains.iter() {
    let authorization = account.authorization(domain)?;
    // ...
}

let cert = account.certificate_signer(&domains).sign_certificate()?;
cert.save_signed_certificate("certificate.pem")?;
cert.save_private_key("certificate.key")?;

Account registration

use acme_client::Directory;

let directory = Directory::lets_encrypt()?;
let account = directory.account_registration()
                       .email("[email protected]")
                       .register()?;

Contact email address is optional. You can also use your own private key during registration. See AccountRegistration helper for more details.

If you already registed with your own keys before, you still need to use register method, in this case it will identify your user account instead of creating a new one.

Identifying ownership of domain name

Before sending a certificate signing request to an ACME server, you need to identify ownership of domain names you want to sign a certificate for. To do that you need to create an Authorization object for a domain name and fulfill at least one challenge (http or dns for Let's Encrypt).

To create an Authorization object for a domain:

let authorization = account.authorization("example.com")?;

Authorization object will contain challenges created by ACME server. You can create as many Authorization object as you want to verifiy ownership of the domain names. For example if you want to sign a certificate for example.com and example.org:

let domains = ["example.com", "example.org"];
for domain in domains.iter() {
    let authorization = account.authorization(domain)?;
    // ...
}

Identifier validation challenges

When you send authorization request to an ACME server, it will generate identifier validation challenges to provide assurence that an account holder is also the entity that controls an identifier.

HTTP challenge

With HTTP validation, the client in an ACME transaction proves its control over a domain name by proving that it can provision resources on an HTTP server that responds for that domain name.

acme-client has save_key_authorization method to save vaditation file to a public directory. This directory must be accessible to outside world.

let authorization = account.authorization("example.com")?;
let http_challenge = authorization.get_http_challenge().ok_or("HTTP challenge not found")?;

// This method will save key authorization into
// /var/www/.well-known/acme-challenge/ directory.
http_challenge.save_key_authorization("/var/www")?;

// Validate ownership of example.com with http challenge
http_challenge.validate()?;

During validation, ACME server will check http://example.com/.well-known/acme-challenge/{token} to identify ownership of domain name. You need to make sure token is publicly accessible.

DNS challenge:

The DNS challenge requires the client to provision a TXT record containing a designated value under a specific validation domain name.

acme-client can generated this value with signature method.

The user constructs the validation domain name by prepending the label "_acme-challenge" to the domain name being validated, then provisions a TXT record with the digest value under that name. For example, if the domain name being validated is "example.com", then the client would provision the following DNS record:

_acme-challenge.example.com: dns_challenge.signature()

Example validation with DNS challenge:

let authorization = account.authorization("example.com")?;
let dns_challenge = authorization.get_dns_challenge().ok_or("DNS challenge not found")?;
let signature = dns_challenge.signature()?;

// User creates a TXT record for _acme-challenge.example.com with the value of signature.

// Validate ownership of example.com with DNS challenge
dns_challenge.validate()?;

Signing a certificate

After validating all the domain names you can send a sign certificate request. acme-client provides CertificateSigner helper for this. You can use your own key and CSR or you can let CertificateSigner to generate them for you.

let domains = ["example.com", "example.org"];

// ... validate ownership of domain names

let certificate_signer = account.certificate_signer(&domains);
let cert = certificate_signer.sign_certificate()?;
cert.save_signed_certificate("certificate.pem")?;
cert.save_private_key("certificate.key")?;

Revoking a signed certificate

You can use revoke_certificate or revoke_certificate_from_file methods to revoke a signed certificate. You need to register with the same private key you registered before to successfully revoke a signed certificate. You can also use private key used to generate CSR.

let account = directory.account_registration()
                       .pkey_from_file("user.key")?
                       .register()?;
account.revoke_certificate_from_file("certificate.pem")?;

References

Comments

  • Released binary seems to use non-standard hardcoded OpenSSL paths
    Released binary seems to use non-standard hardcoded OpenSSL paths

    Mar 9, 2018

    I was trying this project out today in my search for a better ACME client and couldn't seem to get past an error stating https://acme-v01.api.letsencrypt.org/directory: The OpenSSL library reported an error: The OpenSSL library reported an error: error:14007086:SSL routines:CONNECT_CR_CERT:certificate verify failed:ssl_clnt.c:1026:. The binary appears to be statically linked and is using paths to your home directory instead of the default system paths for SSL certificates. Given that OpenSSL is statically linked I'm not sure how useful releasing a binary is, since different distributions all have different places where they keep their CA bundles.

    open("/home/onur/code/libressl/etc/ssl/cert.pem", O_RDONLY) = -1 ENOENT (No such file or directory)
    stat("/home/onur/code/libressl/etc/ssl/certs/2e5ac55d.0", 0x7fffc81ce740) = -1 ENOENT (No such file or directory)
    stat("/home/onur/code/libressl/etc/ssl/certs/ef954a4e.0", 0x7fffc81ce740) = -1 ENOENT (No such file or directory)
    stat("/home/onur/code/libressl/etc/ssl/certs/b3964d1c.0", 0x7fffc81ce740) = -1 ENOENT (No such file or directory)
    
    Reply
  • Upgrade to latest hyper/tokio
    Upgrade to latest hyper/tokio

    Apr 11, 2018

    This would be a significant breaking change, but I given how the ecosystem is moving in Rust, I think it makes sense for acme-client to move to using the latest hyper version and the tokio ecosystem.

    Reply
  • Bumped reqwest and hyper to latest versions
    Bumped reqwest and hyper to latest versions

    Nov 11, 2018

                                                                                                                                                                                                           
    Reply
  • Update reqwest version from 0.6 to 0.9.5
    Update reqwest version from 0.6 to 0.9.5

    Nov 29, 2018

                                                                                                                                                                                                           
    Reply
  • typo?
    typo?

    Dec 26, 2018

    acme-client $ rg contract
    src/lib.rs
    726:            map.insert("contract".to_owned(), to_value(contact)?);
    728:            map.insert("contract".to_owned(),
    

    contract should be contact?

    Reply
  • Does not work with modern openssl versions
    Does not work with modern openssl versions

    Jan 25, 2019

    acme-client depends on an old version of reqwest which in turn depends on an old version of the openssl crate. The old openssl crate is not able to use at least openssl 1.1.1 (I don't know the exact version where it stops working)

    thread 'main' panicked at 'Unable to detect OpenSSL version', /.../registry/src/github.com-1ecc6299db9ec823/openssl-0.9.24/build.rs:16:14
    stack backtrace:
       0: std::sys::unix::backtrace::tracing::imp::unwind_backtrace
                 at libstd/sys/unix/backtrace/tracing/gcc_s.rs:49
       1: std::sys_common::backtrace::print
                 at libstd/sys_common/backtrace.rs:71
                 at libstd/sys_common/backtrace.rs:59
       2: std::panicking::default_hook::{{closure}}
                 at libstd/panicking.rs:211
       3: std::panicking::default_hook
                 at libstd/panicking.rs:227
       4: std::panicking::rust_panic_with_hook
                 at libstd/panicking.rs:476
       5: std::panicking::begin_panic
       6: build_script_build::main
       7: std::rt::lang_start::{{closure}}
       8: std::panicking::try::do_call
                 at libstd/rt.rs:59
                 at libstd/panicking.rs:310
       9: __rust_maybe_catch_panic
                 at libpanic_unwind/lib.rs:102
      10: std::rt::lang_start_internal
                 at libstd/panicking.rs:289
                 at libstd/panic.rs:392
                 at libstd/rt.rs:58
      11: main
      12: __libc_start_main
      13: _start
    
    
    Reply
  • Update reqwest to 0.9.x
    Update reqwest to 0.9.x

    Jan 25, 2019

    Older reqwest versions depends on an old openssl crate which does not work with openssl 1.1.1.

    I believe I have not introduced any new panics.

    Fixes #43

    Reply
  • Ensure consistent JWK generation.
    Ensure consistent JWK generation.

    Feb 14, 2019

    I'm opening this PR because I was running into issues where the HTTP-01 workflow was failing due to invalid signatures and such. I investigated the code here after thoroughly reviewing my own code in the service which is using this crate. The issue has been resolved after using BTreeMap instead of standard HashMap.

    It was one of those tough bugs to resolve where the issue was presenting itself intermittently. Sometimes the workflow would succeed, other times it would fail for no apparently reason or code change on my side.

    After making this update, the issue has not presented itself. For reasoning straight from the docs, see the first few paragraphs under this heading: https://doc.rust-lang.org/std/collections/index.html#iterators

    Reply
  • upgrade reqwest to 0.9
    upgrade reqwest to 0.9

    Apr 26, 2019

    null

                                                                                                                                                                                                           
    Reply
  • Put a note in README that this library uses deprecated protocol version and won't work
    Put a note in README that this library uses deprecated protocol version and won't work

    Nov 12, 2019

    The registration is now failing as per announcement: https://community.letsencrypt.org/t/end-of-life-plan-for-acmev1/88430

    Reply
  • Drop rustc_serialize in favor of serde_json
    Drop rustc_serialize in favor of serde_json

    Oct 14, 2017

    Hi.

    This should close #11. I've checked it only with cargo test, could be great if some real case could be tested as well on.

    Reply
  • Bumped log dependency to 0.4
    Bumped log dependency to 0.4

    Oct 23, 2018

                                                                                                                                                                                                           
    Reply
  • add private and public  domain- and userkey accessors
    add private and public domain- and userkey accessors

    Nov 4, 2016

    Fixes issue #3.

                                                                                                                                                                                                           
    Reply
  • Question about renewing
    Question about renewing

    May 21, 2018

    The README says, "Easy to use Let's Encrypt compatible ACME client to issue, renew and revoke TLS certificates.", but there's no other mention of renewing. Is the renew process just the sign process repeated or are their different steps?

    Reply
  • compile on older compiler versions
    compile on older compiler versions

    Feb 28, 2018

    On the current stable version of OpenBSD (6.2) for example the most current package is rust-1.20.0.

    And honestly, this little piece of syntax is not really necessary in that place ^^

    Reply
  • Transition to openssl 0.10
    Transition to openssl 0.10

    Mar 4, 2018

                                                                                                                                                                                                           
    Reply
  • Cross-compiled, now runtime error ‘The OpenSSL library reported an error’ (target `x86_64-unknown-linux-musl`)
    Cross-compiled, now runtime error ‘The OpenSSL library reported an error’ (target `x86_64-unknown-linux-musl`)

    May 26, 2017

    The openssl dependency makes cross-compilation very hard. (Once there is feature parity, I would be in favor of replacing it with webpki.) Even when cross-compilation succeeds, the letsencrypt-rs fails for me at runtime.

    I compiled openssl under Arch Linux, as required by the openssl crate:

    sudo pacman -Syu musl
    mkdir /opt/openssl/
    cd /srv/installers/
    curl --tlsv1.2 -O 'https://www.openssl.org/source/openssl-1.1.0e.tar.gz'
    tar xf 'openssl-1.1.0e.tar.gz'
    cd 'openssl-1.1.0e/'
    export CC='musl-gcc'
    ./Configure no-unit-test no-async no-afalgeng no-ui \
        --prefix=/opt/openssl/ linux-x86_64 -fPIC
    make -j"$(nproc)"
    sudo make install
    

    Then I compiled letsencrypt-rs:

    cd "$HOME/devel/github.com/onur/letsencrypt-rs/letsencrypt-rs/"
    env PKG_CONFIG_ALLOW_CROSS=1 OPENSSL_STATIC=1 OPENSSL_DIR=/opt/openssl/ \
        cargo build --release --target=x86_64-unknown-linux-musl
    

    However, when running with some CLI params under Alpine Linux 3.5, letsencrypt-rs exits with status 1 and message:

    The OpenSSL library reported an error
    

    How can I solve this? Could the error message be improved?

    Reply
  • openssl upgrade
    openssl upgrade

    Feb 20, 2017

    Please consider making a version that works with the upgraded openssl package. Perhaps in a different branch? Right now the crate isn't compatible with hyper-native-tls = "0.2.2" (needed for TLS in Hyper 0.10) because of it's older openssl-sys dependency.

    Reply
  • Drop rustc-serialize dependency
    Drop rustc-serialize dependency

    Apr 23, 2017

    It has been deprecated: announcement.

    Is there any functionality you would require from Serde or another library before this would be possible?

    Reply
  • New acme-client API and SAN support
    New acme-client API and SAN support

    Apr 3, 2017

    Closes: #7 Closes: #4

    New more idiomatic acme-client API with SAN support. Few examples:

    use acme_client::Directory;
    
    let directory = Directory::lets_encrypt().unwrap();
    let account = directory.account_registration().register().unwrap();
    
    // Create a identifier authorization for example.com
    let authorization = account.authorization("example.com").unwrap();
    
    // Validate ownership of example.com with http challenge
    let http_challenge = authorization.get_http_challenge().unwrap();
    http_challenge.save_key_authorization("/var/www").unwrap();
    http_challenge.validate().unwrap();
    
    let cert = account.certificate_signer(&["example.com"]).sign_certificate().unwrap();
    cert.save_signed_certificate("certificate.pem").unwrap();
    cert.save_private_key("certificate.key").unwrap();
    

    acme-client supports signing a certificate for multiple domain names with SAN. You need to validate ownership of each domain name:

    use acme_client::Directory;
    
    let directory = Directory::lets_encrypt().unwrap();
    let account = directory.account_registration().register().unwrap();
    
    let domains = ["example.com", "example.org"];
    
    for domain in domains.iter() {
        let authorization = account.authorization(domain).unwrap();
        // ...
    }
    
    let cert = account.certificate_signer(&domains).sign_certificate().unwrap();
    cert.save_signed_certificate("certificate.pem").unwrap();
    cert.save_private_key("certificate.key").unwrap();
    

    @ArtemGr what do you think?

    Reply