21个版本 (稳定版)

5.0.0-rc.22024年6月1日
5.0.0-alpha.32023年12月15日
4.0.0 2023年11月26日
3.0.0 2023年11月25日
1.5.0 2023年3月30日

#357 in 密码学

每月42次下载

MIT许可

170KB
3.5K SLoC

另一个证书管理引擎

crate Docs Build Status MIT licensed

YACME是ACME协议的实现。

功能

YACME支持自定义证书、CA和ACME服务器。它支持HTTP-01和DNS-01授权挑战。目前不支持TLS-ALPN-01挑战,但未来可能会支持。

YACME也不支持证书吊销或账户证书更新。

YACME使用jaws提供JWT支持,因此支持该crate支持的所有签名算法。

入门指南

使用高级服务接口,您可以连接到letsencrypt(或者实际上是ACME提供商)并颁发证书

(有关此示例的更多详细信息,请参阅letsencrypt-pebble.rs,下面的代码不旨在编译)

//! Run a certificate issue process via the pebble local ACME server
//!
//! *Prerequisite*: Start the pebble server via docker-compose. It is defined in the
//! pebble/ directory, or available at https://github.com/letsencrypt/pebble/
//!
//! This example handles the challenge using pebble's challenge server. In the real world,
//! you would have to implement this yourself.

use std::sync::Arc;

use pkcs8::DecodePrivateKey;
use signature::rand_core::OsRng;
use yacme::schema::authorizations::AuthorizationStatus;
use yacme::schema::challenges::{ChallengeKind, Http01Challenge};

const PRIVATE_KEY_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/reference-keys/ec-p255.pem");
const PEBBLE_ROOT_CA: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/pebble/pebble.minica.pem");

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    tracing::debug!("Loading root certificate from {PEBBLE_ROOT_CA}");
    let cert = reqwest::Certificate::from_pem(&std::fs::read(PEBBLE_ROOT_CA)?)?;

    let provider = yacme::service::Provider::build()
        .directory_url(yacme::service::provider::PEBBLE.parse().unwrap())
        .add_root_certificate(cert)
        .timeout(std::time::Duration::from_secs(30))
        .build()
        .await?;

    tracing::info!("Loading private key from {PRIVATE_KEY_PATH:?}");

    let key = Arc::new(ecdsa::SigningKey::from_pkcs8_pem(
        &std::fs::read_to_string(PRIVATE_KEY_PATH)?,
    )?);

    // Step 1: Get an account
    tracing::info!("Requesting account");
    let account = provider
        .account(key)
        .add_contact_email("[email protected]")?
        .agree_to_terms_of_service()
        .create()
        .await?;

    tracing::trace!("Account: \n{account:#?}");
    tracing::info!("Requesting order");

    let mut order = account
        .order()
        .dns("www.example.test")
        .dns("internal.example.test")
        .create()
        .await?;
    tracing::trace!("Order: \n{order:#?}");

    tracing::info!("Completing Authorizations");

    for auth in order.authorizations().await?.iter_mut() {
        tracing::info!("Authorizing {:?}", auth.identifier());
        tracing::trace!("Authorization: \n{auth:#?}");

        if !matches!(auth.data().status, AuthorizationStatus::Pending) {
            continue;
        }

        let mut chall = auth
            .challenge(&ChallengeKind::Http01)
            .ok_or("No http01 challenge provided")?;
        let inner = chall.http01().unwrap();
        http01_challenge_response(&inner, &account.key()).await?;

        chall.ready().await?;
        auth.finalize().await?;
        tracing::info!("Authorization finalized");
    }

    tracing::info!("Finalizing order");
    tracing::debug!("Generating random certificate key");
    let certificate_key = Arc::new(ecdsa::SigningKey::<p256::NistP256>::random(&mut OsRng));
    let cert = order
        .finalize_and_download::<ecdsa::SigningKey::<p256::NistP256>, ecdsa::der::Signature<p256::NistP256>>(&certificate_key)
        .await?;

    println!("{}", cert.to_pem_documents()?.join(""));

    Ok(())
}

// This method is specific to pebble - you would set up your challenge respons in an appropriate fashion
async fn http01_challenge_response(
    challenge: &Http01Challenge,
    key: &ecdsa::SigningKey<p256::NistP256>,
) -> Result<(), reqwest::Error> {
    #[derive(Debug, serde::Serialize)]
    struct Http01ChallengeSetup {
        token: String,
        content: String,
    }

    let chall_setup = Http01ChallengeSetup {
        token: challenge.token().into(),
        content: (*challenge.authorization(key)).to_owned(),
    };

    tracing::trace!(
        "Challenge Setup:\n{}",
        serde_json::to_string(&chall_setup).unwrap()
    );

    let resp = reqwest::Client::new()
        .post("https://127.0.0.1:8055/add-http01")
        .json(&chall_setup)
        .send()
        .await?;
    match resp.error_for_status_ref() {
        Ok(_) => {}
        Err(_) => {
            eprintln!("Request:");
            eprintln!("{}", serde_json::to_string(&chall_setup).unwrap());
            eprintln!("ERROR:");
            eprintln!("Status: {:?}", resp.status().canonical_reason());
            eprintln!("{}", resp.text().await?);
            panic!("Failed to update challenge server");
        }
    }

    Ok(())
}

导航指南

YACME分为几个API级别

  • service是高级API,提供了一种简单的接口来颁发证书。
  • schema提供了实现单个ACME端点的所有数据结构。
  • protocol提供了ACME服务器使用的JWT协议。
  • cert提供了对X.509证书的支持。

目标

这是一个为了获得一个我喜欢的Rust ACME客户端,并更多地了解ACME和互联网协议密码学而进行的yak-shave项目。

本项目的目标设计是

  • 无OpenSSL依赖。这里的密码学应该是纯Rust。
  • 模块化和可重用。这不是一个有偏见的命令行工具来帮助您入门。相反,这个库希望能够轻松集成到现有项目中,如基于hyper的项目。
  • 易于扩展:添加新的签名算法、挑战类型和其他扩展(假设它们由纯Rust库支持)应该相对简单。
  • 运行时灵活。可以更换签名算法,而无需更改调用ACME服务的代码中的类型。

这可能不适合生产使用,但它基于RustCrypto的工作,他们制作了很好的东西。不要责怪他们,责怪我吧!

依赖关系

约14-27MB
约443K SLoC