Skip to content

Commit d91ad61

Browse files
committed
Added cert
1 parent be1414a commit d91ad61

File tree

4 files changed

+598
-0
lines changed

4 files changed

+598
-0
lines changed

pkg/cert/cert.go

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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+
"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+
92+
// Create the certificate
93+
signer := cert.Signer
94+
if signer == nil {
95+
signer = cert
96+
}
97+
if data, err := x509.CreateCertificate(rand.Reader, &cert.x509, &signer.x509, cert.PublicKey(), signer.priv); err != nil {
98+
return nil, err
99+
} else {
100+
cert.x509.Raw = data
101+
}
102+
103+
// Return the certificate
104+
return cert, nil
105+
}
106+
107+
// Read a certificate
108+
func Read(r io.Reader) (*Cert, error) {
109+
cert := new(Cert)
110+
data, err := io.ReadAll(r)
111+
if err != nil {
112+
return nil, err
113+
}
114+
115+
// Read until EOF
116+
for len(data) > 0 {
117+
// Decode the PEM block
118+
block, rest := pem.Decode(data)
119+
if block == nil {
120+
return nil, fmt.Errorf("invalid PEM block")
121+
}
122+
123+
// Parse the block
124+
switch block.Type {
125+
case PemTypeCertificate:
126+
c, err := x509.ParseCertificate(block.Bytes)
127+
if err != nil {
128+
return nil, err
129+
} else {
130+
cert.x509 = *c
131+
}
132+
case PemTypePrivateKey:
133+
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
134+
if err != nil {
135+
return nil, err
136+
} else {
137+
cert.priv = key
138+
}
139+
default:
140+
return nil, fmt.Errorf("invalid PEM block type: %q", block.Type)
141+
}
142+
143+
// Move to next block
144+
data = rest
145+
}
146+
147+
// Return success
148+
return cert, nil
149+
}
150+
151+
///////////////////////////////////////////////////////////////////////////////
152+
// STRINGIFY
153+
154+
func (c Cert) MarshalJSON() ([]byte, error) {
155+
return json.Marshal(c.Meta())
156+
}
157+
158+
func (c Cert) String() string {
159+
data, err := json.MarshalIndent(c, "", " ")
160+
if err != nil {
161+
return err.Error()
162+
}
163+
return string(data)
164+
}
165+
166+
///////////////////////////////////////////////////////////////////////////////
167+
// PUBLIC METHODS
168+
169+
// Return metadata from a cert
170+
func (c Cert) Meta() schema.CertMeta {
171+
signerNamePtr := func() *string {
172+
if c.Signer == nil {
173+
return nil
174+
}
175+
return types.StringPtr(c.Signer.Name)
176+
}
177+
return schema.CertMeta{
178+
Name: c.Name,
179+
Signer: signerNamePtr(),
180+
SerialNumber: fmt.Sprintf("%x", c.x509.SerialNumber),
181+
Subject: c.x509.Subject.String(),
182+
NotBefore: c.x509.NotBefore,
183+
NotAfter: c.x509.NotAfter,
184+
IPs: c.x509.IPAddresses,
185+
Hosts: c.x509.DNSNames,
186+
IsCA: c.IsCA(),
187+
KeyType: c.keyType(),
188+
KeyBits: c.keySubtype(),
189+
}
190+
}
191+
192+
// Return true if the certificate is a certificate authority
193+
func (c *Cert) IsCA() bool {
194+
return c.x509.IsCA
195+
}
196+
197+
// Return the private key, or nil
198+
func (c *Cert) PrivateKey() any {
199+
return c.priv
200+
}
201+
202+
// Return the public key, or nil
203+
func (c *Cert) PublicKey() any {
204+
switch k := c.priv.(type) {
205+
case *rsa.PrivateKey:
206+
return &k.PublicKey
207+
case *ecdsa.PrivateKey:
208+
return &k.PublicKey
209+
case ed25519.PrivateKey:
210+
return k.Public().(ed25519.PublicKey)
211+
default:
212+
return nil
213+
}
214+
}
215+
216+
// Output certificate as PEM format
217+
func (c *Cert) Write(w io.Writer) error {
218+
return pem.Encode(w, &pem.Block{Type: PemTypeCertificate, Bytes: c.x509.Raw})
219+
}
220+
221+
// Write the private key as PEM format
222+
func (c *Cert) WritePrivateKey(w io.Writer) error {
223+
data, err := x509.MarshalPKCS8PrivateKey(c.priv)
224+
if err != nil {
225+
return err
226+
}
227+
return pem.Encode(w, &pem.Block{Type: PemTypePrivateKey, Bytes: data})
228+
}
229+
230+
///////////////////////////////////////////////////////////////////////////////
231+
// PRIVATE METHODS
232+
233+
func (c *Cert) keyType() string {
234+
switch c.priv.(type) {
235+
case *rsa.PrivateKey:
236+
return keyTypeRSA
237+
case *ecdsa.PrivateKey:
238+
return keyTypeECDSA
239+
default:
240+
return ""
241+
}
242+
}
243+
244+
func (c *Cert) keySubtype() string {
245+
switch k := c.priv.(type) {
246+
case *rsa.PrivateKey:
247+
return fmt.Sprintf("%d", k.N.BitLen())
248+
case *ecdsa.PrivateKey:
249+
return k.Params().Name
250+
default:
251+
return ""
252+
}
253+
}

