Skip to content

Commit f5f4e4a

Browse files
authored
feat(signserver): support encrypted PEM in TLS connection with SignServer (#1865)
Signed-off-by: Jose I. Paris <[email protected]>
1 parent a34fe53 commit f5f4e4a

File tree

6 files changed

+91
-9
lines changed

6 files changed

+91
-9
lines changed

app/cli/cmd/attestation_push.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ func newAttestationPushCmd() *cobra.Command {
3131
pkPath, bundle string
3232
annotationsFlag []string
3333
signServerCAPath string
34-
// Client certificate for SignServer auth
34+
// Client certificate and passphrase for SignServer auth
3535
signServerAuthCertPath string
36+
signServerAuthCertPass string
3637
bypassPolicyCheck bool
3738
)
3839

@@ -71,6 +72,7 @@ func newAttestationPushCmd() *cobra.Command {
7172
SignServerOpts: &action.SignServerOpts{
7273
CAPath: signServerCAPath,
7374
AuthClientCertPath: signServerAuthCertPath,
75+
AuthClientCertPass: signServerAuthCertPass,
7476
},
7577
})
7678
if err != nil {
@@ -131,6 +133,7 @@ func newAttestationPushCmd() *cobra.Command {
131133

132134
cmd.Flags().StringVar(&signServerCAPath, "signserver-ca-path", "", "custom CA to be used for SignServer TLS connection")
133135
cmd.Flags().StringVar(&signServerAuthCertPath, "signserver-client-cert", "", "path to client certificate in PEM format for authenticated SignServer TLS connection")
136+
cmd.Flags().StringVar(&signServerAuthCertPass, "signserver-client-pass", "", "certificate passphrase for authenticated SignServer TLS connection")
134137
cmd.Flags().BoolVar(&bypassPolicyCheck, exceptionFlagName, false, "do not fail this command on policy violations enforcement")
135138

136139
return cmd

app/cli/internal/action/attestation_push.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ type AttestationPushOpts struct {
4444
type SignServerOpts struct {
4545
// CA certificate for TLS connection
4646
CAPath string
47-
// (optional) Client cert for mutual TLS authentication
48-
AuthClientCertPath string
47+
// (optional) Client cert and passphrase for mutual TLS authentication
48+
AuthClientCertPath, AuthClientCertPass string
4949
}
5050

5151
type AttestationResult struct {
@@ -161,6 +161,7 @@ func (action *AttestationPush) Run(ctx context.Context, attestationID string, ru
161161
signerOpts.SignServerOpts = &signer.SignServerOpts{
162162
CAPath: action.signServerOpts.CAPath,
163163
AuthClientCertPath: action.signServerOpts.AuthClientCertPath,
164+
AuthClientCertPass: action.signServerOpts.AuthClientCertPass,
164165
}
165166
}
166167
sig, err := signer.GetSigner(action.keyPath, action.Logger, signerOpts)

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ require (
9191
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.8
9292
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.8
9393
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.8
94+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78
9495
gitlab.com/gitlab-org/security-products/analyzers/report/v5 v5.3.0
9596
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed
9697
google.golang.org/genproto/googleapis/bytestream v0.0.0-20240903143218-8af14fe29dc1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1419,6 +1419,8 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
14191419
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
14201420
github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
14211421
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
1422+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
1423+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
14221424
github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
14231425
github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=
14241426
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=

pkg/attestation/signer/signer.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ type Opts struct {
3535
type SignServerOpts struct {
3636
// CA certificate for TLS connection
3737
CAPath string
38-
// (optional) Client cert for mutual TLS authentication
39-
AuthClientCertPath string
38+
// (optional) Client cert and passphrase for mutual TLS authentication
39+
AuthClientCertPath, AuthClientCertPass string
4040
}
4141

4242
// GetSigner creates a new Signer based on input parameters
@@ -54,7 +54,8 @@ func GetSigner(keyPath string, logger zerolog.Logger, opts *Opts) (sigstoresigne
5454
}
5555
signer = signserver.NewSigner(host, worker,
5656
signserver.WithCAPath(opts.SignServerOpts.CAPath),
57-
signserver.WithClientCertPath(opts.SignServerOpts.AuthClientCertPath))
57+
signserver.WithClientCertPath(opts.SignServerOpts.AuthClientCertPath),
58+
signserver.WithClientCertPass(opts.SignServerOpts.AuthClientCertPass))
5859
} else {
5960
signer = cosign.NewSigner(keyPath, logger)
6061
}

pkg/attestation/signer/signserver/signserver.go

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"crypto"
2121
"crypto/tls"
2222
"crypto/x509"
23+
"encoding/pem"
2324
"errors"
2425
"fmt"
2526
"io"
@@ -30,14 +31,15 @@ import (
3031
"strings"
3132

3233
sigstoresigner "github.com/sigstore/sigstore/pkg/signature"
34+
"github.com/youmark/pkcs8"
3335
)
3436

3537
// ReferenceScheme is the scheme to be used when using signserver keys. Ex: signserver://host/worker
3638
const ReferenceScheme = "signserver"
3739

3840
// Signer implements a signer for SignServer
3941
type Signer struct {
40-
host, worker, caPath, clientCertPath string
42+
host, worker, caPath, clientCertPath, clientCertPass string
4143
}
4244

4345
type SignerOpt func(*Signer)
@@ -53,6 +55,11 @@ func WithClientCertPath(clientCertPath string) SignerOpt {
5355
s.clientCertPath = clientCertPath
5456
}
5557
}
58+
func WithClientCertPass(clientCertPass string) SignerOpt {
59+
return func(s *Signer) {
60+
s.clientCertPass = clientCertPass
61+
}
62+
}
5663

5764
var _ sigstoresigner.Signer = (*Signer)(nil)
5865

@@ -122,10 +129,30 @@ func (s Signer) SignMessage(message io.Reader, _ ...sigstoresigner.SignOption) (
122129
}
123130

124131
if s.clientCertPath != "" {
125-
cert, err := tls.LoadX509KeyPair(s.clientCertPath, s.clientCertPath)
132+
var cert tls.Certificate
133+
pemBytes, err := os.ReadFile(s.clientCertPath)
126134
if err != nil {
127-
return nil, fmt.Errorf("failed to load client cert and key: %w", err)
135+
return nil, fmt.Errorf("failed to read client cert file: %w", err)
128136
}
137+
if s.clientCertPass != "" {
138+
cert, err = certFromPem(pemBytes)
139+
if err != nil {
140+
return nil, fmt.Errorf("failed to read client cert file: %w", err)
141+
}
142+
// decrypt the private key
143+
key, err := privateKeyFromPem(pemBytes, s.clientCertPass)
144+
if err != nil {
145+
return nil, fmt.Errorf("failed to load private key: %w", err)
146+
}
147+
cert.PrivateKey = key
148+
} else {
149+
// Try with unencrypted PEM
150+
cert, err = tls.LoadX509KeyPair(s.clientCertPath, s.clientCertPath)
151+
if err != nil {
152+
return nil, fmt.Errorf("failed to load client cert and key: %w", err)
153+
}
154+
}
155+
129156
if tlsConfig == nil {
130157
tlsConfig = &tls.Config{MinVersion: tls.VersionTLS12}
131158
}
@@ -165,3 +192,50 @@ func ParseKeyReference(keyPath string) (string, string, error) {
165192
}
166193
return parts[0], parts[1], nil
167194
}
195+
196+
func certFromPem(pemBytes []byte) (tls.Certificate, error) {
197+
var cert tls.Certificate
198+
for {
199+
var certDERBlock *pem.Block
200+
certDERBlock, pemBytes = pem.Decode(pemBytes)
201+
if certDERBlock == nil {
202+
break
203+
}
204+
if certDERBlock.Type == "CERTIFICATE" {
205+
cert.Certificate = append(cert.Certificate, certDERBlock.Bytes)
206+
}
207+
}
208+
return cert, nil
209+
}
210+
211+
func privateKeyFromPem(pemBytes []byte, password string) (any, error) {
212+
var keyDERBlock *pem.Block
213+
for {
214+
keyDERBlock, pemBytes = pem.Decode(pemBytes)
215+
if keyDERBlock == nil {
216+
return nil, errors.New("failed to find any PEM data in key input")
217+
}
218+
219+
if keyDERBlock.Type == "PRIVATE KEY" || strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY") {
220+
break
221+
}
222+
}
223+
224+
// Try with RFC1423
225+
var key any
226+
derBytes, err := x509.DecryptPEMBlock(keyDERBlock, []byte(password))
227+
if err != nil {
228+
// try with PKCS#8 encryption (not supported by stdlib)
229+
key, err = pkcs8.ParsePKCS8PrivateKey(keyDERBlock.Bytes, []byte(password))
230+
if err != nil {
231+
return nil, fmt.Errorf("failed to decrypt private key: %w", err)
232+
}
233+
} else {
234+
key, err = x509.ParsePKCS8PrivateKey(derBytes)
235+
if err != nil {
236+
return nil, fmt.Errorf("failed to parse private key: %w", err)
237+
}
238+
}
239+
240+
return key, nil
241+
}

0 commit comments

Comments
 (0)