Skip to content

Commit df5f07b

Browse files
authored
Merge pull request #85 from mutablelogic/v5
V5
2 parents ac7624c + b4948ff commit df5f07b

35 files changed

+3221
-452
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/mutablelogic/go-server
33
go 1.23.5
44

55
require (
6-
github.com/djthorpe/go-pg v1.0.2
6+
github.com/djthorpe/go-pg v1.0.3
77
github.com/stretchr/testify v1.10.0
88
)
99

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
2424
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2525
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
2626
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
27-
github.com/djthorpe/go-pg v1.0.2 h1:hbuyyRWt26k9jwoyt2WdYJdMICMtRozXj6lBgcIvwT0=
28-
github.com/djthorpe/go-pg v1.0.2/go.mod h1:XHl/w8+66Hs746nOYd+gdjqPImNuLVZ5UsXLI47rb4c=
27+
github.com/djthorpe/go-pg v1.0.3 h1:mD0fXftDlEaqxuCH+XAhHGmY/l93uBuPIgEy6iCSqRI=
28+
github.com/djthorpe/go-pg v1.0.3/go.mod h1:XHl/w8+66Hs746nOYd+gdjqPImNuLVZ5UsXLI47rb4c=
2929
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
3030
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
3131
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=

pkg/cert/cert.go

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
package cert
2+
3+
import (
4+
"crypto/ecdsa"
5+
"crypto/ed25519"
6+
"crypto/rand"
7+
"crypto/rsa"
8+
"crypto/x509"
9+
"encoding/json"
10+
"encoding/pem"
11+
"fmt"
12+
"io"
13+
"time"
14+
15+
// Packages
16+
schema "github.com/mutablelogic/go-server/pkg/cert/schema"
17+
types "github.com/mutablelogic/go-server/pkg/types"
18+
)
19+
20+
////////////////////////////////////////////////////////////////////////////////
21+
// TYPES
22+
23+
// Certificate
24+
type Cert struct {
25+
Name string `json:"name"` // Common Name
26+
Subject *uint64 `json:"subject,omitempty"` // Subject
27+
Signer *Cert `json:"signer,omitempty"` // Signer
28+
Ts *time.Time `json:"timestamp,omitempty"` // Timestamp
29+
30+
// The private key and certificate
31+
priv any
32+
x509 x509.Certificate
33+
}
34+
35+
////////////////////////////////////////////////////////////////////////////////
36+
// GLOBALS
37+
38+
const (
39+
// Supported key types
40+
keyTypeRSA = "RSA"
41+
keyTypeECDSA = "ECDSA"
42+
43+
// DefaultBits is the default number of bits for a RSA private key
44+
defaultBits = 2048
45+
)
46+
47+
const (
48+
PemTypePrivateKey = "PRIVATE KEY"
49+
PemTypeCertificate = "CERTIFICATE"
50+
)
51+
52+
////////////////////////////////////////////////////////////////////////////////
53+
// LIFECYCLE
54+
55+
// Create a new certificate
56+
func New(opts ...Opt) (*Cert, error) {
57+
cert, err := apply(opts...)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
// Check for key
63+
if cert.priv == nil || cert.PublicKey() == nil {
64+
return nil, fmt.Errorf("missing private or public key")
65+
}
66+
67+
// Set the NotBefore and NotAfter dates based on signer
68+
if cert.Signer != nil {
69+
if !cert.Signer.x509.NotBefore.IsZero() && cert.Signer.x509.NotBefore.After(cert.x509.NotBefore) {
70+
cert.x509.NotBefore = cert.Signer.x509.NotBefore
71+
}
72+
if !cert.Signer.x509.NotAfter.IsZero() && cert.Signer.x509.NotAfter.Before(cert.x509.NotAfter) {
73+
cert.x509.NotAfter = cert.Signer.x509.NotAfter
74+
}
75+
}
76+
77+
// Check for expiry
78+
if cert.x509.NotAfter.IsZero() {
79+
return nil, fmt.Errorf("missing expiry date")
80+
}
81+
82+
// Set random serial number if not set
83+
if cert.x509.SerialNumber == nil {
84+
if err := WithRandomSerial()(cert); err != nil {
85+
return nil, err
86+
}
87+
}
88+
89+
// Set the name from the common name
90+
cert.Name = cert.x509.Subject.CommonName
91+
if cert.Name == "" {
92+
cert.Name = fmt.Sprintf("%x", cert.x509.SerialNumber)
93+
}
94+
95+
// Create the certificate
96+
signer := cert.Signer
97+
if signer == nil {
98+
signer = cert
99+
} else {
100+
cert.x509.Issuer = signer.x509.Subject
101+
}
102+
if data, err := x509.CreateCertificate(rand.Reader, &cert.x509, &signer.x509, cert.PublicKey(), signer.priv); err != nil {
103+
return nil, err
104+
} else {
105+
cert.x509.Raw = data
106+
}
107+
108+
// Return the certificate
109+
return cert, nil
110+
}
111+
112+
// Read a certificate
113+
func Read(r io.Reader) (*Cert, error) {
114+
cert := new(Cert)
115+
data, err := io.ReadAll(r)
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
// Read until EOF
121+
for len(data) > 0 {
122+
// Decode the PEM block
123+
block, rest := pem.Decode(data)
124+
if block == nil {
125+
return nil, fmt.Errorf("invalid PEM block")
126+
}
127+
128+
// Parse the block
129+
switch block.Type {
130+
case PemTypeCertificate:
131+
c, err := x509.ParseCertificate(block.Bytes)
132+
if err != nil {
133+
return nil, err
134+
} else {
135+
cert.x509 = *c
136+
}
137+
case PemTypePrivateKey:
138+
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
139+
if err != nil {
140+
return nil, err
141+
} else {
142+
cert.priv = key
143+
}
144+
default:
145+
return nil, fmt.Errorf("invalid PEM block type: %q", block.Type)
146+
}
147+
148+
// Move to next block
149+
data = rest
150+
}
151+
152+
// Set name from serial number if not set
153+
if cert.Name == "" {
154+
cert.Name = fmt.Sprintf("%x", cert.x509.SerialNumber)
155+
}
156+
157+
// Return success
158+
return cert, nil
159+
}
160+
161+
///////////////////////////////////////////////////////////////////////////////
162+
// STRINGIFY
163+
164+
func (c Cert) MarshalJSON() ([]byte, error) {
165+
return json.Marshal(c.Meta())
166+
}
167+
168+
func (c Cert) String() string {
169+
data, err := json.MarshalIndent(c, "", " ")
170+
if err != nil {
171+
return err.Error()
172+
}
173+
return string(data)
174+
}
175+
176+
///////////////////////////////////////////////////////////////////////////////
177+
// PUBLIC METHODS
178+
179+
// Return metadata from a cert
180+
func (c Cert) Meta() schema.CertMeta {
181+
signerNamePtr := func() *string {
182+
if c.Signer != nil {
183+
return types.StringPtr(c.Signer.Name)
184+
} else {
185+
return nil
186+
}
187+
}
188+
return schema.CertMeta{
189+
Name: c.Name,
190+
Signer: signerNamePtr(),
191+
SerialNumber: fmt.Sprintf("%x", c.x509.SerialNumber),
192+
Subject: c.x509.Subject.String(),
193+
NotBefore: c.x509.NotBefore,
194+
NotAfter: c.x509.NotAfter,
195+
IPs: c.x509.IPAddresses,
196+
Hosts: c.x509.DNSNames,
197+
IsCA: c.IsCA(),
198+
KeyType: c.keyType(),
199+
KeyBits: c.keySubtype(),
200+
}
201+
}
202+
203+
// Return metadata from a cert
204+
func (c Cert) SubjectMeta() schema.NameMeta {
205+
fieldPtr := func(field []string) *string {
206+
if len(field) > 0 {
207+
return types.StringPtr(field[0])
208+
} else {
209+
return nil
210+
}
211+
}
212+
return schema.NameMeta{
213+
CommonName: c.x509.Subject.CommonName,
214+
Org: fieldPtr(c.x509.Subject.Organization),
215+
Unit: fieldPtr(c.x509.Subject.OrganizationalUnit),
216+
Country: fieldPtr(c.x509.Subject.Country),
217+
City: fieldPtr(c.x509.Subject.Locality),
218+
State: fieldPtr(c.x509.Subject.Province),
219+
StreetAddress: fieldPtr(c.x509.Subject.StreetAddress),
220+
PostalCode: fieldPtr(c.x509.Subject.PostalCode),
221+
}
222+
}
223+
224+
// Return true if the certificate is a certificate authority
225+
func (c *Cert) IsCA() bool {
226+
return c.x509.IsCA
227+
}
228+
229+
// Return the private key, or nil
230+
func (c *Cert) PrivateKey() any {
231+
return c.priv
232+
}
233+
234+
// Return the public key, or nil
235+
func (c *Cert) PublicKey() any {
236+
switch k := c.priv.(type) {
237+
case *rsa.PrivateKey:
238+
return &k.PublicKey
239+
case *ecdsa.PrivateKey:
240+
return &k.PublicKey
241+
case ed25519.PrivateKey:
242+
return k.Public().(ed25519.PublicKey)
243+
default:
244+
return nil
245+
}
246+
}
247+
248+
// Output certificate as PEM format
249+
func (c *Cert) Write(w io.Writer) error {
250+
return pem.Encode(w, &pem.Block{Type: PemTypeCertificate, Bytes: c.x509.Raw})
251+
}
252+
253+
// Write the private key as PEM format
254+
func (c *Cert) WritePrivateKey(w io.Writer) error {
255+
data, err := x509.MarshalPKCS8PrivateKey(c.priv)
256+
if err != nil {
257+
return err
258+
}
259+
return pem.Encode(w, &pem.Block{Type: PemTypePrivateKey, Bytes: data})
260+
}
261+
262+
///////////////////////////////////////////////////////////////////////////////
263+
// PRIVATE METHODS
264+
265+
func (c *Cert) keyType() string {
266+
switch c.priv.(type) {
267+
case *rsa.PrivateKey:
268+
return keyTypeRSA
269+
case *ecdsa.PrivateKey:
270+
return keyTypeECDSA
271+
default:
272+
return ""
273+
}
274+
}
275+
276+
func (c *Cert) keySubtype() string {
277+
switch k := c.priv.(type) {
278+
case *rsa.PrivateKey:
279+
return fmt.Sprintf("%d", k.N.BitLen())
280+
case *ecdsa.PrivateKey:
281+
return k.Params().Name
282+
default:
283+
return ""
284+
}
285+
}

0 commit comments

Comments
 (0)