blob: 5079c548fe7f5aafc1db5c68e1825c7f4d9f2372 [file] [log] [blame]
Serge Bazanski2dc42802024-06-04 14:30:19 +00001package main
2
3// Minimal GnuTLS certtool-like tool in Go. Implements only what's needed for
4// compatibility with `swtpm_localca`.
5
6import (
7 "crypto/rand"
8 "crypto/rsa"
9 "crypto/x509"
10 "encoding/pem"
11 "log"
12 "math/big"
13 "os"
14 "strconv"
15 "strings"
16 "time"
17
18 "github.com/spf13/pflag"
19)
20
21var (
22 flagGeneratePrivkey bool
23 flagGenerateSelfSigned bool
24 flagGenerateCertificate bool
25
26 flagOutfile string
27 flagTemplate string
28 flagLoadPrivkey string
29 flagLoadCAPrivkey string
30 flagLoadCACertificate string
31)
32
33func main() {
34 pflag.BoolVar(&flagGeneratePrivkey, "generate-privkey", false, "Generate RSA private kay")
35 pflag.BoolVar(&flagGenerateSelfSigned, "generate-self-signed", false, "Generate self-signed certificate")
36 pflag.BoolVar(&flagGenerateCertificate, "generate-certificate", false, "Sign certificate")
37
38 pflag.StringVar(&flagOutfile, "outfile", "", "Output file for operation")
39 pflag.StringVar(&flagTemplate, "template", "", "Certificate template file (GnuTLS proprietary)")
40 pflag.StringVar(&flagLoadPrivkey, "load-privkey", "", "Path to private key")
41 pflag.StringVar(&flagLoadCAPrivkey, "load-ca-privkey", "", "Path to CA private key")
42 pflag.StringVar(&flagLoadCACertificate, "load-ca-certificate", "", "Path to CA certificate")
43 pflag.Parse()
44
45 modesActive := 0
46 for _, mode := range []bool{flagGeneratePrivkey, flagGenerateSelfSigned, flagGenerateCertificate} {
47 if mode {
48 modesActive++
49 }
50 }
51 if modesActive != 1 {
52 log.Fatalf("Exactly one of --generate-privkey, --generate-self-signed, --generate-certificate must be set")
53 }
54
55 if flagGeneratePrivkey || flagGenerateSelfSigned || flagGenerateCertificate {
56 if flagOutfile == "" {
57 log.Fatalf("--outfile must be set")
58 }
59 }
60 if flagGenerateSelfSigned || flagGenerateCertificate {
61 if flagTemplate == "" {
62 log.Fatalf("--template must be set")
63 }
64 if flagLoadPrivkey == "" {
65 log.Fatalf("--load-privkey must be set")
66 }
67 if flagOutfile == "" {
68 log.Fatalf("--outfile must be set")
69 }
70 }
71 if flagGenerateCertificate {
72 if flagLoadCAPrivkey == "" {
73 log.Fatalf("--load-ca-privkey must be set")
74 }
75 if flagLoadCACertificate == "" {
76 log.Fatalf("--load-ca-certificate must be set")
77 }
78 }
79 switch {
80 case flagGeneratePrivkey:
81 generatePrivkey(flagOutfile)
82 case flagGenerateSelfSigned:
83 generateSelfSigned(flagTemplate, flagLoadPrivkey, flagOutfile)
84 case flagGenerateCertificate:
85 generateCertificate(flagTemplate, flagLoadPrivkey, flagLoadCAPrivkey, flagLoadCACertificate, flagOutfile)
86 }
87}
88
89func generatePrivkey(outfile string) {
90 priv, err := rsa.GenerateKey(rand.Reader, 3072)
91 if err != nil {
92 log.Fatalf("Could not generate RSA key: %v", err)
93 }
94 block := pem.Block{
95 Type: "RSA PRIVATE KEY",
96 Bytes: x509.MarshalPKCS1PrivateKey(priv),
97 }
98 if err := os.WriteFile(outfile, pem.EncodeToMemory(&block), 0600); err != nil {
99 log.Fatalf("Could not write RSA key: %v", err)
100 }
101}
102
103// certificateFromTemplate parses a GnuTLS 'template' file. This template file is
104// made up of newline-separated stanzas, with an optional 'data' part after a =
105// character.
106//
107// Supported stanzas (on conflict last stanza wins):
108//
109// cn=data: set subject CN to data
110// ca: mark certificate as CA
111// cert_signing_key: enable keyCertSign KeyUsage
112// expiration_days: number of days in which cert expires (if -1/default, no expiry date)
113func certificateFromTemplate(template string) *x509.Certificate {
114 serial, err := rand.Int(rand.Reader, big.NewInt(1).Lsh(big.NewInt(1), 128))
115 if err != nil {
116 log.Fatalf("Could not generate serial: %v", err)
117 }
118 res := x509.Certificate{
119 SerialNumber: serial,
120 NotBefore: time.Now().Add(-time.Minute),
121 BasicConstraintsValid: true,
122 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
123 }
124 for _, line := range strings.Split(template, "\n") {
125 line = strings.TrimSpace(line)
126 if line == "" {
127 continue
128 }
129 parts := strings.SplitN(line, "=", 2)
130 for i := range parts {
131 parts[i] = strings.TrimSpace(parts[i])
132 }
133 switch parts[0] {
134 case "cn":
135 res.Subject.CommonName = parts[1]
136 case "ca":
137 res.IsCA = true
138 case "cert_signing_key":
139 res.KeyUsage |= x509.KeyUsageCertSign
140 case "expiration_days":
141 days, err := strconv.ParseInt(parts[1], 10, 64)
142 if err != nil {
143 log.Fatalf("Invalid expiration_days: %q", err)
144 }
145 if days != -1 {
146 res.NotAfter = time.Now().Add(time.Hour * 24 * time.Duration(days))
147 } else {
148 res.NotAfter = time.Unix(253402300799, 0)
149 }
150 default:
151 log.Fatalf("Unhandled template line %q", line)
152 }
153 }
154 return &res
155}
156
157func readPrivkey(path string) *rsa.PrivateKey {
158 bytes, err := os.ReadFile(path)
159 if err != nil {
160 log.Fatalf("Could not read private key: %v", err)
161 }
162 block, _ := pem.Decode(bytes)
163 if block.Type != "RSA PRIVATE KEY" {
164 log.Fatalf("Private key contains invalid PEM data")
165 }
166 res, err := x509.ParsePKCS1PrivateKey(block.Bytes)
167 if err != nil {
168 log.Fatalf("Could not parse private key: %v", err)
169 }
170 return res
171}
172
173func readCertificate(path string) *x509.Certificate {
174 bytes, err := os.ReadFile(path)
175 if err != nil {
176 log.Fatalf("Could not read certificate: %v", err)
177 }
178 block, _ := pem.Decode(bytes)
179 if block.Type != "CERTIFICATE" {
180 log.Fatalf("Certificate contains invalid PEM data")
181 }
182 res, err := x509.ParseCertificate(block.Bytes)
183 if err != nil {
184 log.Fatalf("Could not parse certificate: %v", err)
185 }
186 return res
187}
188
189func generateSelfSigned(templatePath, privkeyPath, outfile string) {
190 template, err := os.ReadFile(templatePath)
191 if err != nil {
192 log.Fatalf("Could not read template: %v", err)
193 }
194 priv := readPrivkey(privkeyPath)
195 cert := certificateFromTemplate(string(template))
196
197 derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, priv.Public(), priv)
198 if err != nil {
199 log.Fatalf("Could not generate self-signed certificate: %v", err)
200 }
201 block := pem.Block{
202 Type: "CERTIFICATE",
203 Bytes: derBytes,
204 }
205 if err := os.WriteFile(outfile, pem.EncodeToMemory(&block), 0600); err != nil {
206 log.Fatalf("Could not write self-signed certificate: %v", err)
207 }
208}
209
210func generateCertificate(templatePath, privkeyPath, caPrivkeyPath, caCertificatePath, outfile string) {
211 template, err := os.ReadFile(templatePath)
212 if err != nil {
213 log.Fatalf("Could not read template: %v", err)
214 }
215 priv := readPrivkey(privkeyPath)
216 caPriv := readPrivkey(caPrivkeyPath)
217 cert := certificateFromTemplate(string(template))
218 ca := readCertificate(caCertificatePath)
219
220 derBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, priv.Public(), caPriv)
221 if err != nil {
222 log.Fatalf("Could not generate certificate: %v", err)
223 }
224 block := pem.Block{
225 Type: "CERTIFICATE",
226 Bytes: derBytes,
227 }
228 if err := os.WriteFile(outfile, pem.EncodeToMemory(&block), 0600); err != nil {
229 log.Fatalf("Could not write certificate: %v", err)
230 }
231}