关于Let’s Encrypt证书
官网的说明是
一个为 2.25 亿个网站提供 TLS 证书的非盈利性证书颁发机构。
官网 : https://letsencrypt.org/zh-cn/
获取证书的验证方式
目前有很多自动工具可以获取,需要有相关的域名或者服务器权限。认证的方式有三种,这里引用下官网的说明。验证方式
当您从 Let’s Encrypt 获得证书时,我们的服务器会验证您是否使用 ACME 标准定义的验证方式来验证您对证书中域名的控制权。 大多数情况下,验证由 ACME 客户端自动处理,但如果您需要做出一些更复杂的配置决策,那么了解更多有关它们的信息会很有用。 如果您不确定怎么做,请使用您的客户端的默认设置或使用 HTTP-01。
- HTTP-01 验证 这是当今最常见的验证方式。 Let’s Encrypt 向您的 ACME 客户端提供一个令牌,然后您的 ACME 客户端将在您对 Web 服务器的 http://<你的域名>/.well-known/acme-challenge/(用提供的令牌替换 )路径上放置指定文件。 该文件包含令牌以及帐户密钥的指纹。 一旦您的 ACME 客户端告诉 Let’s Encrypt 文件已准备就绪,Let’s Encrypt 会尝试获取它(可能从多个地点进行多次尝试)。 如果我们的验证机制在您的 Web 服务器上找到了放置于正确地点的正确文件,则该验证被视为成功,您可以继续申请颁发证书。 如果验证检查失败,您将不得不再次使用新证书重新申请。
我们的 HTTP-01 验证最多接受 10 次重定向。 我们只接受目标为“http:”或“https:”且端口为 80 或 443 的重定向。 我们不目标为 IP 地址的重定向。 当被重定向到 HTTPS 链接时,我们不会验证证书是否有效(因为验证的目的是申请有效证书,所以它可能会遇到自签名或过期的证书)。
HTTP-01 验证只能使用 80 端口。 因为允许客户端指定任意端口会降低安全性,所以 ACME 标准已禁止此行为。
优点:
它可以轻松地自动化进行而不需要关于域名配置的额外知识。 它允许托管服务提供商为通过 CNAME 指向它们的域名颁发证书。 它适用于现成的 Web 服务器。 缺点:
如果您的 ISP 封锁了 80 端口,该验证将无法正常工作(这种情况很少见,但一些住宅 ISP 会这么做)。 Let’s Encrypt 不允许您使用此验证方式来颁发通配符证书。 您如果有多个 Web 服务器,则必须确保该文件在所有这些服务器上都可用。
- DNS-01 验证 此验证方式要求您在该域名下的 TXT 记录中放置特定值来证明您控制域名的 DNS 系统。 该配置比 HTTP-01 略困难,但可以在某些 HTTP-01 不可用的情况下工作。 它还允许您颁发通配符证书。 在 Let’s Encrypt 为您的 ACME 客户端提供令牌后,您的客户端将创建从该令牌和您的帐户密钥派生的 TXT 记录,并将该记录放在 _acme-challenge.<YOUR_DOMAIN> 下。 然后 Let’s Encrypt 将向 DNS 系统查询该记录。 如果找到匹配项,您就可以继续颁发证书!
由于颁发和续期的自动化非常重要,只有当您的 DNS 提供商拥有可用于自动更新的 API 时,使用 DNS-01 验证方式才有意义。 我们的社区在此处提供了此类 DNS 提供商的列表。 您的 DNS 提供商可能与您的域名注册商(您从中购买域名的公司)相同或不同。 如果您想更改 DNS 提供商,只需在注册商处进行一些小的更改, 无需等待域名即将到期。
请注意,将完整的 DNS API 凭据放在 Web 服务器上会显着增加该服务器被黑客攻击造成的影响。 最佳做法是使用权限范围受限的 API 凭据,或在单独的服务器上执行 DNS 验证并自动将证书复制到 Web 服务器上。
由于 Let’s Encrypt 在查找用于 DNS-01 验证的 TXT 记录时遵循 DNS 标准,因此您可以使用 CNAME 记录或 NS 记录将验证工作委派给其他 DNS 区域。 这可以用于将 _acme-challenge 子域名委派给验证专用的服务器或区域。 如果您的 DNS 提供商更新速度很慢,那么您也可以使用此方法把验证工作委派给更新速度更快的服务器。
大多数 DNS 提供商都有一个“更新时间”,它反映了从更新 DNS 记录到其在所有服务器上都可用所需的时间。 这个时间可能很难测量,因为这些提供商通常也使用任播,这意味着多个服务器可以拥有相同的 IP 地址,并且根据您在世界上的位置,您和 Let’s Encrypt 可能会与不同的服务器通信(并获得不同的应答)。 最好的情况是 DNS API 为您提供了自动检查更新是否完成的方法。 如果您的 DNS 提供商没有这样的方法,您只需将客户端配置为等待足够长的时间(通常多达一个小时),以确保在触发验证之前更新已经完全完成。
您可以为同一名称提供多个 TXT 记录。 例如,如果您同时验证通配符和非通配符证书,那么这种情况可能会发生。 但是,您应该确保清理旧的 TXT 记录,因为如果响应大小太大,Let’s Encrypt 将拒绝该记录。
优点:
您可以使用此验证方式来颁发包含通配符域名的证书。 即使您有多个 Web 服务器,它也能正常工作。 缺点:
在 Web 服务器上保留 API 凭据存在风险。 您的 DNS 提供商可能不提供 API。 您的 DNS API 可能无法提供有关更新时间的信息。 TLS-SNI-01验证 ACME 的草案版本中定义了这一验证方式。 它在 443 端口上进行 TLS 握手,并发送了一个特定的 [SNI] 标头以获取包含令牌的证书。 由于安全原因,该验证已于 2019 年 3 月禁用。
- TLS-ALPN-01验证 这一验证类型是在 TLS-SNI-01 被弃用后开发的,并且已经开发为单独的标准。 与 TLS-SNI-01 一样,它通过 443 端口上的 TLS 执行。 但是,它使用自定义的 ALPN 协议来确保只有知道此验证类型的服务器才会响应验证请求。 这还允许对此质询类型的验证请求使用与要验证的域名匹配的SNI字段,从而使其更安全。
这一验证类型并不适合大多数人。 它最适合那些想要执行类似于 HTTP-01 的基于主机的验证,但希望它完全在 TLS 层进行以分离关注点的 TLS 反向代理的作者。 现在其主要使用者为大型托管服务提供商,但 Apache 和 Nginx 等主流 Web 服务器有朝一日可能会实现对其的支持(Caddy已经支持了这一验证类型)。
优点:
它在 80 端口不可用时仍可以正常工作。 它可以完全仅在 TLS 层执行。 缺点:
它不支持 Apache、Nginx 和 Certbot,且很可能短期内不会兼容这些软件。 与 HTTP-01 一样,如果您有多台服务器,则它们需要使用相同的内容进行应答。 此方法不能用于验证通配符域名。
七牛的证书申请
目前七牛支持两种证书设置方式。自行购买和上传。自行购买需要购买七牛提供的证书,目前只有一个一年的免费。后面不清楚什么时候就要开始收费。一般自行购买的证书需要进行验证。我们可以选择文件验证。下面是一个go的例子。
package main
import (
"bytes"
"context"
"fmt"
"github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage"
)
const (
accessKey = "lUkkMTqUK-fY7t6Tbg7zq-p3iaopntRM3232kEEDW"
secretKey = "3svBHRvSBJlp0iVJMx-7urereu82mcLQPLJ1"
bucket = "golang"
challengeKey = ".well-known/pki-validation/2FA8BE94165E014EA0AE3F664EF548E8.txt"
challengeContent = `88518DD627533E6481D735B37C1BF258DCAF3ECA120CC86ED49A243446CB7D0B
trust-provider.com
TTDzhPt2m4`
)
func main() {
putPolicy := storage.PutPolicy{
Scope: bucket,
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
cfg := storage.Config{}
// 空间对应的机房
cfg.Region = &storage.ZoneXinjiapo
// 是否使用https域名
cfg.UseHTTPS = true
// 上传是否使用CDN上传加速
cfg.UseCdnDomains = false
formUploader := storage.NewFormUploader(&cfg)
ret := storage.PutRet{}
putExtra := storage.PutExtra{
Params: map[string]string{
"x:name": challengeKey,
},
}
data := []byte(challengeContent)
dataLen := int64(len(data))
err := formUploader.Put(context.Background(),
&ret, upToken, challengeKey,
bytes.NewReader(data),
dataLen,
&putExtra)
if err != nil {
fmt.Println(err)
}
fmt.Println(ret.Key, ret.Hash)
}
如果我们需要使用Let’s Encrypt的免费证书,我们还要配合ACME的客户端。上面的几种认证,如果本身没有DNS的权限,但是又想更新七牛证书,那么选择http-01更感觉适合,因为七牛的bucket域名是通过dns直接绑定的,同时还要配合七牛的sdk来实现证书申请的功能。
下面是一个go的代码:
package main
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage"
"log"
)
const (
accessKey = "xxxxx"
secretKey = "xxxxxx"
bucket = "golang"
domain = "golang.czyt.tech"
)
// You'll need a user or account type that implements acme.User
type MyUser struct {
Email string
Registration *registration.Resource
key crypto.PrivateKey
}
func (u *MyUser) GetEmail() string {
return u.Email
}
func (u *MyUser) GetRegistration() *registration.Resource {
return u.Registration
}
func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
return u.key
}
// 实现自定义的 challenge.Provider
type MyChallengeProvider struct {
}
func NewMyChallengeProvider() (*MyChallengeProvider, error) {
return &MyChallengeProvider{}, nil
}
func (d *MyChallengeProvider) Present(domain, token, keyAuth string) error {
_ = dns01.GetChallengeInfo(domain, keyAuth)
putPolicy := storage.PutPolicy{
Scope: bucket,
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
cfg := storage.Config{}
// 空间对应的机房
cfg.Region = &storage.ZoneXinjiapo
// 是否使用https域名
cfg.UseHTTPS = true
// 上传是否使用CDN上传加速
cfg.UseCdnDomains = false
formUploader := storage.NewFormUploader(&cfg)
ret := storage.PutRet{}
putExtra := storage.PutExtra{
Params: map[string]string{
"x:name": http01.ChallengePath(token)[1:],
},
}
data := []byte(keyAuth)
dataLen := int64(len(data))
err := formUploader.Put(context.Background(), &ret, upToken, http01.ChallengePath(token)[1:], bytes.NewReader(data), dataLen, &putExtra)
if err != nil {
fmt.Println(err)
}
fmt.Println(ret.Key, ret.Hash)
return nil
}
func (d *MyChallengeProvider) CleanUp(domain, token, keyAuth string) error {
// 清理任务,可以在这里清理掉之前上传的文件
return nil
}
func main() {
// Create a user. New accounts need an email and private key to start.
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatal(err)
}
myUser := MyUser{
Email: "[email protected]",
key: privateKey,
}
config := lego.NewConfig(&myUser)
// This CA URL is configured for a local dev instance of Boulder running in Docker in a VM.
config.CADirURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
// A client facilitates communication with the CA server.
client, err := lego.NewClient(config)
if err != nil {
log.Fatal(err)
}
dns, err := NewMyChallengeProvider()
if err != nil {
log.Fatal("init provider failed:", err)
}
err = client.Challenge.SetHTTP01Provider(dns)
if err != nil {
log.Fatal(err)
}
// 新用户需要注册
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
log.Fatal(err)
}
myUser.Registration = reg
log.Println("reg info:", *reg)
request := certificate.ObtainRequest{
Domains: []string{domain},
Bundle: true,
}
log.Println("request:", request)
certificates, err := client.Certificate.Obtain(request)
if err != nil {
log.Fatal("Obtain failed:", err)
}
// 证书返回
// 私钥和 证书相关信息. 可以在这里保存到磁盘.
fmt.Printf("%#v\n", certificates)
}
证书获取成功以后,再上传到七牛即可。上传的这部分代码,我在GitHub上找到了相关的go包:源
调用:
package qiniu
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/qiniu/api.v7/auth/qbox"
)
const APIHost = "http://api.qiniu.com"
type Client struct {
*qbox.Mac
}
func New(accessKey, secretKey string) *Client {
return &Client{
Mac: qbox.NewMac(accessKey, secretKey),
}
}
func (c *Client) Request(method string, path string, body interface{}) (resData []byte,
err error) {
urlStr := fmt.Sprintf("%s%s", APIHost, path)
reqData, _ := json.Marshal(body)
req, reqErr := http.NewRequest(method, urlStr, bytes.NewReader(reqData))
if reqErr != nil {
err = reqErr
return
}
accessToken, signErr := c.SignRequest(req)
if signErr != nil {
err = signErr
return
}
req.Header.Add("Authorization", "QBox "+accessToken)
req.Header.Add("Content-Type", "application/json")
resp, respErr := http.DefaultClient.Do(req)
if respErr != nil {
err = respErr
return
}
defer resp.Body.Close()
resData, ioErr := ioutil.ReadAll(resp.Body)
if ioErr != nil {
err = ioErr
return
}
return
}
func (c *Client) GetDomainInfo(domain string) (*DomainInfo, error) {
b, err := c.Request("GET", "/domain/"+domain, nil)
if err != nil {
return nil, err
}
info := &DomainInfo{}
if err := json.Unmarshal(b, info); err != nil {
return nil, err
}
if info.Code > 200 {
return nil, fmt.Errorf("%d: %s", info.Code, info.Error)
}
return info, nil
}
func (c *Client) GetCertInfo(certID string) (*CertInfo, error) {
b, err := c.Request("GET", "/sslcert/"+certID, nil)
if err != nil {
return nil, err
}
info := &CertInfo{}
if err := json.Unmarshal(b, info); err != nil {
return nil, err
}
if info.Code > 200 {
return nil, fmt.Errorf("%d: %s", info.Code, info.Error)
}
return info, nil
}
func (c *Client) UploadCert(cert Cert) (*UploadCertResp, error) {
b, err := c.Request("POST", "/sslcert", cert)
if err != nil {
return nil, err
}
resp := &UploadCertResp{}
if err := json.Unmarshal(b, resp); err != nil {
return nil, err
}
if resp.Code > 200 {
return nil, fmt.Errorf("%d: %s", resp.Code, resp.Error)
}
return resp, nil
}
func (c *Client) UpdateHttpsConf(domain, certID string) (*CodeErr, error) {
b, err := c.Request("PUT", "/domain/"+domain+"/httpsconf", HTTPSConf{
CertID: certID,
ForceHttps: true,
})
if err != nil {
return nil, err
}
resp := &CodeErr{}
if err := json.Unmarshal(b, resp); err != nil {
return nil, err
}
if resp.Code > 200 {
return nil, fmt.Errorf("%d: %s", resp.Code, resp.Error)
}
return resp, nil
}
func (c *Client) DeleteCert(certID string) (*CodeErr, error) {
b, err := c.Request("DELETE", "/sslcert/"+certID, nil)
if err != nil {
return nil, err
}
resp := &CodeErr{}
if err := json.Unmarshal(b, resp); err != nil {
return nil, err
}
if resp.Code > 200 {
return nil, fmt.Errorf("%d: %s", resp.Code, resp.Error)
}
return resp, nil
}
func (c *Client) DomainSSLize(domain, certID string) (*CodeErr, error) {
b, err := c.Request("PUT", "/domain/"+domain+"/sslize", HTTPSConf{
CertID: certID,
ForceHttps: true,
})
if err != nil {
return nil, err
}
resp := &CodeErr{}
if err := json.Unmarshal(b, resp); err != nil {
return nil, err
}
if resp.Code > 200 {
return nil, fmt.Errorf("%d: %s", resp.Code, resp.Error)
}
return resp, nil
}
相关结构定义
package qiniu
import (
"strconv"
"time"
)
type CodeErr struct {
Code int `json:"code"`
Error string `json:"error"`
}
type DomainInfo struct {
CodeErr
Name string `json:"name"`
PareDomain string `json:"pareDomain"`
Type string `json:"type"`
Cname string `json:"cname"`
TestURLPath string `json:"testURLPath"`
Protocol string `json:"protocol"`
Platform string `json:"platform"`
GeoCover string `json:"geoCover"`
QiniuPrivate bool `json:"qiniuPrivate"`
OperationType string `json:"operationType"`
OperatingState string `json:"operatingState"`
OperatingStateDesc string `json:"operatingStateDesc"`
CreateAt time.Time `json:"createAt"`
ModifyAt time.Time `json:"modifyAt"`
HTTPS struct {
CertID string `json:"certId"`
ForceHTTPS bool `json:"forceHttps"`
} `json:"https"`
CouldOperateBySelf bool `json:"couldOperateBySelf"`
RegisterNo string `json:"registerNo"`
}
type Cert struct {
Name string `json:"name"`
CommonName string `json:"common_name"`
CA string `json:"ca"`
Pri string `json:"pri"`
}
type UploadCertResp struct {
CodeErr
CertID string `json:"certID"`
}
type CertInfo struct {
CodeErr
Cert struct {
CertID string `json:"certid"`
Name string `json:"name"`
UID int `json:"uid"`
CommonName string `json:"common_name"`
DNSNames []string `json:"dnsnames"`
CreateTime TimeStamp `json:"create_time"`
NotBefore TimeStamp `json:"not_before"`
NotAfter TimeStamp `json:"not_after"`
OrderID string `json:"orderid"`
ProductShortName string `json:"product_short_name"`
ProductType string `json:"product_type"`
Encrypt string `json:"encrypt"`
EncryptParameter string `json:"encryptParameter"`
Enable bool `json:"enable"`
Ca string `json:"ca"`
Pri string `json:"pri"`
} `json:"cert"`
}
type TimeStamp struct {
time.Time
}
func (t *TimeStamp) UnmarshalJSON(b []byte) error {
i, err := strconv.ParseInt(string(b), 10, 64)
if err != nil {
return err
}
t.Time = time.Unix(i, 0)
return nil
}
type HTTPSConf struct {
CertID string `json:"certid"`
ForceHttps bool `json:"forceHttps"`
}