Skip to content

Commit 969bdc7

Browse files
committed
Adds crypto/spiffe
Adds spiffe package to crypto. This is a refactored version of the existing `pkg/security` package. This new package is more modulated and fuller test coverage. This package has been moved so that it can be both imported by dapr & components-contrib, as well as making the package more suitable for further development to support X.509 Component auth. dapr/proposals#51 Also moves in `test/utils` from dapr to `crypto/test` for shared usage. Signed-off-by: joshvanl <[email protected]>
1 parent e33fbab commit 969bdc7

File tree

13 files changed

+2042
-51
lines changed

13 files changed

+2042
-51
lines changed

crypto/pem/pem.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
Copyright 2023 The Dapr Authors
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package pem
15+
16+
import (
17+
"bytes"
18+
"crypto"
19+
"crypto/ecdsa"
20+
"crypto/ed25519"
21+
"crypto/rsa"
22+
"crypto/x509"
23+
"encoding/pem"
24+
"errors"
25+
"fmt"
26+
)
27+
28+
// DecodePEMCertificatesChain takes a PEM-encoded x509 certificates byte array
29+
// and returns all certificates in a slice of x509.Certificate objects.
30+
// Expects certificates to be a chain with leaf certificate to be first in the
31+
// byte array.
32+
func DecodePEMCertificatesChain(crtb []byte) ([]*x509.Certificate, error) {
33+
certs, err := DecodePEMCertificates(crtb)
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
for i := 0; i < len(certs)-1; i++ {
39+
if certs[i].CheckSignatureFrom(certs[i+1]) != nil {
40+
return nil, errors.New("certificate chain is not valid")
41+
}
42+
}
43+
44+
return certs, nil
45+
}
46+
47+
// DecodePEMCertificatesChain takes a PEM-encoded x509 certificates byte array
48+
// and returns all certificates in a slice of x509.Certificate objects.
49+
func DecodePEMCertificates(crtb []byte) ([]*x509.Certificate, error) {
50+
certs := []*x509.Certificate{}
51+
for len(crtb) > 0 {
52+
var err error
53+
var cert *x509.Certificate
54+
55+
cert, crtb, err = decodeCertificatePEM(crtb)
56+
if err != nil {
57+
return nil, err
58+
}
59+
if cert != nil {
60+
// it's a cert, add to pool
61+
certs = append(certs, cert)
62+
}
63+
}
64+
65+
if len(certs) == 0 {
66+
return nil, errors.New("no certificates found")
67+
}
68+
69+
return certs, nil
70+
}
71+
72+
func decodeCertificatePEM(crtb []byte) (*x509.Certificate, []byte, error) {
73+
block, crtb := pem.Decode(crtb)
74+
if block == nil {
75+
return nil, nil, nil
76+
}
77+
if block.Type != "CERTIFICATE" {
78+
return nil, nil, nil
79+
}
80+
c, err := x509.ParseCertificate(block.Bytes)
81+
return c, crtb, err
82+
}
83+
84+
// DecodePEMPrivateKey takes a key PEM byte array and returns an object that
85+
// represents either an RSA or EC private key.
86+
func DecodePEMPrivateKey(key []byte) (crypto.Signer, error) {
87+
block, _ := pem.Decode(key)
88+
if block == nil {
89+
return nil, errors.New("key is not PEM encoded")
90+
}
91+
92+
switch block.Type {
93+
case "EC PRIVATE KEY":
94+
return x509.ParseECPrivateKey(block.Bytes)
95+
case "RSA PRIVATE KEY":
96+
return x509.ParsePKCS1PrivateKey(block.Bytes)
97+
case "PRIVATE KEY":
98+
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
99+
if err != nil {
100+
return nil, err
101+
}
102+
return key.(crypto.Signer), nil
103+
default:
104+
return nil, fmt.Errorf("unsupported block type %s", block.Type)
105+
}
106+
}
107+
108+
// EncodePrivateKey will encode a private key into PEM format.
109+
func EncodePrivateKey(key any) ([]byte, error) {
110+
var (
111+
keyBytes []byte
112+
err error
113+
blockType string
114+
)
115+
116+
switch key := key.(type) {
117+
case *ecdsa.PrivateKey, *ed25519.PrivateKey:
118+
keyBytes, err = x509.MarshalPKCS8PrivateKey(key)
119+
if err != nil {
120+
return nil, err
121+
}
122+
blockType = "PRIVATE KEY"
123+
default:
124+
return nil, fmt.Errorf("unsupported key type %T", key)
125+
}
126+
127+
return pem.EncodeToMemory(&pem.Block{
128+
Type: blockType, Bytes: keyBytes,
129+
}), nil
130+
}
131+
132+
// EncodeX509 will encode a single *x509.Certificate into PEM format.
133+
func EncodeX509(cert *x509.Certificate) ([]byte, error) {
134+
caPem := bytes.NewBuffer([]byte{})
135+
err := pem.Encode(caPem, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
136+
if err != nil {
137+
return nil, err
138+
}
139+
140+
return caPem.Bytes(), nil
141+
}
142+
143+
// EncodeX509Chain will encode a list of *x509.Certificates into a PEM format chain.
144+
// Self-signed certificates are not included as per
145+
// https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.2
146+
// Certificates are output in the order they're given; if the input is not ordered
147+
// as specified in RFC5246 section 7.4.2, the resulting chain might not be valid
148+
// for use in TLS.
149+
func EncodeX509Chain(certs []*x509.Certificate) ([]byte, error) {
150+
if len(certs) == 0 {
151+
return nil, errors.New("no certificates in chain")
152+
}
153+
154+
certPEM := bytes.NewBuffer([]byte{})
155+
for _, cert := range certs {
156+
if cert == nil {
157+
continue
158+
}
159+
160+
if cert.CheckSignatureFrom(cert) == nil {
161+
// Don't include self-signed certificate
162+
continue
163+
}
164+
165+
err := pem.Encode(certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
166+
if err != nil {
167+
return nil, err
168+
}
169+
}
170+
171+
return certPEM.Bytes(), nil
172+
}
173+
174+
// PublicKeysEqual compares two given public keys for equality.
175+
// The definition of "equality" depends on the type of the public keys.
176+
// Returns true if the keys are the same, false if they differ or an error if
177+
// the key type of `a` cannot be determined.
178+
func PublicKeysEqual(a, b crypto.PublicKey) (bool, error) {
179+
switch pub := a.(type) {
180+
case *rsa.PublicKey:
181+
return pub.Equal(b), nil
182+
case *ecdsa.PublicKey:
183+
return pub.Equal(b), nil
184+
case ed25519.PublicKey:
185+
return pub.Equal(b), nil
186+
default:
187+
return false, fmt.Errorf("unrecognised public key type: %T", a)
188+
}
189+
}

crypto/spiffe/spiffe.go

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
Copyright 2024 The Dapr Authors
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package spiffe
15+
16+
import (
17+
"context"
18+
"crypto/ecdsa"
19+
"crypto/elliptic"
20+
"crypto/rand"
21+
"crypto/x509"
22+
"errors"
23+
"fmt"
24+
"sync"
25+
"sync/atomic"
26+
"time"
27+
28+
"github.com/spiffe/go-spiffe/v2/svid/x509svid"
29+
"k8s.io/utils/clock"
30+
31+
"github.com/dapr/kit/logger"
32+
)
33+
34+
type (
35+
RequestSVIDFn func(context.Context, []byte) ([]*x509.Certificate, error)
36+
)
37+
38+
type Options struct {
39+
Log logger.Logger
40+
RequestSVIDFn RequestSVIDFn
41+
}
42+
43+
// SPIFFE is a readable/writeable store of a SPIFFE X.509 SVID.
44+
// Used to manage a workload SVID, and share read-only interfaces to consumers.
45+
type SPIFFE struct {
46+
currentSVID *x509svid.SVID
47+
requestSVIDFn RequestSVIDFn
48+
49+
log logger.Logger
50+
lock sync.RWMutex
51+
clock clock.Clock
52+
running atomic.Bool
53+
readyCh chan struct{}
54+
}
55+
56+
func New(opts Options) *SPIFFE {
57+
return &SPIFFE{
58+
requestSVIDFn: opts.RequestSVIDFn,
59+
log: opts.Log,
60+
clock: clock.RealClock{},
61+
readyCh: make(chan struct{}),
62+
}
63+
}
64+
65+
func (s *SPIFFE) Run(ctx context.Context) error {
66+
if !s.running.CompareAndSwap(false, true) {
67+
return errors.New("already running")
68+
}
69+
70+
s.lock.Lock()
71+
s.log.Info("Fetching initial identity certificate")
72+
initialCert, err := s.fetchIdentityCertificate(ctx)
73+
if err != nil {
74+
close(s.readyCh)
75+
s.lock.Unlock()
76+
return fmt.Errorf("failed to retrieve the initial identity certificate: %w", err)
77+
}
78+
79+
s.currentSVID = initialCert
80+
close(s.readyCh)
81+
s.lock.Unlock()
82+
83+
s.log.Infof("Security is initialized successfully")
84+
s.runRotation(ctx)
85+
86+
return nil
87+
}
88+
89+
// Ready blocks until SPIFFE is ready or the context is done which will return
90+
// the context error.
91+
func (s *SPIFFE) Ready(ctx context.Context) error {
92+
select {
93+
case <-ctx.Done():
94+
return ctx.Err()
95+
case <-s.readyCh:
96+
return nil
97+
}
98+
}
99+
100+
// runRotation starts up the manager responsible for renewing the workload
101+
// certificate. Receives the initial certificate to calculate the next rotation
102+
// time.
103+
func (s *SPIFFE) runRotation(ctx context.Context) {
104+
defer s.log.Debug("stopping workload cert expiry watcher")
105+
s.lock.RLock()
106+
cert := s.currentSVID.Certificates[0]
107+
s.lock.RUnlock()
108+
renewTime := renewalTime(cert.NotBefore, cert.NotAfter)
109+
s.log.Infof("Starting workload cert expiry watcher; current cert expires on: %s, renewing at %s",
110+
cert.NotAfter.String(), renewTime.String())
111+
112+
for {
113+
select {
114+
case <-s.clock.After(min(time.Minute, renewTime.Sub(s.clock.Now()))):
115+
if s.clock.Now().Before(renewTime) {
116+
continue
117+
}
118+
s.log.Infof("Renewing workload cert; current cert expires on: %s", cert.NotAfter.String())
119+
svid, err := s.fetchIdentityCertificate(ctx)
120+
if err != nil {
121+
s.log.Errorf("Error renewing identity certificate, trying again in 10 seconds: %s", err)
122+
select {
123+
case <-s.clock.After(10 * time.Second):
124+
continue
125+
case <-ctx.Done():
126+
return
127+
}
128+
}
129+
s.lock.Lock()
130+
s.currentSVID = svid
131+
cert = svid.Certificates[0]
132+
s.lock.Unlock()
133+
renewTime = renewalTime(cert.NotBefore, cert.NotAfter)
134+
s.log.Infof("Successfully renewed workload cert; new cert expires on: %s", cert.NotAfter.String())
135+
136+
case <-ctx.Done():
137+
return
138+
}
139+
}
140+
}
141+
142+
// fetchIdentityCertificate fetches a new SVID using the configured requester.
143+
func (s *SPIFFE) fetchIdentityCertificate(ctx context.Context) (*x509svid.SVID, error) {
144+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
145+
if err != nil {
146+
return nil, fmt.Errorf("failed to generate private key: %w", err)
147+
}
148+
149+
csrDER, err := x509.CreateCertificateRequest(rand.Reader, new(x509.CertificateRequest), key)
150+
if err != nil {
151+
return nil, fmt.Errorf("failed to create sidecar csr: %w", err)
152+
}
153+
154+
workloadcert, err := s.requestSVIDFn(ctx, csrDER)
155+
if err != nil {
156+
return nil, err
157+
}
158+
159+
if len(workloadcert) == 0 {
160+
return nil, errors.New("no certificates received from sentry")
161+
}
162+
163+
spiffeID, err := x509svid.IDFromCert(workloadcert[0])
164+
if err != nil {
165+
return nil, fmt.Errorf("error parsing spiffe id from newly signed certificate: %w", err)
166+
}
167+
168+
return &x509svid.SVID{
169+
ID: spiffeID,
170+
Certificates: workloadcert,
171+
PrivateKey: key,
172+
}, nil
173+
}
174+
175+
func (s *SPIFFE) SVIDSource() x509svid.Source {
176+
return &svidSource{spiffe: s}
177+
}
178+
179+
// renewalTime is 50% through the certificate validity period.
180+
func renewalTime(notBefore, notAfter time.Time) time.Time {
181+
return notBefore.Add(notAfter.Sub(notBefore) / 2)
182+
}

0 commit comments

Comments
 (0)