Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- package certrefresh
- // LetsEncrypt.org Client
- import (
- "bytes"
- "crypto"
- "crypto/rand"
- "crypto/rsa"
- "crypto/sha256"
- "encoding/base64"
- "encoding/json"
- "fmt"
- "io"
- "log"
- "math/big"
- "net/http"
- "sync"
- )
- // rpcUrls maps LetsEncrypt RPC names to their URLs.
- type rpcUrls struct {
- KeyChange string `json:"keyChange"`
- NewAccount string `json:"newAccount"`
- NewNonce string `json:"newNonce"`
- NewOrder string `json:"newOrder"`
- RenewalInfo string `json:"renewalInfo"`
- RevokeCert string `json:"revokeCert"`
- }
- type LetsEncrypt struct {
- dirUrl string
- mu sync.Mutex // protects the fields below
- rpcUrls *rpcUrls
- privateKey *rsa.PrivateKey
- nonce string // for use in the next LetsEncrypt RPC
- kid string // key ID in LetsEncrypt's database
- challengeToken string // for the ongoing http-01 challenge
- challengeUrl string // URL to poll while waiting for LetsEncrypt
- }
- func NewLetsEncryptClientStaging() *LetsEncrypt {
- return &LetsEncrypt{
- dirUrl: "https://acme-staging-v02.api.letsencrypt.org/directory",
- }
- }
- // get returns the body of the HTTP GET response.
- func get(url string) ([]byte, error) {
- log.Printf("HTTP GET %s", url)
- resp, err := http.Get(url)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- return io.ReadAll(resp.Body)
- }
- // head returns the headers of the HTTP HEAD response.
- func head(url string) (*http.Header, error) {
- req, err := http.NewRequest("HEAD", url, nil)
- if err != nil {
- return nil, err
- }
- client := &http.Client{}
- log.Printf("HTTP HEAD %s", url)
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
- err = resp.Body.Close()
- if err != nil {
- return nil, err
- }
- return &resp.Header, nil
- }
- // fetchEndpoints gets the RPC URLs from 'dirUrl', caching it locally.
- func (le *LetsEncrypt) fetchEndpoints() (*rpcUrls, error) {
- le.mu.Lock()
- defer le.mu.Unlock()
- return le.fetchEndpointsLocked()
- }
- // fetchEndpointsLocked is fetchEndpoints when le.mu is locked.
- func (le *LetsEncrypt) fetchEndpointsLocked() (*rpcUrls, error) {
- if le.rpcUrls != nil {
- return le.rpcUrls, nil
- }
- body, err := get(le.dirUrl)
- if err != nil {
- return nil, err
- }
- urls := &rpcUrls{}
- err = json.Unmarshal(body, urls)
- if err != nil {
- return nil, err
- }
- le.rpcUrls = urls
- return urls, nil
- }
- // fetchNonce gets a new nonce using an HTTP HEAD request.
- func (le *LetsEncrypt) fetchNonce() (string, error) {
- le.mu.Lock()
- defer le.mu.Unlock()
- if le.nonce == "" {
- urls, err := le.fetchEndpointsLocked()
- if err != nil {
- return "", err
- }
- headers, err := head(urls.NewNonce)
- if err != nil {
- return "", err
- }
- le.nonce = headers.Get("Replay-Nonce")
- }
- nonce := le.nonce
- le.nonce = ""
- return nonce, nil
- }
- // b64 Base64-encodes the string the way LetsEncrypt likes it.
- func b64(s []byte) string {
- return base64.RawURLEncoding.EncodeToString(s)
- }
- // fetchPrivateKeyLocked loads or creates the private key.
- // Assumes that le.mu is locked.
- func (le *LetsEncrypt) fetchPrivateKeyLocked() (*rsa.PrivateKey, error) {
- if le.privateKey != nil {
- return le.privateKey, nil
- }
- var err error
- le.privateKey, err = rsa.GenerateKey(rand.Reader, 2048)
- return le.privateKey, err
- }
- func (le *LetsEncrypt) fetchPrivateKey() (*rsa.PrivateKey, error) {
- le.mu.Lock()
- defer le.mu.Unlock()
- return le.fetchPrivateKeyLocked()
- }
- type jsonWebKey struct {
- Kty string `json:"kty"`
- E string `json:"e"`
- N string `json:"n"`
- }
- // keyToJwk serializes the key as a JSON web key.
- func keyToJwk(key rsa.PublicKey) *jsonWebKey {
- return &jsonWebKey {
- Kty: "RSA",
- E: b64(big.NewInt(int64(key.E)).Bytes()),
- N: b64(key.N.Bytes()),
- }
- }
- func (le *LetsEncrypt) protectedHeader(url string) (string, error) {
- nonce, err := le.fetchNonce()
- if err != nil {
- return "", err
- }
- var metadata struct {
- Alg string `json:"alg"`
- Nonce string `json:"nonce"`
- Url string `json:"url"`
- Kid string `json:"kid,omitempty"`
- Jwk *jsonWebKey `json:"jwk,omitempty"`
- }
- metadata.Alg = "RS256"
- metadata.Nonce = nonce
- metadata.Url = url
- le.mu.Lock()
- if le.kid != "" {
- metadata.Kid = le.kid
- } else {
- privateKey, err := le.fetchPrivateKeyLocked()
- if err != nil {
- le.mu.Unlock()
- return "", err
- }
- metadata.Jwk = keyToJwk(privateKey.PublicKey)
- }
- le.mu.Unlock()
- metaStr, err := json.Marshal(metadata)
- if err != nil {
- return "", err
- }
- return b64(metaStr), nil
- }
- // rpc sends an HTTP POST.
- func (le *LetsEncrypt) rpc(url string, req, resp interface{}) (*http.Header, error) {
- reqStr, err := json.Marshal(req)
- if err != nil {
- return nil, err
- }
- if req == nil {
- reqStr = nil
- }
- var rreq struct {
- Payload string `json:"payload"`
- Protected string `json:"protected"`
- Signature string `json:"signature"`
- }
- rreq.Payload = b64(reqStr)
- rreq.Protected, err = le.protectedHeader(url)
- if err != nil {
- return nil, err
- }
- sig := sha256.New()
- sig.Write([]byte(rreq.Protected))
- sig.Write([]byte("."))
- sig.Write([]byte(rreq.Payload))
- privateKey, err := le.fetchPrivateKey()
- if err != nil {
- return nil, err
- }
- ssig, err := privateKey.Sign(rand.Reader, sig.Sum(nil), crypto.SHA256)
- if err != nil {
- return nil, err
- }
- rreq.Signature = b64(ssig)
- buf, err := json.Marshal(rreq)
- if err != nil {
- return nil, err
- }
- hreq, err := http.NewRequest("POST", url, bytes.NewBuffer(buf))
- if err != nil {
- return nil, err
- }
- hreq.Header.Set("Content-Type", "application/jose+json")
- hreq.Header.Set("User-Agent", "OscarKilo CertRefresh")
- client := &http.Client{}
- log.Printf("HTTP POST %s", url)
- rresp, err := client.Do(hreq)
- defer rresp.Body.Close()
- body, err := io.ReadAll(rresp.Body)
- if err != nil {
- return &rresp.Header, err
- }
- log.Printf("--- response:\n%s", string(body)) // DEBUG
- if rresp.StatusCode/100 != 2 {
- return &rresp.Header, fmt.Errorf("StatusCode=%d, body=%s", rresp.StatusCode, string(body))
- }
- if resp != nil {
- err = json.Unmarshal(body, resp)
- if err != nil {
- return &rresp.Header, err
- }
- }
- le.mu.Lock()
- le.nonce = rresp.Header.Get("Replay-Nonce")
- le.mu.Unlock()
- return &rresp.Header, nil
- }
- func (le *LetsEncrypt) NewAccount() error {
- urls, err := le.fetchEndpoints()
- if err != nil {
- return err
- }
- var req struct {
- TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
- Contact []string `json:"contact"`
- }
- req.TermsOfServiceAgreed = true
- req.Contact = []string{"mailto:igor@shygypsy.com"}
- var resp struct {
- Key jsonWebKey `json:"key"`
- Contact []string `json:"contact"`
- InitialIp string `json:"initialIp"`
- CreatedAt string `json:"createdAt"`
- Status string `json:"status"`
- }
- headers, err := le.rpc(urls.NewAccount, req, &resp)
- if err != nil {
- return err
- }
- if resp.Status != "valid" {
- return fmt.Errorf("status=%s", resp.Status)
- }
- le.mu.Lock()
- le.kid = headers.Get("Location")
- le.mu.Unlock()
- return nil
- }
- func (le *LetsEncrypt) NewOrder() error {
- urls, err := le.fetchEndpoints()
- if err != nil {
- return err
- }
- type id struct {
- Type string `json:"type"`
- Value string `json:"value"`
- }
- var req struct {
- Identifiers []id `json:"identifiers"`
- }
- req.Identifiers = []id{
- {Type: "dns", Value: "oscarkilo.com"},
- }
- var resp struct {
- Status string `json:"status"`
- Expires string `json:"expires"`
- Identifiers []id `json:"identifiers"`
- Authorizations []string `json:"authorizations"`
- Finalize string `json:"finalize"`
- }
- _, err = le.rpc(urls.NewOrder, req, &resp)
- if err != nil {
- return err
- }
- if resp.Status != "pending" {
- return fmt.Errorf("newOrder returned status=%s", resp.Status)
- }
- if len(resp.Authorizations) != 1 {
- return fmt.Errorf("newOrder returned %v", resp)
- }
- type challenge struct {
- Type string `json:"type"`
- Status string `json:"status"`
- Url string `json:"url"`
- Token string `json:"token"`
- }
- var authResp struct {
- Identifier id `json:"identifier"`
- Status string `json:"status"`
- Expires string `json:"expires"`
- Challenges []challenge `json:"challenges"`
- }
- _, err = le.rpc(resp.Authorizations[0], nil, &authResp)
- if err != nil {
- return err
- }
- le.mu.Lock()
- le.challengeToken = ""
- challengeUrl := ""
- for _, ch := range authResp.Challenges {
- if ch.Type == "http-01" && ch.Status == "pending" {
- le.challengeToken = ch.Token
- le.challengeUrl = ch.Url
- challengeUrl = ch.Url
- break
- }
- }
- if le.challengeToken == "" {
- err = fmt.Errorf("LetsEncrypt didn't allow for an http-01 challenge")
- }
- le.mu.Unlock()
- if err != nil {
- return err
- }
- var empty struct{}
- _, err = le.rpc(challengeUrl, empty, nil)
- return err
- }
- // CheckChallengeStatus() returns the status of the challenge.
- func (le *LetsEncrypt) CheckChallengeStatus() (string, error) {
- challengeUrl := le.GetChallengeUrl()
- if challengeUrl == "" {
- return "none", nil
- }
- // DEBUG
- i := len(challengeUrl) - 1
- for challengeUrl[i] != '/' {
- i--
- }
- orderUrl := challengeUrl[:i]
- _, err := le.rpc(orderUrl, nil, nil)
- var resp struct {
- Status string `json:"status"`
- }
- var empty struct{}
- _, err = le.rpc(challengeUrl, empty, &resp)
- if err != nil {
- return "error", err
- }
- return resp.Status, nil
- }
- // GetChallengeToken returns the active http-01 challenge token, or "".
- func (le *LetsEncrypt) GetChallengeToken() string {
- le.mu.Lock()
- defer le.mu.Unlock()
- return le.challengeToken
- }
- // GetChallengeUrl returns the active http-01 challenge URL, or "".
- func (le *LetsEncrypt) GetChallengeUrl() string {
- le.mu.Lock()
- defer le.mu.Unlock()
- return le.challengeUrl
- }
- // GetJwkThumbprint returns the http-01 challenge response.
- func (le *LetsEncrypt) GetJwkThumbprint() (string, error) {
- privateKey, err := le.fetchPrivateKey()
- if err != nil {
- return "", err
- }
- jwk := keyToJwk(privateKey.PublicKey)
- jwks, err := json.Marshal(jwk)
- if err != nil {
- return "", err
- }
- sig := sha256.New()
- sig.Write(jwks)
- return b64(sig.Sum(nil)), nil
- }
Add Comment
Please, Sign In to add comment