main
go 105 lines 3.04 KB
Raw
1 package acme
2
3 import (
4 "crypto/ecdsa"
5 "crypto/elliptic"
6 "crypto/rand"
7 "crypto/x509"
8 "crypto/x509/pkix"
9 "encoding/pem"
10 "fmt"
11 "math/big"
12 "net"
13 "path/filepath"
14 "time"
15
16 "github.com/gosuda/portal-tunnel/v2/utils"
17 )
18
19 const localDevelopmentCertificateTTL = 3650 * 24 * time.Hour
20
21 func ensureLocalDevelopmentCertificate(keyDir, baseHost string) error {
22 domains := localDevelopmentDomains(baseHost)
23 keyFile := filepath.Join(keyDir, keyFileName)
24 certFile := filepath.Join(keyDir, fullChainFileName)
25
26 if utils.FileExists(keyFile) && utils.FileExists(certFile) {
27 covered, err := certCoversDomains(certFile, domains)
28 if err == nil && covered {
29 return nil
30 }
31 }
32
33 if err := utils.EnsureParentDir(keyFile); err != nil {
34 return err
35 }
36 if err := utils.EnsureParentDir(certFile); err != nil {
37 return err
38 }
39
40 privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
41 if err != nil {
42 return fmt.Errorf("generate local dev private key: %w", err)
43 }
44
45 serialLimit := new(big.Int).Lsh(big.NewInt(1), 128)
46 serialNumber, err := rand.Int(rand.Reader, serialLimit)
47 if err != nil {
48 return fmt.Errorf("generate local dev certificate serial: %w", err)
49 }
50
51 now := time.Now().UTC()
52 template := &x509.Certificate{
53 SerialNumber: serialNumber,
54 Subject: pkix.Name{
55 CommonName: baseHost,
56 Organization: []string{"Portal Local Development"},
57 },
58 NotBefore: now.Add(-1 * time.Hour),
59 NotAfter: now.Add(localDevelopmentCertificateTTL),
60 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign,
61 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
62 BasicConstraintsValid: true,
63 IsCA: true,
64 }
65
66 for _, domain := range domains {
67 if ip := net.ParseIP(domain); ip != nil {
68 template.IPAddresses = append(template.IPAddresses, ip)
69 continue
70 }
71 template.DNSNames = append(template.DNSNames, domain)
72 }
73
74 certDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
75 if err != nil {
76 return fmt.Errorf("create local dev certificate: %w", err)
77 }
78
79 certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
80 keyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
81 if err != nil {
82 return fmt.Errorf("marshal local dev private key: %w", err)
83 }
84 keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
85
86 if err := utils.WriteFileAtomic(certFile, certPEM, 0o644); err != nil {
87 return fmt.Errorf("write local dev certificate: %w", err)
88 }
89 if err := utils.WriteFileAtomic(keyFile, keyPEM, 0o600); err != nil {
90 return fmt.Errorf("write local dev private key: %w", err)
91 }
92 return nil
93 }
94
95 func localDevelopmentDomains(baseHost string) []string {
96 baseHost = utils.NormalizeBaseDomain(baseHost)
97 domains := []string{"localhost", "*.localhost", "127.0.0.1", "::1"}
98 if baseHost != "" && baseHost != "localhost" {
99 domains = append(domains, baseHost)
100 if net.ParseIP(baseHost) == nil {
101 domains = append(domains, "*."+baseHost)
102 }
103 }
104 return domains
105 }