Skip to content
File

Blob: spec/acme/acme.go

go114 lines
1package acme
2 
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "fmt"
7 "net/url"
8 "regexp"
9 "strings"
10 "time"
11 "unicode"
12 
13 "github.com/caddyserver/certmagic"
14 "github.com/miekg/dns"
15 "golang.org/x/net/idna"
16)
17 
18const (
19 HashcashDifficulty int = 18
20 HashcashExpires time.Duration = time.Second * 10
21)
22 
23const (
24 ManagedDelegation = "managed"
25)
26 
27var nonDnsRegex = regexp.MustCompile(`[^a-z0-9-.]+`)
28 
29func removeSpace(str string) string {
30 var b strings.Builder
31 b.Grow(len(str))
32 for _, ch := range str {
33 if !unicode.IsSpace(ch) {
34 b.WriteRune(ch)
35 }
36 }
37 return b.String()
38}
39 
40func Normalize(zone string) (string, error) {
41 trimmed := removeSpace(zone)
42 if !certmagic.SubjectQualifiesForPublicCert(trimmed) {
43 return "", fmt.Errorf("acme: invalid zone for acme certificate")
44 }
45 if strings.Contains(trimmed, "*") {
46 return "", fmt.Errorf("acme: wildcard zone is not supported")
47 }
48 uni, err := idna.ToASCII(trimmed)
49 if err != nil {
50 return "", fmt.Errorf("acme: error converting zone to ascii: %w", err)
51 }
52 invalid := nonDnsRegex.FindStringIndex(uni)
53 if len(invalid) > 0 {
54 return "", fmt.Errorf("acme: zone contains invalid dns characters")
55 }
56 return uni, nil
57}
58 
59func EncodeClientToken(token []byte) string {
60 hash := sha256.New224()
61 hash.Write(token)
62 return hex.EncodeToString(hash.Sum(nil))
63}
64 
65func generateRecord(zone, delegation, subdomain string) (name, content string) {
66 builder := strings.Builder{}
67 
68 builder.WriteString("_acme-challenge.")
69 builder.WriteString(zone)
70 if !dns.IsFqdn(zone) {
71 builder.WriteString(".")
72 }
73 name = builder.String()
74 
75 builder.Reset()
76 builder.WriteString(subdomain)
77 builder.WriteString(".")
78 builder.WriteString(delegation)
79 if !dns.IsFqdn(delegation) {
80 builder.WriteString(".")
81 }
82 content = builder.String()
83 
84 return
85}
86 
87func GenerateManagedRecord(zone, delegation string) (name, content string) {
88 return generateRecord(zone, delegation, ManagedDelegation)
89}
90 
91func GenerateCustomRecord(zone, delegation string, token []byte) (name, content string) {
92 return generateRecord(zone, delegation, EncodeClientToken(token))
93}
94 
95func ParseAcmeURI(uri string) (email, zone string, err error) {
96 parse, err := url.Parse(uri)
97 if err != nil {
98 err = fmt.Errorf("error parsing acme uri: %w", err)
99 return
100 }
101 email = parse.User.Username()
102 if email == "" {
103 err = fmt.Errorf("missing email address")
104 return
105 }
106 zone = parse.Hostname()
107 if zone == "" {
108 err = fmt.Errorf("missing hosted zone")
109 return
110 }
111 
112 return
113}