Skip to content

Commit

Permalink
add 'cosign sign' command-line parameters for mTLS (sigstore#3052)
Browse files Browse the repository at this point in the history
* add 'cosign sign' command-line parameters for mTLS

Add command-line parameters for key/cert/cacert used for
the connection to the TSA server.

Fixes sigstore#3006

Signed-off-by: Dmitry S <dsavints@gmail.com>

* simplify ServerName validation

Signed-off-by: Dmitry S <dsavints@gmail.com>

* add 'tsa-mtls.sh to test mTLS to TSA

Signed-off-by: Dmitry S <dsavints@gmail.com>

* add tsa mtls test to github actions

Signed-off-by: Dmitry S <dsavints@gmail.com>

* use smallstep's step to generate test certificates

Signed-off-by: Dmitry S <dsavints@gmail.com>

* in e2e_tsa_mtls.sh, build timestamp-server only if not found

Signed-off-by: Dmitry S <dsavints@gmail.com>

* add test/gencert utility, copyright

Signed-off-by: Dmitry S <dsavints@gmail.com>

---------

Signed-off-by: Dmitry S <dsavints@gmail.com>
  • Loading branch information
dmitris authored Jul 20, 2023
1 parent 1861be1 commit f633221
Show file tree
Hide file tree
Showing 15 changed files with 465 additions and 6 deletions.
23 changes: 19 additions & 4 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,26 @@ jobs:
check-latest: true

- uses: imjasonh/setup-crane@00c9e93efa4e1138c9a7a5c594acd6c75a2fbf0c # v0.3

- name: Run e2e_test_secrets.sh
shell: bash
run: ./test/e2e_test_secrets.sh

- name: Run e2e_test_attach.sh
shell: bash
run: ./test/e2e_test_attach.sh

e2e-tsa-mtls:
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
with:
go-version: '1.20.x'
check-latest: true

- uses: imjasonh/setup-crane@00c9e93efa4e1138c9a7a5c594acd6c75a2fbf0c # v0.3

- name: Run e2e_tsa_mtls.sh
shell: bash
run: make && PATH="$PWD:$PATH" ./test/e2e_tsa_mtls.sh
4 changes: 4 additions & 0 deletions cmd/cosign/cli/options/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ type KeyOpts struct {
OIDCProvider string // Specify which OIDC credential provider to use for keyless signer
BundlePath string
SkipConfirmation bool
TSAClientCACert string
TSAClientCert string
TSAClientKey string
TSAServerName string // expected SAN field in the TSA server's certificate - https://pkg.go.dev/crypto/tls#Config.ServerName
TSAServerURL string
RFC3161TimestampPath string
TSACertChainPath string
Expand Down
18 changes: 18 additions & 0 deletions cmd/cosign/cli/options/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ type SignOptions struct {
Attachment string
SkipConfirmation bool
TlogUpload bool
TSAClientCACert string
TSAClientCert string
TSAClientKey string
TSAServerName string
TSAServerURL string
IssueCertificate bool
SignContainerIdentity string
Expand Down Expand Up @@ -104,9 +108,23 @@ func (o *SignOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&o.TlogUpload, "tlog-upload", true,
"whether or not to upload to the tlog")

cmd.Flags().StringVar(&o.TSAClientCACert, "timestamp-client-cacert", "",
"path to the X.509 CA certificate file in PEM format to be used for the connection to the TSA Server")

cmd.Flags().StringVar(&o.TSAClientCert, "timestamp-client-cert", "",
"path to the X.509 certificate file in PEM format to be used for the connection to the TSA Server")

cmd.Flags().StringVar(&o.TSAClientKey, "timestamp-client-key", "",
"path to the X.509 private key file in PEM format to be used, together with the 'timestamp-client-cert' value, for the connection to the TSA Server")

cmd.Flags().StringVar(&o.TSAServerName, "timestamp-server-name", "",
"SAN name to use as the 'ServerName' tls.Config field to verify the mTLS connection to the TSA Server")

cmd.Flags().StringVar(&o.TSAServerURL, "timestamp-server-url", "",
"url to the Timestamp RFC3161 server, default none. Must be the path to the API to request timestamp responses, e.g. https://freetsa.org/tsr")

_ = cmd.Flags().SetAnnotation("certificate", cobra.BashCompFilenameExt, []string{"cert"})

cmd.Flags().BoolVar(&o.IssueCertificate, "issue-certificate", false,
"issue a code signing certificate from Fulcio, even if a key is provided")

Expand Down
4 changes: 4 additions & 0 deletions cmd/cosign/cli/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ race conditions or (worse) malicious tampering.
OIDCDisableProviders: o.OIDC.DisableAmbientProviders,
OIDCProvider: o.OIDC.Provider,
SkipConfirmation: o.SkipConfirmation,
TSAClientCACert: o.TSAClientCACert,
TSAClientCert: o.TSAClientCert,
TSAClientKey: o.TSAClientKey,
TSAServerName: o.TSAServerName,
TSAServerURL: o.TSAServerURL,
IssueCertificateForExistingKey: o.IssueCertificate,
}
Expand Down
11 changes: 10 additions & 1 deletion cmd/cosign/cli/sign/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,16 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko opti
}

if ko.TSAServerURL != "" {
s = tsa.NewSigner(s, client.NewTSAClient(ko.TSAServerURL))
if ko.TSAClientCACert == "" && ko.TSAClientCert == "" { // no mTLS params or custom CA
s = tsa.NewSigner(s, client.NewTSAClient(ko.TSAServerURL))
} else {
s = tsa.NewSigner(s, client.NewTSAClientMTLS(ko.TSAServerURL,
ko.TSAClientCACert,
ko.TSAClientCert,
ko.TSAClientKey,
ko.TSAServerName,
))
}
}
shouldUpload, err := ShouldUploadToTlog(ctx, ko, digest, signOpts.TlogUpload)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions doc/cosign_sign.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

96 changes: 95 additions & 1 deletion internal/pkg/cosign/tsa/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ package client

import (
"bytes"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net"
"net/http"
"os"
"time"
Expand All @@ -37,17 +40,97 @@ type TimestampAuthorityClientImpl struct {

// URL is the path to the API to request timestamp responses
URL string
// CACert is the filepath to the PEM-encoded CA certificate for the connection to the TSA server
CACert string
// Cert is the filepath to the PEM-encoded certificate for the connection to the TSA server
Cert string
// Cert is the filepath to the PEM-encoded key corresponding to the certificate for the connection to the TSA server
Key string
// ServerName is the expected SAN value in the server's certificate - used for https://pkg.go.dev/crypto/tls#Config.ServerName
ServerName string

// Timeout is the request timeout
Timeout time.Duration
}

const defaultTimeout = 10 * time.Second

func getHTTPTransport(cacertFilename, certFilename, keyFilename, serverName string, timeout time.Duration) (http.RoundTripper, error) {
if timeout == 0 {
timeout = defaultTimeout
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{
CipherSuites: []uint16{
// TLS 1.3 cipher suites.
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_CHACHA20_POLY1305_SHA256,
},
MinVersion: tls.VersionTLS13,
SessionTicketsDisabled: true,
},
// the rest of default settings are copied verbatim from https://golang.org/pkg/net/http/#DefaultTransport
// to minimize surprises for the users.
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: timeout,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
var pool *x509.CertPool
if cacertFilename != "" {
f, err := os.Open(cacertFilename)
if err != nil {
return nil, err
}
defer f.Close()
caCertBytes, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("unable to read CA certs from %s: %w", cacertFilename, err)
}
pool = x509.NewCertPool()
if !pool.AppendCertsFromPEM(caCertBytes) {
return nil, fmt.Errorf("no valid CA certs found in %s", cacertFilename)
}
tr.TLSClientConfig.RootCAs = pool
}
if certFilename != "" && keyFilename != "" {
cert, err := tls.LoadX509KeyPair(certFilename, keyFilename)
if err != nil {
return nil, fmt.Errorf("unable to read CA certs from cert %s, key %s: %w",
certFilename, keyFilename, err)
}
tr.TLSClientConfig.Certificates = []tls.Certificate{cert}
}

if serverName != "" {
tr.TLSClientConfig.ServerName = serverName
}
return tr, nil
}

// GetTimestampResponse sends a timestamp query to a timestamp authority, returning a timestamp response.
// The query and response are defined by RFC 3161.
func (t *TimestampAuthorityClientImpl) GetTimestampResponse(tsq []byte) ([]byte, error) {
client := http.Client{
Timeout: t.Timeout,
}

// if mTLS-related fields are set, create a custom Transport for the Client
if t.CACert != "" || t.Cert != "" {
tr, err := getHTTPTransport(t.CACert, t.Cert, t.Key, t.ServerName, t.Timeout)
if err != nil {
return nil, err
}
client.Transport = tr
}

req, err := http.NewRequest("POST", t.URL, bytes.NewReader(tsq))
if err != nil {
return nil, errors.Wrap(err, "error creating HTTP request")
Expand Down Expand Up @@ -79,5 +162,16 @@ func (t *TimestampAuthorityClientImpl) GetTimestampResponse(tsq []byte) ([]byte,
}

func NewTSAClient(url string) *TimestampAuthorityClientImpl {
return &TimestampAuthorityClientImpl{URL: url, Timeout: 10 * time.Second}
return &TimestampAuthorityClientImpl{URL: url, Timeout: defaultTimeout}
}

func NewTSAClientMTLS(url, cacert, cert, key, serverName string) *TimestampAuthorityClientImpl {
return &TimestampAuthorityClientImpl{
URL: url,
CACert: cacert,
Cert: cert,
Key: key,
ServerName: serverName,
Timeout: defaultTimeout,
}
}
91 changes: 91 additions & 0 deletions test/e2e_tsa_mtls.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
#
# Copyright 2023 The Sigstore Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -exuo pipefail

## Requirements
# - cosign
# - crane
# - go

CERT_BASE="test/testdata"

# the certificates listed below are generated with the `gen-tsa-mtls-certs.sh` script.
TIMESTAMP_CACERT=$CERT_BASE/tsa-mtls-ca.crt
TIMESTAMP_CLIENT_CERT=$CERT_BASE/tsa-mtls-client.crt
TIMESTAMP_CLIENT_KEY=$CERT_BASE/tsa-mtls-client.key
TIMESTAMP_SERVER_CERT=$CERT_BASE/tsa-mtls-server.crt
TIMESTAMP_SERVER_KEY=$CERT_BASE/tsa-mtls-server.key
TIMESTAMP_SERVER_NAME="server.example.com"
TIMESTAMP_SERVER_URL=https://localhost:3000/api/v1/timestamp

set +e
command -v timestamp-server >& /dev/null
exit_code=$?
set -e
if [[ $exit_code != 0 ]]; then
rm -fr /tmp/timestamp-authority
git clone https://github.com/sigstore/timestamp-authority /tmp/timestamp-authority
pushd /tmp/timestamp-authority
make
export PATH="/tmp/timestamp-authority/bin:$PATH"
popd
fi

timestamp-server serve --disable-ntp-monitoring --tls-host 0.0.0.0 --tls-port 3000 \
--scheme https --tls-ca $TIMESTAMP_CACERT --tls-key $TIMESTAMP_SERVER_KEY \
--tls-certificate $TIMESTAMP_SERVER_CERT &


IMG=${IMAGE_URI_DIGEST:-}
if [[ "$#" -ge 1 ]]; then
IMG=$1
elif [[ -z "${IMG}" ]]; then
# Upload an image to ttl.sh - commands from https://docs.sigstore.dev/cosign/keyless/
SRC_IMAGE=busybox
SRC_DIGEST=$(crane digest busybox)
IMAGE_URI=ttl.sh/$(uuidgen | head -c 8 | tr 'A-Z' 'a-z')
crane cp $SRC_IMAGE@$SRC_DIGEST $IMAGE_URI:3h
IMG=$IMAGE_URI@$SRC_DIGEST
fi

echo "IMG (IMAGE_URI_DIGEST): $IMG, TIMESTAMP_SERVER_URL: $TIMESTAMP_SERVER_URL"

rm -f *.pem import-cosign.* key.pem


# use gencert to generate CA, keys and certificates
echo "generate keys and certificates with gencert"

passwd=$(uuidgen | head -c 32 | tr 'A-Z' 'a-z')
rm -f *.pem import-cosign.* && go run test/gencert/main.go && COSIGN_PASSWORD="$passwd" cosign import-key-pair --key key.pem

COSIGN_PASSWORD="$passwd" cosign sign --timestamp-server-url "${TIMESTAMP_SERVER_URL}" \
--timestamp-client-cacert ${TIMESTAMP_CACERT} --timestamp-client-cert ${TIMESTAMP_CLIENT_CERT} \
--timestamp-client-key ${TIMESTAMP_CLIENT_KEY} --timestamp-server-name ${TIMESTAMP_SERVER_NAME}\
--upload=true --tlog-upload=false --key import-cosign.key --certificate-chain cacert.pem --cert cert.pem $IMG

# key is now longer needed
rm -f key.pem import-cosign.*

echo "cosign verify:"
cosign verify --insecure-ignore-tlog --insecure-ignore-sct --check-claims=true \
--certificate-identity-regexp 'xyz@nosuchprovider.com' --certificate-oidc-issuer-regexp '.*' \
--certificate-chain cacert.pem $IMG

# cleanup
rm -fr ca-key.pem cacert.pem cert.pem /tmp/timestamp-authority
pkill -f 'timestamp-server'
Loading

0 comments on commit f633221

Please sign in to comment.