pkg/cert/cert_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package cert_test
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
"time"
7+
8+
// Packages
9+
cert "github.com/mutablelogic/go-server/pkg/cert"
10+
assert "github.com/stretchr/testify/assert"
11+
)
12+
13+
func Test_Cert_001(t *testing.T) {
14+
assert := assert.New(t)
15+
16+
t.Run("1", func(t *testing.T) {
17+
cert, err := cert.New()
18+
if assert.Error(err) {
19+
assert.Nil(cert)
20+
t.Log(err)
21+
}
22+
})
23+
24+
t.Run("2", func(t *testing.T) {
25+
cert, err := cert.New(cert.WithRSAKey(0))
26+
if assert.Error(err) {
27+
assert.Nil(cert)
28+
t.Log(err)
29+
}
30+
})
31+
32+
t.Run("3", func(t *testing.T) {
33+
cert, err := cert.New(
34+
cert.WithRSAKey(0),
35+
cert.WithExpiry(time.Hour),
36+
)
37+
if assert.NoError(err) {
38+
assert.NotNil(cert)
39+
assert.NotNil(cert.PrivateKey())
40+
assert.NotNil(cert.PublicKey())
41+
}
42+
})
43+
44+
}
45+
46+
func Test_Cert_002(t *testing.T) {
47+
assert := assert.New(t)
48+
49+
ca, err := cert.New(
50+
cert.WithRSAKey(0),
51+
cert.WithExpiry(time.Hour),
52+
cert.WithCA(),
53+
)
54+
if assert.NoError(err) {
55+
assert.NotNil(ca)
56+
}
57+
58+
t.Run("1", func(t *testing.T) {
59+
var data bytes.Buffer
60+
cert, err := cert.New(
61+
cert.WithRSAKey(0),
62+
cert.WithExpiry(time.Hour),
63+
)
64+
if assert.NoError(err) {
65+
assert.NoError(cert.Write(&data))
66+
t.Log(data.String())
67+
}
68+
if assert.NoError(err) {
69+
assert.NoError(cert.WritePrivateKey(&data))
70+
t.Log(data.String())
71+
}
72+
})
73+
74+
t.Run("2", func(t *testing.T) {
75+
var data bytes.Buffer
76+
cert, err := cert.New(
77+
cert.WithRSAKey(0),
78+
cert.WithExpiry(time.Hour),
79+
)
80+
if assert.NoError(err) {
81+
assert.NoError(cert.Write(&data))
82+
t.Log(data.String())
83+
}
84+
if assert.NoError(err) {
85+
assert.NoError(cert.WritePrivateKey(&data))
86+
t.Log(data.String())
87+
}
88+
})
89+
}

0 commit comments

Comments
 (0)