-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcmd_server.go
More file actions
440 lines (384 loc) · 11.8 KB
/
Copy pathcmd_server.go
File metadata and controls
440 lines (384 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
package main
import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/briandowns/spinner"
"github.com/snowmerak/mtls/ent"
"github.com/snowmerak/mtls/ent/certificate"
"github.com/spf13/cobra"
)
// createServerCertCmd creates a new server certificate
func createServerCertCmd() *cobra.Command {
var batch bool
var caPath, commonName, organization, dnsNames, ipAddresses, outputDir string
var validYears int
var keyType string
cmd := &cobra.Command{
Use: "create",
Short: "Create a new server certificate",
Long: "Interactively create a new server certificate signed by a CA",
RunE: func(cmd *cobra.Command, args []string) error {
// Interactive mode if not batch
if !batch {
if err := promptServerCertInfo(&caPath, &commonName, &organization, &dnsNames, &ipAddresses, &validYears, &keyType, &outputDir); err != nil {
return err
}
}
// Validate inputs
if commonName == "" {
return fmt.Errorf("common name is required")
}
if caPath == "" {
return fmt.Errorf("CA path is required")
}
// Load CA
s := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
s.Suffix = " Loading CA certificate..."
s.Start()
caCertPath := filepath.Join(caPath, "ca-cert.pem")
caKeyPath := filepath.Join(caPath, "ca-key.pem")
ca, err := LoadCAFromFiles(caCertPath, caKeyPath)
s.Stop()
if err != nil {
errorColor.Printf("✗ Failed to load CA: %v\n", err)
return err
}
successColor.Println("✓ CA loaded")
// Parse DNS names and IPs
var dnsNamesList []string
var ipList []net.IP
if dnsNames != "" {
dnsNamesList = strings.Split(dnsNames, ",")
for i := range dnsNamesList {
dnsNamesList[i] = strings.TrimSpace(dnsNamesList[i])
}
}
if ipAddresses != "" {
ips := strings.Split(ipAddresses, ",")
for _, ipStr := range ips {
ipStr = strings.TrimSpace(ipStr)
ip := net.ParseIP(ipStr)
if ip == nil {
warnColor.Printf("⚠ Invalid IP address: %s\n", ipStr)
continue
}
ipList = append(ipList, ip)
}
}
// Create server cert options
opts := DefaultServerCertOptions(commonName)
if organization != "" {
opts.Subject.Organization = []string{organization}
}
opts.DNSNames = dnsNamesList
opts.IPAddresses = ipList
opts.ValidYears = validYears
opts.KeyType = KeyType(keyType)
// Generate server certificate
s.Suffix = " Generating server certificate..."
s.Start()
serverCert, err := ca.GenerateServerCertificateWithOptions(opts)
s.Stop()
if err != nil {
errorColor.Printf("✗ Failed to generate server certificate: %v\n", err)
return err
}
successColor.Println("✓ Server certificate generated")
// Set output directory
if outputDir == "" {
outputDir = filepath.Join(defaultServerDir, commonName)
}
// Save files
certPath := filepath.Join(outputDir, "server-cert.pem")
keyPath := filepath.Join(outputDir, "server-key.pem")
caCertCopyPath := filepath.Join(outputDir, "ca-cert.pem")
metadataPath := filepath.Join(outputDir, ".metadata.json")
s.Suffix = " Saving certificate files..."
s.Start()
if err := serverCert.SaveServerCertToFiles(certPath, keyPath); err != nil {
s.Stop()
errorColor.Printf("✗ Failed to save server certificate: %v\n", err)
return err
}
// Copy CA certificate
caData, _ := os.ReadFile(caCertPath)
os.WriteFile(caCertCopyPath, caData, 0644)
s.Stop()
successColor.Println("✓ Certificate files saved")
// Calculate fingerprint
fingerprint, err := CalculateFingerprint(certPath)
if err != nil {
warnColor.Printf("⚠ Could not calculate fingerprint: %v\n", err)
fingerprint = "unknown"
}
// Save metadata
ipStrings := make([]string, len(ipList))
for i, ip := range ipList {
ipStrings[i] = ip.String()
}
metadata := CertMetadata{
Type: "server",
CommonName: commonName,
Organization: organization,
KeyType: keyType,
CreatedAt: time.Now(),
ExpiresAt: time.Now().AddDate(validYears, 0, 0),
SerialNumber: serverCert.Certificate.SerialNumber.String(),
FingerprintSHA256: fingerprint,
CertPath: certPath,
KeyPath: keyPath,
DNSNames: dnsNamesList,
IPAddresses: ipStrings,
CAPath: caPath,
Issuer: ca.Certificate.Subject.CommonName,
}
if err := SaveMetadata(&metadata, metadataPath); err != nil {
warnColor.Printf("⚠ Could not save metadata: %v\n", err)
}
// Save to DB
if err := SaveCertificateToDB(context.Background(), metadata); err != nil {
warnColor.Printf("⚠ Could not save to database: %v\n", err)
}
// Print success message
fmt.Println()
successColor.Println("✓ Server certificate created successfully!")
infoColor.Printf(" Certificate: %s\n", certPath)
infoColor.Printf(" Private Key: %s (permissions: 0600)\n", keyPath)
infoColor.Printf(" CA Certificate (copy): %s\n", caCertCopyPath)
infoColor.Printf(" Fingerprint: SHA256:%s\n", fingerprint[:16]+"...")
fmt.Println()
infoColor.Println(" Usage example (Go):")
fmt.Printf(" cert, _ := tls.LoadX509KeyPair(\"%s\", \"%s\")\n", certPath, keyPath)
fmt.Println()
return nil
},
}
cmd.Flags().BoolVar(&batch, "batch", false, "Non-interactive mode")
cmd.Flags().StringVar(&caPath, "ca", "", "CA directory path")
cmd.Flags().StringVar(&commonName, "cn", "", "Common Name")
cmd.Flags().StringVar(&organization, "org", "Server Certificate", "Organization")
cmd.Flags().StringVar(&dnsNames, "dns", "", "DNS names (comma separated)")
cmd.Flags().StringVar(&ipAddresses, "ip", "", "IP addresses (comma separated)")
cmd.Flags().IntVar(&validYears, "years", 5, "Valid years")
cmd.Flags().StringVar(&keyType, "key-type", "rsa2048", "Key type (rsa2048, rsa4096, ecp256, ecp384, ecp521)")
cmd.Flags().StringVar(&outputDir, "output", "", "Output directory")
return cmd
}
// listServerCertsCmd lists all server certificates
func listServerCertsCmd() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all server certificates",
RunE: func(cmd *cobra.Command, args []string) error {
certs, err := GetAllCertificates(context.Background())
if err != nil {
return fmt.Errorf("failed to query certificates: %w", err)
}
// Filter for server certificates
var serverCerts []*ent.Certificate
for _, c := range certs {
if c.Type == certificate.TypeServer {
serverCerts = append(serverCerts, c)
}
}
if len(serverCerts) == 0 {
infoColor.Println("No server certificates found. Create one with 'mtls cert create'")
return nil
}
fmt.Println()
successColor.Println("Server Certificates:")
fmt.Println()
for i, cert := range serverCerts {
fmt.Printf("%d. %s\n", i+1, cert.CommonName)
infoColor.Printf(" Organization: %s\n", cert.Organization)
infoColor.Printf(" Key Type: %s\n", cert.KeyType)
infoColor.Printf(" Created: %s\n", cert.CreatedAt.Format("2006-01-02 15:04:05"))
infoColor.Printf(" Expires: %s\n", cert.ExpiresAt.Format("2006-01-02 15:04:05"))
infoColor.Printf(" Path: %s\n", cert.CertPath)
if len(cert.DNSNames) > 0 {
infoColor.Printf(" DNS: %s\n", strings.Join(cert.DNSNames, ", "))
}
if len(cert.IPAddresses) > 0 {
infoColor.Printf(" IP: %s\n", strings.Join(cert.IPAddresses, ", "))
}
fmt.Println()
}
return nil
},
}
}
// inspectCmd inspects a certificate
func inspectCmd() *cobra.Command {
return &cobra.Command{
Use: "inspect [cert-file]",
Short: "Inspect a certificate file",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
certPath := args[0]
data, err := os.ReadFile(certPath)
if err != nil {
return fmt.Errorf("failed to read file: %w", err)
}
info, err := InspectCertificate(data)
if err != nil {
return err
}
fmt.Println(info)
return nil
},
}
}
// verifyCmd verifies a certificate
func verifyCmd() *cobra.Command {
var rootPath, interPath string
cmd := &cobra.Command{
Use: "verify [cert-file]",
Short: "Verify a certificate",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
certPath := args[0]
certData, err := os.ReadFile(certPath)
if err != nil {
return fmt.Errorf("failed to read certificate: %w", err)
}
var rootData, interData []byte
if rootPath != "" {
rootData, err = os.ReadFile(rootPath)
if err != nil {
return fmt.Errorf("failed to read root CA: %w", err)
}
}
if interPath != "" {
interData, err = os.ReadFile(interPath)
if err != nil {
return fmt.Errorf("failed to read intermediate CA: %w", err)
}
}
if err := VerifyCertificate(rootData, interData, certData); err != nil {
errorColor.Printf("✗ Verification failed: %v\n", err)
return nil // Don't return error to avoid cobra usage printing
}
successColor.Println("✓ Certificate is valid")
return nil
},
}
cmd.Flags().StringVar(&rootPath, "root", "", "Root CA certificate path")
cmd.Flags().StringVar(&interPath, "intermediate", "", "Intermediate CA certificate path")
cmd.MarkFlagRequired("root")
return cmd
}
func promptServerCertInfo(caPath, cn, org, dnsNames, ipAddresses *string, years *int, keyType, outputDir *string) error {
// Load registry to show available CAs
cas, err := GetCAs(context.Background())
if err == nil && len(cas) > 0 {
caOptions := make([]string, len(cas))
caPaths := make(map[string]string)
for i, ca := range cas {
label := fmt.Sprintf("%s (expires %s)", ca.CommonName, ca.ExpiresAt.Format("2006-01-02"))
caOptions[i] = label
caPaths[label] = filepath.Dir(ca.CertPath)
}
caOptions = append(caOptions, "Browse for CA certificate...")
var selected string
prompt := &survey.Select{
Message: "Select CA:",
Options: caOptions,
}
if err := survey.AskOne(prompt, &selected); err != nil {
return err
}
if selected == "Browse for CA certificate..." {
prompt := &survey.Input{
Message: "CA directory path:",
Default: defaultCADir,
}
if err := survey.AskOne(prompt, caPath); err != nil {
return err
}
} else {
*caPath = caPaths[selected]
}
} else {
prompt := &survey.Input{
Message: "CA directory path:",
Default: defaultCADir,
}
if err := survey.AskOne(prompt, caPath); err != nil {
return err
}
}
questions := []*survey.Question{
{
Name: "commonName",
Prompt: &survey.Input{
Message: "Common Name:",
Help: "e.g., api.example.com or 192.168.1.100",
},
Validate: survey.Required,
},
{
Name: "dnsNames",
Prompt: &survey.Input{
Message: "DNS names (comma separated):",
Help: "e.g., api.example.com,*.api.example.com,localhost",
},
},
{
Name: "ipAddresses",
Prompt: &survey.Input{
Message: "IP addresses (comma separated):",
Help: "e.g., 127.0.0.1,192.168.1.100",
},
},
{
Name: "organization",
Prompt: &survey.Input{
Message: "Organization (optional):",
},
},
{
Name: "validYears",
Prompt: &survey.Input{
Message: "Valid Years:",
Default: "5",
},
},
{
Name: "keyType",
Prompt: &survey.Select{
Message: "Key Type:",
Options: []string{"rsa2048", "rsa4096", "ecp256", "ecp384", "ecp521", "ed25519"},
Default: "rsa2048",
},
},
}
answers := struct {
CommonName string
DNSNames string
IPAddresses string
Organization string
ValidYears string
KeyType string
}{}
if err := survey.Ask(questions, &answers); err != nil {
return err
}
*cn = answers.CommonName
*dnsNames = answers.DNSNames
*ipAddresses = answers.IPAddresses
*org = answers.Organization
*keyType = answers.KeyType
*outputDir = filepath.Join(defaultServerDir, *cn)
// Parse valid years
fmt.Sscanf(answers.ValidYears, "%d", years)
if *years <= 0 {
*years = 5
}
return nil
}