-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcsr.go
More file actions
238 lines (212 loc) · 7.45 KB
/
Copy pathcsr.go
File metadata and controls
238 lines (212 loc) · 7.45 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
package certkit
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"net"
"net/mail"
"net/url"
)
var errCSRPrivateKeyNotSigner = errors.New("private key does not implement crypto.Signer")
// GenerateCSR creates a Certificate Signing Request that copies Subject, DNSNames,
// IPAddresses, and URIs from the given leaf certificate. If privateKey is nil,
// a new EC P-256 key is generated. Returns the PEM-encoded CSR and, if a key was
// auto-generated, its PEM-encoded PKCS#8 private key (empty string if caller provided the key).
func GenerateCSR(leaf *x509.Certificate, privateKey crypto.PrivateKey) (csrPEM string, keyPEM string, err error) {
if leaf == nil {
return "", "", errCertificateNil
}
var signer crypto.Signer
autoGenerated := false
if privateKey != nil {
var ok bool
signer, ok = privateKey.(crypto.Signer)
if !ok {
return "", "", errCSRPrivateKeyNotSigner
}
} else {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return "", "", fmt.Errorf("generating EC P-256 key: %w", err)
}
signer = key
autoGenerated = true
}
template := &x509.CertificateRequest{
Subject: leaf.Subject,
DNSNames: leaf.DNSNames,
IPAddresses: leaf.IPAddresses,
URIs: leaf.URIs,
}
csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, signer)
if err != nil {
return "", "", fmt.Errorf("creating CSR: %w", err)
}
csrPEM = string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csrDER,
}))
if autoGenerated {
keyDER, err := x509.MarshalPKCS8PrivateKey(signer)
if err != nil {
return "", "", fmt.Errorf("encoding private key: %w", err)
}
keyPEM = string(pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: keyDER,
}))
}
return csrPEM, keyPEM, nil
}
// CSRTemplateOtherName represents an OtherName SAN entry in a CSR template.
type CSRTemplateOtherName struct {
// Type is a well-known label ("UPN", "SRV", "XMPP", "SmtpUTF8Mailbox")
// or a dotted-decimal OID string.
Type string `json:"type"`
// Value is the string value for the OtherName entry.
Value string `json:"value"`
}
// CSRTemplate is a JSON-serializable template for CSR generation.
type CSRTemplate struct {
// Subject contains the distinguished name fields for the CSR.
Subject CSRSubject `json:"subject"`
// Hosts lists the DNS names, IP addresses, URIs, and email addresses for SANs.
Hosts []string `json:"hosts"`
// OtherNames lists OtherName SAN entries (e.g. UPN for mTLS user certs).
OtherNames []CSRTemplateOtherName `json:"other_names,omitempty"`
}
// CSRSubject holds the subject fields for a CSR template.
type CSRSubject struct {
CommonName string `json:"common_name"`
Organization []string `json:"organization,omitempty"`
OrganizationalUnit []string `json:"organizational_unit,omitempty"`
Country []string `json:"country,omitempty"`
Province []string `json:"province,omitempty"`
Locality []string `json:"locality,omitempty"`
}
// ParseCSRTemplate unmarshals JSON data into a CSRTemplate.
func ParseCSRTemplate(data []byte) (*CSRTemplate, error) {
var tmpl CSRTemplate
if err := json.Unmarshal(data, &tmpl); err != nil {
return nil, fmt.Errorf("parsing CSR template: %w", err)
}
return &tmpl, nil
}
// ClassifyHosts splits a mixed host list into DNS names, IPs, URIs, and emails.
// Classification precedence: IP address, email (RFC 5322), URI with scheme+host,
// then DNS name. Email detection uses mail.ParseAddress with a bare-address guard
// to reject display-name forms like "John <john@example.com>".
func ClassifyHosts(hosts []string) (dnsNames []string, ips []net.IP, uris []*url.URL, emails []string) {
for _, h := range hosts {
if ip := net.ParseIP(h); ip != nil {
ips = append(ips, ip)
} else if addr, err := mail.ParseAddress(h); err == nil && addr.Address == h {
emails = append(emails, h)
} else if parsed, err := url.Parse(h); err == nil && parsed.Scheme != "" && parsed.Host != "" {
uris = append(uris, parsed)
} else {
dnsNames = append(dnsNames, h)
}
}
return
}
// GenerateCSRFromTemplate creates a PEM-encoded CSR from a template and signer.
//
// When OtherNames are present, the entire SAN extension is built via
// MarshalSANExtension and placed in ExtraExtensions. The typed SAN fields
// (DNSNames, IPAddresses, etc.) are left nil on the CSR template to avoid
// Go generating a duplicate SAN extension.
func GenerateCSRFromTemplate(tmpl *CSRTemplate, signer crypto.Signer) (string, error) {
subject := pkix.Name{
CommonName: tmpl.Subject.CommonName,
Organization: tmpl.Subject.Organization,
OrganizationalUnit: tmpl.Subject.OrganizationalUnit,
Country: tmpl.Subject.Country,
Province: tmpl.Subject.Province,
Locality: tmpl.Subject.Locality,
}
dnsNames, ips, uris, emails := ClassifyHosts(tmpl.Hosts)
// Auto-fill CN from first DNS name if empty
if subject.CommonName == "" && len(dnsNames) > 0 {
subject.CommonName = dnsNames[0]
}
csrTemplate := &x509.CertificateRequest{
Subject: subject,
}
if len(tmpl.OtherNames) > 0 {
otherNames := make([]OtherNameSAN, 0, len(tmpl.OtherNames))
for _, on := range tmpl.OtherNames {
oid, err := ResolveOtherNameOID(on.Type)
if err != nil {
return "", fmt.Errorf("resolving othername type: %w", err)
}
otherNames = append(otherNames, OtherNameSAN{OID: oid, Value: on.Value})
}
sanExt, err := MarshalSANExtension(MarshalSANExtensionInput{
DNSNames: dnsNames,
EmailAddresses: emails,
IPAddresses: ips,
URIs: uris,
OtherNames: otherNames,
})
if err != nil {
return "", fmt.Errorf("building SAN extension: %w", err)
}
csrTemplate.ExtraExtensions = []pkix.Extension{sanExt}
} else {
csrTemplate.DNSNames = dnsNames
csrTemplate.IPAddresses = ips
csrTemplate.URIs = uris
csrTemplate.EmailAddresses = emails
}
csrDER, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, signer)
if err != nil {
return "", fmt.Errorf("creating CSR: %w", err)
}
return string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csrDER,
})), nil
}
// GenerateCSRFromCSR creates a new CSR using an existing CSR as template,
// signed by the provided key. String-typed OtherName SAN entries from the
// source CSR are preserved; binary-typed OtherNames are silently skipped.
func GenerateCSRFromCSR(source *x509.CertificateRequest, signer crypto.Signer) (string, error) {
csrTemplate := &x509.CertificateRequest{
Subject: source.Subject,
}
otherNames := parseOtherNameSANEntries(source.Extensions)
if len(otherNames) > 0 {
sanExt, err := MarshalSANExtension(MarshalSANExtensionInput{
DNSNames: source.DNSNames,
EmailAddresses: source.EmailAddresses,
IPAddresses: source.IPAddresses,
URIs: source.URIs,
OtherNames: otherNames,
})
if err != nil {
return "", fmt.Errorf("building SAN extension: %w", err)
}
csrTemplate.ExtraExtensions = []pkix.Extension{sanExt}
} else {
csrTemplate.DNSNames = source.DNSNames
csrTemplate.IPAddresses = source.IPAddresses
csrTemplate.URIs = source.URIs
csrTemplate.EmailAddresses = source.EmailAddresses
}
csrDER, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, signer)
if err != nil {
return "", fmt.Errorf("creating CSR: %w", err)
}
return string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csrDER,
})), nil
}