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