Last active
December 26, 2018 16:36
-
-
Save tom-code/c2be17fcab211e8733ca9b20cbc2406b to your computer and use it in GitHub Desktop.
acme client v2 with http auth
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "fmt" | |
| "crypto" | |
| "crypto/ecdsa" | |
| "crypto/elliptic" | |
| "crypto/rand" | |
| "crypto/x509" | |
| "crypto/x509/pkix" | |
| "os" | |
| "io" | |
| "encoding/pem" | |
| "encoding/base64" | |
| "net/http" | |
| "encoding/json" | |
| "gopkg.in/square/go-jose.v2" | |
| "strings" | |
| "time" | |
| ) | |
| func writeKey(path string, k *ecdsa.PrivateKey) error { | |
| f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) | |
| if err != nil { | |
| return err | |
| } | |
| bytes, err := x509.MarshalECPrivateKey(k) | |
| if err != nil { | |
| return err | |
| } | |
| b := &pem.Block{Type: "EC PRIVATE KEY", Bytes: bytes} | |
| if err := pem.Encode(f, b); err != nil { | |
| f.Close() | |
| return err | |
| } | |
| return f.Close() | |
| } | |
| type Directory struct { | |
| NewAccount string | |
| NewNonce string | |
| NewOrder string | |
| } | |
| func discover(url string) *Directory { | |
| resp, err := http.Get(url) | |
| if err != nil { | |
| panic(err) | |
| } | |
| defer resp.Body.Close() | |
| if resp.StatusCode != 200 { | |
| fmt.Printf("can't get %s\n", url) | |
| fmt.Println(resp) | |
| return nil | |
| } | |
| var js struct { | |
| NewAccount string `json:"newAccount"` | |
| NewNonce string `json:"newNonce"` | |
| NewOrder string `json:"newOrder"` | |
| RevokeCert string `json:"revokeCert"` | |
| Meta struct { | |
| Terms string `json:"termsOfService"` | |
| } | |
| } | |
| decoder := json.NewDecoder(resp.Body) | |
| err = decoder.Decode(&js) | |
| if err != nil { | |
| panic(err) | |
| } | |
| return &Directory{NewAccount: js.NewAccount, NewNonce: js.NewNonce, NewOrder: js.NewOrder} | |
| } | |
| func getNonce(url string) string { | |
| resp, err := http.Get(url) | |
| if err != nil { | |
| panic(err) | |
| } | |
| resp.Body.Close() | |
| return resp.Header.Get("replay-nonce") | |
| } | |
| type Order struct { | |
| Authorizations []string | |
| FinalizeUrl string | |
| OrderUrl string | |
| } | |
| func parseOrderResponse(resp *http.Response) *Order { | |
| orderLocation := resp.Header.Get("Location") | |
| fmt.Printf("order location %s\n", orderLocation) | |
| var js struct { | |
| Identifiers []struct { | |
| Type string `json:"type"` | |
| Value string `json:"value"` | |
| } `json:"Identifiers"` | |
| Authorizations []string `json:"authorizations"` | |
| Finalize string `json:"finalize"` | |
| } | |
| decoder := json.NewDecoder(resp.Body) | |
| err := decoder.Decode(&js) | |
| if err != nil { | |
| panic(err) | |
| } | |
| return &Order {Authorizations: js.Authorizations, FinalizeUrl: js.Finalize, OrderUrl: orderLocation} | |
| } | |
| type AuthChallenge struct { | |
| Type string | |
| Url string | |
| Token string | |
| } | |
| func parseAuthzChalenge(resp *http.Response) *AuthChallenge { | |
| var js struct { | |
| Challenges []struct { | |
| Type string `json:"type"` | |
| Url string `json:"url"` | |
| Token string `json:"token"` | |
| } `json:"challenges"` | |
| } | |
| decoder := json.NewDecoder(resp.Body) | |
| err := decoder.Decode(&js) | |
| if err != nil { | |
| panic(err) | |
| } | |
| fmt.Println(js) | |
| for _, ch := range(js.Challenges) { | |
| if ch.Type == "http-01" { | |
| return &AuthChallenge{Type: ch.Type, Url: ch.Url, Token: ch.Token} | |
| } | |
| } | |
| return nil | |
| } | |
| type NS struct { | |
| nonce string | |
| } | |
| func (ns NS)Nonce()(string, error) { | |
| return ns.nonce, nil | |
| } | |
| func postJWS(key *ecdsa.PrivateKey, url string, payload string, nonce string, kid string) *http.Response { | |
| jsonWebKey := jose.JSONWebKey{ | |
| Key: key, | |
| //KeyID: kid, | |
| Algorithm: string(jose.ES256), | |
| } | |
| options := &jose.SignerOptions{} | |
| options.WithHeader("url", url) | |
| if len(kid) > 0 { | |
| options.WithHeader("kid", kid) | |
| } else { | |
| options.EmbedJWK = true | |
| } | |
| options.NonceSource = NS{nonce} | |
| signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jsonWebKey}, options) | |
| if err != nil { | |
| panic(err) | |
| } | |
| jws, err := signer.Sign([]byte(payload)) | |
| if err != nil { | |
| fmt.Println(err.Error()) | |
| } | |
| output := jws.FullSerialize() | |
| res, err := http.Post(url, "application/jose+json", strings.NewReader(output)) | |
| return res | |
| } | |
| func thumb(key *ecdsa.PrivateKey) string { | |
| jsonWebKey := jose.JSONWebKey{ | |
| Key: key, | |
| //KeyID: kid, | |
| Algorithm: string(jose.ES256), | |
| } | |
| pub := jsonWebKey.Public() | |
| th, _ := pub.Thumbprint(crypto.SHA256) | |
| //fmt.Println(th) | |
| //th, _ := jsonWebKey.Thumbprint(crypto.SHA256) | |
| return base64.RawURLEncoding.EncodeToString(th[:]) | |
| } | |
| func createCSR(key *ecdsa.PrivateKey) []byte { | |
| req := &x509.CertificateRequest { | |
| Subject: pkix.Name{CommonName: "abc"}, | |
| DNSNames: []string{"example.com.a"}, | |
| EmailAddresses: []string{"gogo@anc"}, | |
| } | |
| csr, err := x509.CreateCertificateRequest(rand.Reader, req, key) | |
| if err != nil { | |
| panic(err) | |
| } | |
| return csr | |
| } | |
| func parseOrderFinal(resp *http.Response) string { | |
| var js struct { | |
| CeriticatePath string `json:"certificate"` | |
| } | |
| decoder := json.NewDecoder(resp.Body) | |
| err := decoder.Decode(&js) | |
| if err != nil { | |
| panic(err) | |
| } | |
| fmt.Println(js) | |
| return js.CeriticatePath | |
| } | |
| func parseAuthResp(resp *http.Response) string { | |
| var js struct { | |
| Status string `json:"status"` | |
| } | |
| decoder := json.NewDecoder(resp.Body) | |
| err := decoder.Decode(&js) | |
| if err != nil { | |
| panic(err) | |
| } | |
| return js.Status | |
| } | |
| func main() { | |
| fmt.Println("generating private key...") | |
| ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | |
| if err != nil { | |
| panic(err) | |
| } | |
| writeKey("key.pem", ecKey) | |
| csr := createCSR(ecKey) | |
| fmt.Println(csr) | |
| fmt.Println("discovering registry") | |
| directory := discover("http://localhost:14000/dir") | |
| fmt.Println(directory) | |
| nonce := getNonce(directory.NewNonce) | |
| fmt.Printf("nonce=%s\n", nonce) | |
| // create new account | |
| fmt.Println("creating account") | |
| newAcc := `{"termsOfServiceAgreed":true,"contact":["mailto:a@c.s"]}` | |
| res := postJWS(ecKey, directory.NewAccount, newAcc, nonce, "") | |
| account := res.Header.Get("Location") | |
| nonce = res.Header.Get("replay-nonce") | |
| res.Body.Close() | |
| fmt.Println(account) | |
| // start certificate order | |
| fmt.Println("issuing new order") | |
| newOrder := `{"identifiers": [ { "type": "dns", "value": "example.com.a" } ]}` | |
| res = postJWS(ecKey, directory.NewOrder, newOrder, nonce, account) | |
| nonce = res.Header.Get("replay-nonce") | |
| order := parseOrderResponse(res) | |
| res.Body.Close() | |
| fmt.Println(order) | |
| // start authorization | |
| fmt.Println("issuing authorization") | |
| res = postJWS(ecKey, order.Authorizations[0], "", nonce, account) | |
| challenge := parseAuthzChalenge(res) | |
| nonce = res.Header.Get("replay-nonce") | |
| res.Body.Close() | |
| fmt.Println("starting http server") | |
| httpHandler := func (w http.ResponseWriter, r *http.Request) { | |
| if len(r.URL.Path) < 3 { | |
| w.WriteHeader(200) | |
| return | |
| } | |
| spl := strings.Split(r.URL.Path, "/") | |
| fmt.Println(spl) | |
| fmt.Println(spl[3]) | |
| w.Header().Add("Content-Type", "application/octet-stream") | |
| out := spl[3] + "." + thumb(ecKey) | |
| w.Write([]byte(out)) | |
| } | |
| httpServer := &http.Server{ | |
| Addr: ":5002", | |
| Handler: http.HandlerFunc(httpHandler), | |
| ReadTimeout: 10 * time.Second, | |
| WriteTimeout: 10 * time.Second, | |
| MaxHeaderBytes: 1 << 20, | |
| } | |
| go httpServer.ListenAndServe() | |
| for { // make sure our http server is up | |
| tstRsp, err := http.Get("http://localhost:5002") | |
| fmt.Println(tstRsp) | |
| if (err == nil) && (tstRsp.StatusCode == 200) { | |
| break | |
| } | |
| } | |
| fmt.Println("confirm server is ready") | |
| // confirm we arranged resource | |
| res = postJWS(ecKey, challenge.Url, "{}", nonce, account) | |
| nonce = res.Header.Get("replay-nonce") | |
| time.Sleep(100 * time.Millisecond) | |
| for { | |
| res = postJWS(ecKey, order.Authorizations[0], "", nonce, account) | |
| nonce = res.Header.Get("replay-nonce") | |
| status := parseAuthResp(res) | |
| fmt.Printf("auth status: %s\n", status) | |
| if (res.StatusCode == 200) && (status == "valid") { | |
| break | |
| } | |
| time.Sleep(100 * time.Millisecond) | |
| } | |
| fmt.Println("send csr") | |
| csrReq := `{"csr":"`+ base64.RawURLEncoding.EncodeToString(csr[:])+`"}` | |
| fmt.Printf("csr requst: %s\n", csrReq) | |
| for { | |
| res = postJWS(ecKey, order.FinalizeUrl, csrReq, nonce, account) | |
| nonce = res.Header.Get("replay-nonce") | |
| if res.StatusCode == 200 { | |
| break | |
| } | |
| time.Sleep(100 * time.Millisecond) | |
| fmt.Println(res) | |
| } | |
| //time.Sleep(1 * time.Second) | |
| fmt.Println("try to get cert") | |
| path := "" | |
| for { | |
| res = postJWS(ecKey, order.OrderUrl, "", nonce, account) | |
| nonce = res.Header.Get("replay-nonce") | |
| path = parseOrderFinal(res) | |
| fmt.Println(path) | |
| if len(path) > 3 { | |
| break | |
| } | |
| time.Sleep(100 * time.Millisecond) | |
| } | |
| fmt.Println("get cert") | |
| res = postJWS(ecKey, path, "", nonce, account) | |
| chainOut, _ := os.Create("chain.pem") | |
| defer chainOut.Close() | |
| io.Copy(chainOut, res.Body) | |
| //time.Sleep(1 * time.Second) | |
| //res = postJWS(ecKey, order.FinalizeUrl, csrReq, nonce, account) | |
| //time.Sleep(3 * time.Second) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment