Skip to content

Instantly share code, notes, and snippets.

@yteraoka
Created November 1, 2022 09:14
Show Gist options
  • Save yteraoka/350836c45ce34ecf7119cd58de20a168 to your computer and use it in GitHub Desktop.
Save yteraoka/350836c45ce34ecf7119cd58de20a168 to your computer and use it in GitHub Desktop.
golang.org/x/crypto/acme を使って証明書を発行する例
package main
import (
"bufio"
"context"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"flag"
"fmt"
"golang.org/x/crypto/acme"
"log"
"os"
"strings"
)
func parsePrivateKey(der []byte) (crypto.Signer, error) {
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
return key, nil
}
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
switch key := key.(type) {
case *rsa.PrivateKey:
return key, nil
case *ecdsa.PrivateKey:
return key, nil
default:
return nil, errors.New("unknown private key type")
}
}
if key, err := x509.ParseECPrivateKey(der); err == nil {
return key, nil
}
return nil, errors.New("failed to parse private key")
}
func main() {
var (
keyFile = flag.String("key-file", "", "account key file")
email = flag.String("email", "", "email address")
domain = flag.String("domain", "", "domain name")
orderURI = flag.String("order-uri", "", "Order URI")
useStaging = flag.Bool("staging", false, "Use Let's Encrypt Staging Endpoint")
)
flag.Parse()
var err error
if *domain == "" {
log.Fatal("domain is required")
}
block := &pem.Block{}
var acctKey crypto.Signer
if *keyFile != "" {
bytes, err := os.ReadFile(*keyFile)
if err != nil {
log.Fatal(err)
}
block, _ = pem.Decode(bytes)
} else {
if *email == "" {
log.Fatal("email is required")
}
k, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
log.Fatal(err)
}
block = &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(k),
}
f, err := os.Create("acme.key")
if err != nil {
log.Fatal(err)
}
if err := pem.Encode(f, block); err != nil {
log.Fatal(err)
}
f.Close()
fmt.Println("Saved new account private key into acme.key")
}
if block == nil {
log.Fatal("failed to decode PEM block containing private key")
}
if strings.Contains(block.Type, "PRIVATE") {
acctKey, err = parsePrivateKey(block.Bytes)
} else {
log.Fatal("Unknown private key type: %s", block.Type)
}
client := &acme.Client{
Key: acctKey,
}
// staging 環境を使う場合は DirectoryURL に https://acme-staging-v02.api.letsencrypt.org/directory を指定する
if *useStaging {
client.DirectoryURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
}
fmt.Printf("### acme.Client ###\n%#v\n\n", client)
ctx := context.Background()
acct := &acme.Account{}
if *keyFile == "" {
acct.Contact = append(acct.Contact, "mailto:"+*email)
fmt.Printf("### Account ###\n%#v\n\n", acct)
acct, err = client.Register(ctx, acct, acme.AcceptTOS)
if err != nil {
log.Fatal(err)
}
} else {
// すでに Let's Encrypt のアカウントを持っている場合は PrivateKey から Account を取得
acct, err := client.GetReg(ctx, "")
if err != nil {
log.Fatal(err)
}
fmt.Printf("### Account ###\n%#v\n\n", acct)
}
directory, err := client.Discover(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("### Directory ###\n%#v\n\n", directory)
order := &acme.Order{}
if *orderURI == "" {
order, err = client.AuthorizeOrder(ctx, acme.DomainIDs(*domain))
} else {
order, err = client.GetOrder(ctx, *orderURI)
}
if err != nil {
log.Fatal(err)
}
fmt.Printf("### AuthorizeOrder ###\n%#v\n\n", order)
if order.Error != nil {
log.Fatal(order.Error.Error())
}
for i, url := range order.AuthzURLs {
authz, err := client.GetAuthorization(ctx, url)
if err != nil {
log.Fatal(err)
}
fmt.Printf("### Authorization[%d] ###\n%#v\n\n", i, authz)
for j, c := range authz.Challenges {
fmt.Printf("### Challenge[%d] ###\n%#v\n\n", j, c)
if c.Type == "http-01" {
path := client.HTTP01ChallengePath(c.Token)
fmt.Printf("### HTTP01ChallengePath ###\n%s\n\n", path)
response, err := client.HTTP01ChallengeResponse(c.Token)
if err != nil {
log.Fatal(err)
}
fmt.Printf("### HTTP01ChallengeResponse ###\n%s\n\n", response)
if c.Status == "pending" {
scanner := bufio.NewScanner(os.Stdin)
fmt.Printf("Type `ENTER` if you are ready: ")
scanner.Scan()
// Accept すると challenge の URL にアクセスが来る
fmt.Println("Going to accept challenge")
c2, err := client.Accept(ctx, c)
if err != nil {
log.Fatal(err)
}
fmt.Printf("### Accept Response ###\n%s\n\n", c2)
}
}
}
// Accept したら Authorization の Status が ready になるまで待つ
if authz.Status == "pending" {
fmt.Printf("### Starting WaitAuthorization ###\n")
a, err := client.WaitAuthorization(ctx, url)
if err != nil {
log.Fatal(err)
}
fmt.Printf("### WaitAuthorization Response ###\n#%v\n\n", a)
}
}
// Order の Status が ready になるのを待つ
if order.Status == "pending" {
fmt.Printf("### Starting WaitOrder ###\n")
if *orderURI == "" {
*orderURI = order.URI
}
order, err = client.WaitOrder(ctx, *orderURI)
if err != nil {
log.Fatal(err)
}
fmt.Printf("### WaitOrder Response ###\n%#v\n\n", order)
}
// Private Key の作成を保存
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
log.Fatal(err)
}
f, err := os.Create(*domain + ".key")
if err != nil {
log.Fatal(err)
}
err = pem.Encode(f, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
if err != nil {
log.Fatal(err)
}
f.Close()
// CSR 作成
req := &x509.CertificateRequest{
Subject: pkix.Name{CommonName: *domain},
DNSNames: []string{*domain},
}
csr, err := x509.CreateCertificateRequest(rand.Reader, req, key)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%#v\n", csr)
// 証明書発行リクエスト
der, certURL, err := client.CreateOrderCert(ctx, order.FinalizeURL, csr, true)
if err != nil {
log.Fatal(err)
}
f, err = os.Create(*domain + ".crt")
if err != nil {
log.Fatal(err)
}
for _, b := range der {
block := &pem.Block{
Type: "CERTIFICATE",
Bytes: b,
}
if err := pem.Encode(os.Stdout, block); err != nil {
log.Fatal(err)
}
if err := pem.Encode(f, block); err != nil {
log.Fatal(err)
}
}
f.Close()
fmt.Printf("certURL: %s\n", certURL)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment