• 50从零开始用Rust编写nginx,原来TLS证书还可以这么申请


    wmproxy

    wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 负载均衡, 静态文件服务器,websocket代理,四层TCP/UDP转发,内网穿透等,会将实现过程分享出来,感兴趣的可以一起造个轮子

    项目地址

    国内: https://gitee.com/tickbh/wmproxy

    github: https://github.com/tickbh/wmproxy

    设计目标

    让系统拥有acme的能力,即可以领取Let's Encrypt的证书签发,快速实现上线部署。

    acme是什么?

    ACME(Automated Certificate Management Environment)是一个用于自动化管理SSL/TLS证书的协议。它通过自动获取、自动更新和自动拒绝等功能,可以大大提高SSL证书的管理和更新效率,降低错误风险,提高网站的安全性和稳定性。

    当ACME服务器发布不安全的SSL证书时,可以通过ACME协议自动拒绝证书,确保网站始终使用安全的SSL证书。此外,ACME协议还支持自动续期功能,这意味着在证书到期之前,系统可以自动申请并获取新的证书,从而避免了因证书过期而导致的网站访问中断或安全风险。

    acme的定义

    acme是一个可以自动获取 TLS证书的协议,acmev1已经被正式弃用,现行的acme在rfc8555定义。其中定义了SSL如何获取的整个过程,包括其中最重要的权限鉴定。

    以下是两种acme判定权限拥有者的鉴权方式,以下是wmproxy.net做为域名来举例。

    HTTP-01 方式鉴定

    HTTP-01 的校验原理是访问给你域名指向的 HTTP 服务增加一个临时 location,Let’s Encrypt 会发送 http 请求到 http://wmproxy.net/.well-known/acme-challenge/wmproxy.net 就是被校验的域名,TOKEN 是 ACME 协议的客户端负责放置的文件,在这里 ACME 客户端就是 acme-lib。Let’s Encrypt 会对比 TOKEN 是否符合预期,校验成功后就会颁发证书。不支持泛域名证书。成功后我们就可以拥有TLS证书了。

    • 优点
      配置简单通用
      任何DNS服务商均可

    • 缺点
      需要依赖HTTP服务器
      集群会无法申请的可能
      不支持泛域名

    DNS-01 方式鉴定

    在 ACME DNS 质询验证的自动化中,以下是一些关键步骤:

    1. 生成一个 DNS TXT 记录,如_acme-challenge
    2. 将 TXT 记录添加到 DNS 区域中。
    3. 通知 Let's Encrypt 验证 DNS 记录。
    4. 等待 Let's Encrypt 验证完成。
    5. 如果验证成功,则生成证书。
    6. 删除 DNS TXT 记录。

    此方法不需要你的服务使用 Http 服务,并且支持泛域名证书。

    • 优点
      不需要HTTP服务器
      支持泛域名

    • 缺点
      各DNS服务商均不一致

    acme在保证安全的情况下缩短了TLS证书的申请流程,可以自动化的进行部署,极大的缓解因证书过期带来的麻烦。

    代码实现

    依赖:acme-lib
    改造:之前是确定配置证书及密钥后直接生成完整的TLS信息TlsAcceptor,那么现在在未申请到证书前,不能确定完整的TlsAcceptor,需要对初始化对象进行重新改造处理。
    源码:wrap_tls_accepter
    定义:

    /// 为了适应acme, 重新改造Acceptor进行封装处理
    #[derive(Clone)]
    pub struct WrapTlsAccepter {
    pub last: Instant,
    pub domain: Option<String>,
    pub accepter: Option,
    }

    同样添加accept方法

    #[inline]
    pub fn accept(&self, stream: IO) -> io::Result>
    where
    IO: AsyncRead + AsyncWrite + Unpin,
    {
    self.accept_with(stream, |_| ())
    }
    pub fn accept_with(&self, stream: IO, f: F) -> io::Result>
    where
    IO: AsyncRead + AsyncWrite + Unpin,
    F: FnOnce(&mut ServerConnection),
    {
    if let Some(a) = &self.accepter {
    Ok(a.accept_with(stream, f))
    } else {
    self.check_and_request_cert()
    .map_err(|_| io::Error::new(io::ErrorKind::Other, "load https error"))?;
    Err(io::Error::new(io::ErrorKind::Other, "try next https error"))
    }
    }

    accepter未初始化时,我们将会试图检查证书,查看是否能签发证书。

    此处我们为了避免并发中,重复多次请求导致请求数过多导致的服务不可用,我们此处定义了全局静态变量。

    lazy_static! {
    static ref CACHE_REQUEST: MutexString, Instant>> = Mutex::new(HashMap::new());
    }

    在检查的时候,我们只允许一段时间内仅有一个请求进入申请证书的流程,其它的请求全部返回错误:

    let mut map = CACHE_REQUEST
    .lock()
    .map_err(|_| io::Error::new(io::ErrorKind::Other, "Fail get Lock"))?;
    if let Some(last) = map.get(self.domain.as_ref().unwrap()) {
    if last.elapsed() < Duration::from_secs(30) {
    return Err(io::Error::new(io::ErrorKind::Other, "等待上次请求结束").into());
    }
    }
    map.insert(self.domain.clone().unwrap(), Instant::now());

    然后我们对该域名发起证书签名请求,此处我们会循环卡住整个线程,而非异步的请求,所以我们这里用了thread::spawn而非tokio::spawn

    let obj = self.clone();
    thread::spawn(move || {
    let _ = obj.request_cert();
    });

    以下是请求证书的函数:

    fn request_cert(&self) -> Result<(), Error> {
    // 使用let's encrypt签发证书
    let url = DirectoryUrl::LetsEncrypt;
    let path = Path::new(".well-known/acme-challenge");
    if !path.exists() {
    let _ = std::fs::create_dir_all(path);
    }
    // 使用内存的存储结构,存储自己做处理
    let persist = MemoryPersist::new();
    // 创建目录节点
    let dir = Directory::from_url(persist, url)?;
    // 设置请求的email信息
    let acc = dir.account("wmproxy@wmproxy.net")?;
    // 请求签发的域名
    let mut ord_new = acc.new_order(&self.domain.clone().unwrap_or_default(), &[])?;
    let start = Instant::now();
    // 以下域名的鉴权,需要等待let's encrypt确认信息
    let ord_csr = loop {
    // 成功签发,跳出循环
    if let Some(ord_csr) = ord_new.confirm_validations() {
    break ord_csr;
    }
    // 超时30秒,认为失败了
    if start.elapsed() > Duration::from_secs(30) {
    println!("获取证书超时");
    return Ok(());
    }
    // 获取鉴权方式
    let auths = ord_new.authorizations()?;
    // 以下是HTTP的请求方法,本质上是请求token的url,然后返回正确的值
    // 此处我们用的是临时服务器
    //
    // /var/www/.well-known/acme-challenge/
    //
    // http://mydomain.io/.well-known/acme-challenge/
    let chall = auths[0].http_challenge();
    // 将token存储在目录下
    let token = chall.http_token();
    let path = format!(".well-known/acme-challenge/{}", token);
    // 获取token的内容
    let proof = chall.http_proof();
    Helper::write_to_file(&path, proof.as_bytes())?;
    // 等待acme检测时间,以ms计
    chall.validate(5000)?;
    // 再尝试刷新acme请求
    ord_new.refresh()?;
    };
    // 创建rsa的密钥对
    let pkey_pri = create_rsa_key(2048);
    // 提交CSR获取最终的签名
    let ord_cert = ord_csr.finalize_pkey(pkey_pri, 5000)?;
    // 下载签名及证书,此时下载下来的为pkcs#8证书格式
    let cert = ord_cert.download_and_save_cert()?;
    Helper::write_to_file(
    &self.get_cert_path().unwrap(),
    cert.certificate().as_bytes(),
    )?;
    Helper::write_to_file(&self.get_key_path().unwrap(), cert.private_key().as_bytes())?;
    Ok(())
    }

    在其中,我们跟acme服务器的时候我们需要架设临时文件服务器以使acme访问我们http服务器的时候http://mydomain.io/.well-known/acme-challenge/能正确的返回正常的请求,我们将在绑定tls的时候,如果没有该证书信息时,我们将自动添加一个.well-known/acme-challenge的location以启用https的验证:

    pub async fn bind(
    &mut self,
    ) -> ProxyResult<(Vec<Option>, Vec<bool>, Vec)> {
    // ...
    for value in &mut self.server {
    // ...
    if has_acme {
    let mut location = LocationConfig::new();
    let file_server = FileServer::new(
    ".well-known/acme-challenge".to_string(),
    "/.well-known/acme-challenge".to_string(),
    );
    location.rule = Matcher::from_str("/.well-known/acme-challenge/").expect("matcher error");
    location.file_server = Some(file_server);
    value.location.insert(0, location);
    }
    }
    Ok((accepters, tlss, listeners))
    }

    以启用远程acme能访问该链接的能力,也就意味着我们不能将敏感信息放置在".well-known/acme-challenge"目录下面,也就是我们使用MemoryPersist的原因。

    测试是否可行

    因为http-01的方式必须使acme能访问我们的服务器,所以此时测试需要公网环境下进行测试:
    我们配置如下文件,reverse.toml:

    # 反向代理相关,七层协议为http及https
    [http]
    # 反向代理中的具体服务,可配置多个多组
    [[http.server]]
    bind_addr = "0.0.0.0:80"
    bind_ssl = "0.0.0.0:443"
    up_name = "auto1.wmproxy.net"
    root = ""
    [[http.server.location]]
    rule = "/"
    static_response = "I'm Ok {client_ip}"

    此时布置在我们的auto1.wmproxy.net的服务器上,我们运行

    wmproxy run -c reverse.toml

    此时当我们访问https://auto1.wmproxy.net的请求的时候,将会触发证书申请,成功后证书将放置在".well-known"下面,下次启动服务器的时候我们将自动加载已请求的tls证书以提供https服务。

    频繁限制问题

    在let's encrypt中,如果有早过5次成功后,需要2天后才能继续申请,他将无限返回429,得注意控制申请证书的频率。

    总结

    TLS证书在当今互联网中处于最重要的一环,他保护着我们的隐私数据的安全,也是最流行的加密方式之一。所以TLS证书的快速部署对于小而美的应用能让其快速的落地使用。

    点击 [关注][在看][点赞] 是对作者最大的支持

  • 相关阅读:
    学习C++图像处理最快最好的途径
    COSCon'22第七届中国开源年会火热筹备中,第一波赞助伙伴已集结,一起上车共赴开源盛宴吧~...
    python实验课7~8---面向对象
    高薪程序员&面试题精讲系列134之微服务网关有哪些限流算法?如何实现限流?
    【每日一题】1261. 在受污染的二叉树中查找元素-2024.3.12
    Oracle主机变量锚定、游标变量
    MEM 英语备考第一篇
    JDBC:批量插入
    触摸屏与施耐德PLC之间MODBUS无线通讯
    CSS transform
  • 原文地址:https://www.cnblogs.com/wmproxy/p/18033496/wmproxy50