Skip to content

Instantly share code, notes, and snippets.

@tom-code
Last active December 26, 2018 16:36
Show Gist options
  • Select an option

  • Save tom-code/c2be17fcab211e8733ca9b20cbc2406b to your computer and use it in GitHub Desktop.

Select an option

Save tom-code/c2be17fcab211e8733ca9b20cbc2406b to your computer and use it in GitHub Desktop.
acme client v2 with http auth
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