Skip to content

Commit 508698c

Browse files
authored
feat(signserver): Add support to SignServer key references for signing (#959)
Signed-off-by: Jose I. Paris <[email protected]>
1 parent e1f3ea0 commit 508698c

File tree

4 files changed

+186
-14
lines changed

4 files changed

+186
-14
lines changed

app/cli/cmd/attestation_push.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,13 @@ import (
2626
)
2727

2828
func newAttestationPushCmd() *cobra.Command {
29-
var pkPath, bundle string
30-
var annotationsFlag []string
29+
var (
30+
pkPath, bundle string
31+
annotationsFlag []string
32+
signServerCAPath string
33+
signServerAuthUser, signServerAuthPass string
34+
)
35+
3136
cmd := &cobra.Command{
3237
Use: "push",
3338
Short: "generate and push the attestation to the control plane",
@@ -57,7 +62,9 @@ func newAttestationPushCmd() *cobra.Command {
5762
return fmt.Errorf("getting executable information: %w", err)
5863
}
5964
a, err := action.NewAttestationPush(&action.AttestationPushOpts{
60-
ActionsOpts: actionOpts, KeyPath: pkPath, BundlePath: bundle, CLIVersion: info.Version, CLIDigest: info.Digest,
65+
ActionsOpts: actionOpts, KeyPath: pkPath, BundlePath: bundle,
66+
CLIVersion: info.Version, CLIDigest: info.Digest,
67+
SignServerCAPath: signServerCAPath,
6168
})
6269
if err != nil {
6370
return fmt.Errorf("failed to load action: %w", err)
@@ -102,5 +109,9 @@ func newAttestationPushCmd() *cobra.Command {
102109
cmd.Flags().StringVar(&bundle, "bundle", "", "output a Sigstore bundle to the provided path ")
103110
flagAttestationID(cmd)
104111

112+
cmd.Flags().StringVar(&signServerCAPath, "signserver-ca-path", "", "custom CA to be used for SignServer communications")
113+
cmd.Flags().StringVar(&signServerAuthUser, "signserver-auth-user", "", "")
114+
cmd.Flags().StringVar(&signServerAuthPass, "signserver-auth-pass", "", "")
115+
105116
return cmd
106117
}

app/cli/internal/action/attestation_push.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import (
3333
type AttestationPushOpts struct {
3434
*ActionsOpts
3535
KeyPath, CLIVersion, CLIDigest, BundlePath string
36+
37+
SignServerCAPath string
3638
}
3739

3840
type AttestationResult struct {
@@ -45,6 +47,7 @@ type AttestationPush struct {
4547
*ActionsOpts
4648
c *crafter.Crafter
4749
keyPath, cliVersion, cliDigest, bundlePath string
50+
signServerCAPath string
4851
}
4952

5053
func NewAttestationPush(cfg *AttestationPushOpts) (*AttestationPush, error) {
@@ -54,12 +57,13 @@ func NewAttestationPush(cfg *AttestationPushOpts) (*AttestationPush, error) {
5457
}
5558

5659
return &AttestationPush{
57-
ActionsOpts: cfg.ActionsOpts,
58-
c: c,
59-
keyPath: cfg.KeyPath,
60-
cliVersion: cfg.CLIVersion,
61-
cliDigest: cfg.CLIDigest,
62-
bundlePath: cfg.BundlePath,
60+
ActionsOpts: cfg.ActionsOpts,
61+
c: c,
62+
keyPath: cfg.KeyPath,
63+
cliVersion: cfg.CLIVersion,
64+
cliDigest: cfg.CLIDigest,
65+
bundlePath: cfg.BundlePath,
66+
signServerCAPath: cfg.SignServerCAPath,
6367
}, nil
6468
}
6569

@@ -132,7 +136,13 @@ func (action *AttestationPush) Run(ctx context.Context, attestationID string, ru
132136
// Indicate that we are done with the attestation
133137
action.c.CraftingState.Attestation.FinishedAt = timestamppb.New(time.Now())
134138

135-
sig := signer.GetSigner(action.keyPath, action.Logger, pb.NewSigningServiceClient(action.CPConnection))
139+
sig, err := signer.GetSigner(action.keyPath, action.Logger, &signer.Opts{
140+
SignServerCAPath: action.signServerCAPath,
141+
Vaultclient: pb.NewSigningServiceClient(action.CPConnection),
142+
})
143+
if err != nil {
144+
return nil, fmt.Errorf("creating signer: %w", err)
145+
}
136146
renderer, err := renderer.NewAttestationRenderer(action.c.CraftingState, action.cliVersion, action.cliDigest, sig,
137147
renderer.WithLogger(action.Logger), renderer.WithBundleOutputPath(action.bundlePath))
138148
if err != nil {

internal/attestation/signer/signer.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,38 @@
1616
package signer
1717

1818
import (
19+
"fmt"
20+
"strings"
21+
1922
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
2023
"github.com/chainloop-dev/chainloop/internal/attestation/signer/chainloop"
2124
"github.com/chainloop-dev/chainloop/internal/attestation/signer/cosign"
25+
"github.com/chainloop-dev/chainloop/internal/attestation/signer/signserver"
2226
"github.com/rs/zerolog"
2327
sigstoresigner "github.com/sigstore/sigstore/pkg/signature"
2428
)
2529

30+
type Opts struct {
31+
SignServerCAPath string
32+
Vaultclient pb.SigningServiceClient
33+
}
34+
2635
// GetSigner creates a new Signer based on input parameters
27-
func GetSigner(keyPath string, logger zerolog.Logger, client pb.SigningServiceClient) sigstoresigner.Signer {
36+
func GetSigner(keyPath string, logger zerolog.Logger, opts *Opts) (sigstoresigner.Signer, error) {
2837
var signer sigstoresigner.Signer
2938
if keyPath != "" {
30-
signer = cosign.NewSigner(keyPath, logger)
39+
if strings.HasPrefix(keyPath, signserver.ReferenceScheme) {
40+
host, worker, err := signserver.ParseKeyReference(keyPath)
41+
if err != nil {
42+
return nil, fmt.Errorf("failed to parse key: %w", err)
43+
}
44+
signer = signserver.NewSigner(host, worker, opts.SignServerCAPath)
45+
} else {
46+
signer = cosign.NewSigner(keyPath, logger)
47+
}
3148
} else {
32-
signer = chainloop.NewSigner(client, logger)
49+
signer = chainloop.NewSigner(opts.Vaultclient, logger)
3350
}
3451

35-
return signer
52+
return signer, nil
3653
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
//
2+
// Copyright 2024 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package signserver
17+
18+
import (
19+
"bytes"
20+
"crypto"
21+
"crypto/tls"
22+
"crypto/x509"
23+
"errors"
24+
"fmt"
25+
"io"
26+
"mime/multipart"
27+
"net/http"
28+
"net/url"
29+
"os"
30+
"strings"
31+
32+
sigstoresigner "github.com/sigstore/sigstore/pkg/signature"
33+
)
34+
35+
// ReferenceScheme is the scheme to be used when using signserver keys. Ex: signserver://host/worker
36+
const ReferenceScheme = "signserver"
37+
38+
// Signer implements a signer for SignServer
39+
type Signer struct {
40+
host, worker, caPath string
41+
}
42+
43+
var _ sigstoresigner.Signer = (*Signer)(nil)
44+
45+
func NewSigner(host, worker, caPath string) *Signer {
46+
return &Signer{
47+
host: host,
48+
worker: worker,
49+
caPath: caPath,
50+
}
51+
}
52+
53+
func (s Signer) PublicKey(_ ...sigstoresigner.PublicKeyOption) (crypto.PublicKey, error) {
54+
return nil, errors.New("public key not yet supported for SignServer")
55+
}
56+
57+
// SignMessage Signs a message by calling to SignServer
58+
func (s Signer) SignMessage(message io.Reader, _ ...sigstoresigner.SignOption) ([]byte, error) {
59+
url, err := url.Parse(fmt.Sprintf("https://%s/signserver/process", s.host))
60+
if err != nil {
61+
return nil, fmt.Errorf("failed to parse url: %w", err)
62+
}
63+
64+
body := &bytes.Buffer{}
65+
mpwriter := multipart.NewWriter(body)
66+
67+
// Send worker name
68+
err = mpwriter.WriteField("workerName", s.worker)
69+
if err != nil {
70+
return nil, fmt.Errorf("failed to write field: %w", err)
71+
}
72+
73+
// Send payload
74+
part, err := mpwriter.CreateFormFile("file", "attestation.json")
75+
if err != nil {
76+
return nil, fmt.Errorf("failed to create form field: %w", err)
77+
}
78+
_, err = io.Copy(part, message)
79+
if err != nil {
80+
return nil, fmt.Errorf("failed to copy message: %w", err)
81+
}
82+
err = mpwriter.Close()
83+
if err != nil {
84+
return nil, fmt.Errorf("failed to close multipart writer: %w", err)
85+
}
86+
87+
req, err := http.NewRequest("POST", url.String(), body)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to create request: %w", err)
90+
}
91+
req.Header.Add("Content-Type", mpwriter.FormDataContentType())
92+
client := &http.Client{}
93+
94+
var caPool *x509.CertPool
95+
if s.caPath != "" {
96+
caPool = x509.NewCertPool()
97+
caContents, err := os.ReadFile(s.caPath)
98+
if err != nil {
99+
return nil, fmt.Errorf("failed to read ca cert: %w", err)
100+
}
101+
caPool.AppendCertsFromPEM(caContents)
102+
client.Transport = &http.Transport{
103+
TLSClientConfig: &tls.Config{RootCAs: caPool, MinVersion: tls.VersionTLS12}}
104+
}
105+
106+
res, err := client.Do(req)
107+
if err != nil {
108+
return nil, fmt.Errorf("failed to send request: %w", err)
109+
}
110+
111+
if res.StatusCode != http.StatusOK {
112+
return nil, fmt.Errorf("failed to send request: status %d", res.StatusCode)
113+
}
114+
115+
resBytes, err := io.ReadAll(res.Body)
116+
if err != nil {
117+
return nil, fmt.Errorf("failed to read response body: %w", err)
118+
}
119+
120+
return resBytes, nil
121+
}
122+
123+
// ParseKeyReference interprets a key reference for SignServer
124+
func ParseKeyReference(keyPath string) (string, string, error) {
125+
parts := strings.SplitAfter(keyPath, "://")
126+
if len(parts) != 2 {
127+
return "", "", fmt.Errorf("invalid key path: %s", keyPath)
128+
}
129+
parts = strings.Split(parts[1], "/")
130+
if len(parts) != 2 {
131+
return "", "", fmt.Errorf("invalid key path: %s", keyPath)
132+
}
133+
return parts[0], parts[1], nil
134+
}

0 commit comments

Comments
 (0)