diff --git a/.gitignore b/.gitignore index 9f8811c..7729ce5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,4 @@ yarn-error.log* my-website .vscode .idea -lib \ No newline at end of file +lib diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index a9d7db9..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# GitHub Copilot persisted chat sessions -/copilot/chatSessions diff --git a/docs/blog.md b/docs/blog.md index 6be0019..6c23a7e 100644 --- a/docs/blog.md +++ b/docs/blog.md @@ -4,8 +4,6 @@ hide_title: true sidebar_position: 1 --- -# BLOG - 好记性不如烂笔头,自从工作以后写得越来越少了,在大学时还时不时的写点,现在回顾起来,感觉不到工作后的任何积累,写作水平也比以前差了不少,实在惭愧。 自前段时间看了大佬们的博客,下定决心重新开写,重新整理了一下过往的文章,~~黑历史去掉~~,然后重新挑选了一个博客程序,你感兴趣的话可以看看[blog](https://github.com/codfrm/blog)仓库,这个仓库之前也有一些代码记录,已经移动到了[archive](https://github.com/CodFrm/blog/tree/main/archive)目录下。 diff --git a/docs/dev/backend/acme协议.md b/docs/dev/backend/acme协议.md new file mode 100644 index 0000000..592d721 --- /dev/null +++ b/docs/dev/backend/acme协议.md @@ -0,0 +1,469 @@ +# ACME 协议流程详解与实现 + +ACME 协议全名为 “Automatic Certificate Management Environment”,[RFC 8555](https://datatracker.ietf.org/doc/html/rfc8555), +意为自动证书管理环境。它是一个由 IETF 制定的协议,用于自动化证书颁发和更新过程。 + +在了解 ACME 之前,我们先来了解一下一些相关的名词: + +- **CA**:Certificate Authority,证书颁发机构,负责颁发数字证书。 +- **CSR**:Certificate Signing Request,证书签名请求,是由申请者生成的包含公钥的数据块,用于向 CA 申请数字证书。 +- **CRT**:Certificate,证书文件,通常使用.crt 扩展名,可以是 PEM 或 DER 格式。.crt 文件包含公钥并由证书颁发机构(CA)签发,用于身份验证和数据加密。 +- **PEM**:Privacy-Enhanced Mail,一种证书文件格式,可用于存储证书(.crt、.pem)、证书请求(.csr)和私钥(.key)。 +- **DER**:Distinguished Encoding Rules,一种证书文件格式,可用于存储证书(.cer、.der)。 +- **DV 证书**:Domain Validation Certificate,域名验证证书,证书颁发机构验证申请者对域名的控制权后颁发的证书。 +- **OV 证书**:Organization Validation Certificate,组织验证证书,除了验证域名所有权外,CA 还验证申请组织的身份。 +- **EV 证书**:Extended Validation Certificate,扩展验证证书,提供最高级别的验证,包括域名所有权、组织信息以及组织的实体存在性。 + +ACME 协议通过 HTTPS 与 JSON 来进行通信,它定义了一套标准的 API,用于客户端与证书颁发机构之间通信,实现证书的自动化颁发和更新。 + +## ACME 提供商 + +常见的免费的 ACME 提供商有: + +- [Let's Encrypt](https://letsencrypt.org/) +- [ZeroSSL](https://zerossl.com/) + +大多数系统都是支持的 Let's Encrypt,因此我们这里通过 Let's Encrypt 来进行介绍。 + +Let’s Encrypt 提供域名验证型(DV)证书。不提供组织验证(OV)或扩展验证(EV),OV 和 EV 证书一般需要验证真实实体才能颁发,不能做到自动化的颁发,自动化也会削弱这种信任。[Introduction](https://datatracker.ietf.org/doc/html/rfc8555#section-1) + +## ACME 协议流程 + +ACME 协议的流程如下: + +### 注册账户 + +> [Account Management](https://datatracker.ietf.org/doc/html/rfc8555#autoid-28) + +通过`new-account`向 ACME 服务商注册账户,获取账户的唯一标识符(在发送请求时的 kid 字段)。有的 ACME 提供商可能还需要验证邮箱等信息,需要先注册 CA 账户,然后在请求 ACME 注册账户时携带上指定信息。 + +### 创建订单 + +> [Applying for Certificate Issuance](https://datatracker.ietf.org/doc/html/rfc8555#autoid-35) + +通过`new-order`向 ACME 服务商创建一个新的订单,指定需要颁发证书的域名。如果通过,此时处于`pending`状态,ACME 服务商会返回一个订单标识符(finalize)和授权(authorizations),用于后续的操作。 + +### 验证域名 + +> [Identifier Authorization](https://datatracker.ietf.org/doc/html/rfc8555#section-7.5) + +> [DNS Challenge](https://datatracker.ietf.org/doc/html/rfc8555#section-8.4) + +在获取到 ACME 服务商返回的授权信息后,此时还只是处于`pending`,后续需要验证域名的所有权。此时我们请求上一步返回的`authorizations`URL,ACME 服务商会返回一个验证信息(challenges),客户端需要根据这个信息来验证域名的所有权。一般情况下会有`http-01`和`dns-01`两种验证方式。 + +我们只介绍`dns-01`的方式。 + +#### DNS-01 验证 + +> [Key Authorizations](https://datatracker.ietf.org/doc/html/rfc8555#section-8.1) + +我们需要在 DNS 中添加一条 TXT 记录,记录名为 `_acme-challenge`加上域名,例如:`docs.scriptcat.org`那么记录名为 `_acme-challenge.docs.scriptcat.org`。 + +但在 dns 管理平台中请注意,添加的 TXT 记录名为:`_acme-challenge.docs`,你可以使用命令:`dig -t txt _acme-challenge.docs.scriptcat.org`来查看是否添加成功。 + +记录值需要计算你的账户指纹,然后与返回的 token 一起组成一个字符串,然后对这个字符串进行 SHA256 哈希,然后 base64 编码,最后得到的值就是记录值,十分复杂。。。。 + +keyAuthorization = token || '.' || base64url(Thumbprint(accountKey)) + +这里我们是用的 ES256 算法,所以 Thumbprint 就是对公钥进行 SHA256 哈希,然后 base64 编码。 + +```go +func ES256Jwk(publicKey ecdsa.PublicKey) string { + return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`, + "P-256", + base64.RawURLEncoding.EncodeToString(publicKey.X.Bytes()), + base64.RawURLEncoding.EncodeToString(publicKey.Y.Bytes()), + ) +} + +func (c *Client) thumbprint() string { + sha256Bytes := sha256.Sum256([]byte(ES256Jwk(c.options.privateKey.PublicKey))) + return base64.RawURLEncoding.EncodeToString(sha256Bytes[:]) +} + +func (c *Client) keyAuthorization(token string) string { + return token + "." + c.thumbprint() +} + +func (c *Client) DNS01ChallengeRecord(token string) string { + hash := sha256.Sum256([]byte(c.keyAuthorization(token))) + return base64.RawURLEncoding.EncodeToString(hash[:]) +} +``` + +### 请求验证 + +> [Responding to Challenges](https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1) + +当我们配置好`challenge`后,我们需要先请求一次`challenges`URL,发送一个`{}`空对象来通知 ACME 服务器已经准备好挑战了,我们可以轮训`authorizations`URL 来查看验证状态,也可以轮询`challenges`URL,更推荐`authorizations`URL,如果验证成功,那么我们的订单就会变成`valid`状态,这时候我们也可以将 dns 等记录删除了。 + +当变为`valid`状态后,我们就可以请求证书了。 + +### 请求颁发证书 + +> [Applying for Certificate Issuance](https://datatracker.ietf.org/doc/html/rfc8555#section-7.4) + +当我们的订单变成`valid`状态后,我们就可以请求颁发证书了,通过`finalize`URL 来请求颁发证书,ACME 服务商会返回我们的证书。其中必须要携带上我们的 CSR 信息。 + +申请成功后,会返回一个`certificate`URL + +### 下载证书 + +> [Downloading the Certificate](https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2) + +当我们请求颁发证书成功后,我们就可以通过`certificate`URL 来下载我们的证书了。然后就可以部署到我们的服务器上了。 + +## ACME 协议的实现 + +> [Request Authentication](https://datatracker.ietf.org/doc/html/rfc8555#section-6.2) + +> [JWS RFC7515](https://datatracker.ietf.org/doc/html/rfc7515) + +ACME 使用 jws(JSON Web Signature)来进行签名,但是 ACME 依赖非对称算法,所以请注意不能使用`HS256`,RFC 中推荐使用`ES256`,我们可以先实现一个 JWS 的签名和验证的工具类。 + +在这里我就不详细展开了,可以看看我的 acme 包中的 jws 实现: [algorithm_test.go](https://github.com/CodFrm/dns-kit/blob/6dcac9b084a8487188af9eb58c5e411b489cedbe/pkg/jws/algorithm_test.go) + +这里为 acme 的请求做了一层封装,方便使用: + +```go +func (c *Client) newRequest(url string, payload any) (*http.Request, error) { + nonce, err := c.NewNonce() + if err != nil { + return nil, err + } + // 注册账户需要签名 + if c.options.privateKey == nil { + return nil, ErrPrivateKeyNotFound + } + var header *jws.Header + // 如果有kid则使用kid签名 + if c.options.kid != "" { + header = jws.NewHeader(newEs256(c.options.kid, c.options.privateKey)) + } else { + header = jws.NewHeader(jws.ES256(c.options.privateKey)) + } + data, err := jws.Encode(header.Set("nonce", nonce).Set("url", url), + payload, jws.WithSerialization(jws.JSONSerialization)) + if err != nil { + return nil, err + } + req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer([]byte(data))) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/jose+json") + return req, nil +} + +func (c *Client) do(url string, payload any) ([]byte, *http.Response, error) { + req, err := c.newRequest(url, payload) + if err != nil { + return nil, nil, err + } + resp, err := c.options.httpClient.Do(req) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, err + } + return body, resp, nil +} +``` + +### 获取目录 + +首先我们需要通过`GET`请求来获取 ACME 服务商的目录信息,目录信息中包含了 ACME 服务商的一些信息,例如:`new-account`、`new-order`等等。 + +我们以 Let's Encrypt 为例,其中 DirectoryUrl 为`https://acme-v02.api.letsencrypt.org/directory`: + +```go +type Directory struct { + NewNonce string `json:"newNonce"` + NewAccount string `json:"newAccount"` + NewOrder string `json:"newOrder"` + NewAuthz string `json:"newAuthz"` + RevokeCert string `json:"revokeCert"` + KeyChange string `json:"keyChange"` + Meta struct { + TermsOfService string `json:"termsOfService"` + Website string `json:"website"` + CaaIdentities []string `json:"caaIdentities"` + ExternalAccountRequired bool `json:"externalAccountRequired"` + } `json:"meta"` +} + +func (c *Client) GetDirectory() (*Directory, error) { + // 请求目录 + req, err := http.NewRequest(http.MethodGet, c.options.directoryUrl, nil) + if err != nil { + return nil, err + } + resp, err := c.options.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + directory := &Directory{} + // 解析目录 + if err := json.NewDecoder(resp.Body).Decode(directory); err != nil { + return nil, err + } + c.options.directory = directory + return directory, nil +} +``` + +### 注册账户 + +此处对应的是注册账户,我们需要在请求中携带上我们的邮箱信息,ACME 服务商会返回一个账户的唯一标识符(kid)。我们需要保存这个 kid 和私钥,后续的请求都需要携带上这个 kid,然后用私钥来签名。 + +注册账户请求需要带上 jws 信息,在`jws.ES256.PreCompute`中有对应实现 + +```go +func (c *Client) NewAccount(contact []string) (string, error) { + body, resp, err := c.do(c.options.directory.NewAccount, map[string]interface{}{ + "termsOfServiceAgreed": true, + "contact": contact, + }) + if err != nil { + return "", err + } + if resp.StatusCode != http.StatusCreated { + return "", fmt.Errorf("NewAccount failed: %s", body) + } + if resp.Header.Get("Location") == "" { + return "", fmt.Errorf("location not found: %s", body) + } + return resp.Header.Get("Location"), nil +} +``` + +### 创建订单 + +此步骤对应创建订单,我们需要在请求中携带上我们的域名信息,ACME 服务商会返回一个订单的标识符(finalize)和授权(authorizations)。 + +```go +type Identifiers struct { + Type string `json:"type"` + Value string `json:"value"` +} + +type NewOrderResponse struct { + Status string `json:"status"` + Expires time.Time `json:"expires"` + NotBefore time.Time `json:"notBefore"` + NotAfter time.Time `json:"notAfter"` + Identifiers []struct { + Type string `json:"type"` + Value string `json:"value"` + } `json:"identifiers"` + Authorizations []string `json:"authorizations"` + Finalize string `json:"finalize"` +} + +func (c *Client) NewOrder(identifiers []*Identifiers) (*NewOrderResponse, error) { + body, resp, err := c.do(c.options.directory.NewOrder, map[string]interface{}{ + "identifiers": identifiers, + }) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusCreated { + return nil, fmt.Errorf("NewOrder failed: %s", body) + } + order := &NewOrderResponse{} + if err := json.Unmarshal(body, order); err != nil { + return nil, err + } + return order, nil +} +``` + +### 获取授权 + +在上一步创建成功订单后,ACME 会返回一个授权(authorizations)URL,我们需要请求这个 URL 来获取授权信息(challenges),然后我们需要根据这个信息来验证域名的所有权。 + +```go +type AuthorizationResponse struct { + Identifier struct { + Type string `json:"type"` + Value string `json:"value"` + } `json:"identifier"` + Status string `json:"status"` + Expires time.Time `json:"expires"` + Challenges []struct { + Type string `json:"type"` + Status string `json:"status"` + Url string `json:"url"` + Token string `json:"token"` + } `json:"challenges"` +} + +func (c *Client) GetAuthorization(url string) (*AuthorizationResponse, error) { + body, resp, err := c.do(url, nil) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GetAuthorization failed: %s", body) + } + auth := &AuthorizationResponse{} + if err := json.Unmarshal(body, auth); err != nil { + return nil, err + } + return auth, nil +} +``` + +### 应对挑战 + +在上一步获取到授权信息后,我们需要根据这个信息来验证域名的所有权,我们需要在 DNS 中添加一条 TXT 记录,记录名为 `_acme-challenge`加上域名。 + +例如:`docs.scriptcat.org`那么记录名为 `_acme-challenge.docs.scriptcat.org`。 + +```go +func (c *Client) thumbprint() string { + sha256Bytes := sha256.Sum256([]byte(jws.ES256Jwk(c.options.privateKey.PublicKey))) + return base64.RawURLEncoding.EncodeToString(sha256Bytes[:]) +} + +func (c *Client) keyAuthorization(token string) string { + return token + "." + c.thumbprint() +} + +func (c *Client) ChallengeRecord(token string) string { + hash := sha256.Sum256([]byte(c.keyAuthorization(token))) + return base64.RawURLEncoding.EncodeToString(hash[:]) +} +``` + +dns 记录设置好了之后,我们需要请求一次`challenges`URL,发送一个`{}`空对象来通知 ACME 服务器已经准备好挑战了,然后我们可以轮询`authorizations`URL 来查看验证状态,如果验证成功,那么我们的订单就会变成`valid`状态,这时候我们也可以将 dns 等记录删除了。 + +```go +type ChallengeResponse struct { + Type string `json:"type"` + Status string `json:"status"` + Url string `json:"url"` + Token string `json:"token"` + ValidationRecord []struct { + Hostname string `json:"hostname"` + ResolverAddrs []string `json:"resolverAddrs"` + } `json:"validationRecord"` + Validated time.Time `json:"validated"` +} + +// GetChallenge 获取挑战 +func (c *Client) GetChallenge(url string) (*ChallengeResponse, error) { + body, resp, err := c.do(url, nil) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GetChanllenge failed: %s", body) + } + challenge := &ChallengeResponse{} + if err := json.Unmarshal(body, challenge); err != nil { + return nil, err + } + return challenge, nil +} + +// RequestChallenge 请求挑战 +// 当你当http-01/dns-01记录准备好后,调用此接口 +// 然后使用GetChallenge或者GetAuthorization轮询查看状态 +func (c *Client) RequestChallenge(url string) (*ChallengeResponse, error) { + body, resp, err := c.do(url, "{}") + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("RequestChallenge failed: %s", body) + } + challenge := &ChallengeResponse{} + if err := json.Unmarshal(body, challenge); err != nil { + return nil, err + } + return challenge, nil +} +``` + +### 请求颁发证书 + +请求颁发证书需要先生成一个 CSR 信息,然后请求`finalize`URL,ACME 服务商会返回我们的证书。其中必须要携带上我们的 CSR 信息。 + +下载下来的证书是一个 PEM 格式的证书,我们可以通过`x509.ParseCertificate`来解析证书,然后就可以使用了。 + +```go + +type FinalizeResponse struct { + Status string `json:"status"` + Expires time.Time `json:"expires"` + Identifiers []struct { + Type string `json:"type"` + Value string `json:"value"` + } `json:"identifiers"` + Authorizations []string `json:"authorizations"` + Finalize string `json:"finalize"` + Certificate string `json:"certificate"` +} + +func (c *Client) Finalize(url string, csr []byte) (*FinalizeResponse, error) { + body, resp, err := c.do(url, map[string]interface{}{ + "csr": base64.RawURLEncoding.EncodeToString(csr), + }) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("FinalizeOrder failed: %s", body) + } + finalize := &FinalizeResponse{} + if err := json.Unmarshal(body, finalize); err != nil { + return nil, err + } + return finalize, nil +} + +func CreateCertificateRequest(auth []*Identifiers) ([]byte, *ecdsa.PrivateKey, error) { + csr := x509.CertificateRequest{} + for _, v := range auth { + switch v.Type { + case "dns": + csr.DNSNames = append(csr.DNSNames, v.Value) + } + } + k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + b, err := x509.CreateCertificateRequest(rand.Reader, &csr, k) + if err != nil { + return nil, nil, err + } + return b, k, nil +} + +func (c *Client) GetCertificate(url string) ([]byte, error) { + body, resp, err := c.do(url, nil) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GetCertificate failed: %s", body) + } + return body, nil +} +``` + +## 测试 + +我们可以写一个方法来测试一下上面的流程是否可行,代码我就不贴这里了,大家可以到这里查看:[acme_test.go](https://github.com/CodFrm/dns-kit/blob/6dcac9b084a8487188af9eb58c5e411b489cedbe/pkg/acme/acme_test.go) + +## 结尾 + +其中也参考了官方写的`acme`包:golang.org/x/crypto/acme,虽然已经有很多现成的包可以使用,也有很多文档,但是理论和实践差距还是挺大的,其中很多坑还是得自己趟一遍才能理解。 + +这次实现又接触到了不少东西:CSR、JWS、JWK、ES256、x509 等等,大家有兴趣也可以再深入一下,有空我也再写几篇,整理一下这些内容。 diff --git a/docs/dev/language/golang/go-io.md b/docs/dev/language/golang/io包.md similarity index 100% rename from docs/dev/language/golang/go-io.md rename to docs/dev/language/golang/io包.md diff --git a/docs/dev/language/golang/go-slice.md b/docs/dev/language/golang/切片.md similarity index 100% rename from docs/dev/language/golang/go-slice.md rename to docs/dev/language/golang/切片.md diff --git a/docs/note/learn/linux-aio-异步io.md b/docs/note/learn/linux-aio-异步io.md index 1795274..e6ef622 100644 --- a/docs/note/learn/linux-aio-异步io.md +++ b/docs/note/learn/linux-aio-异步io.md @@ -2,11 +2,14 @@ title: linux aio 异步io --- -> linux 下的 aio 有 glibc 的和内核所提供的,glibc 是使用的多线程的模式模拟的,另外一种是真正的内核异步通知了,已经使用在了 nginx 上,前面看了一下 swoole 的实现,是类似与 glibc 那种多线程的模式。不过两种方法都有一定的毛病,多线程模拟自然是有所效率损失,然而内核不能利用系统的缓存,只能以 O_DIRECT 方式做直接 IO,所以看知乎上有一个**linux 下的异步 IO(AIO)是否已成熟?**的问题,不过那是 2014 年的事情了,不知道现在怎么样。 +> linux 下的 aio 有 glibc 的和内核所提供的,glibc 是使用的多线程的模式模拟的,另外一种是真正的内核异步通知了, +> 已经使用在了 nginx 上,前面看了一下 swoole 的实现,是类似与 glibc 那种多线程的模式。 +> 不过两种方法都有一定的毛病,多线程模拟自然是有所效率损失,然而内核不能利用系统的缓存,只能以 O_DIRECT 方式做直接 IO,所以看知乎上有一个 +> **linux 下的异步 IO(AIO)是否已成熟?** 的问题,不过那是 2014 年的事情了,不知道现在怎么样。 在此之前需要安装好 **libaio** -``` +```bash sudo apt install libaio-dev ``` diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 80563d3..b8b685d 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -145,7 +145,7 @@ const config: Config = { theme: prismThemes.github, darkTheme: prismThemes.dracula, // https://prismjs.com/download.html - additionalLanguages: ["php", "bash", "docker"], + additionalLanguages: ["php", "bash", "docker", "json"], }, } satisfies Preset.ThemeConfig, };