Skip to content

Commit

Permalink
Fix TSA signing. (#196)
Browse files Browse the repository at this point in the history
This fixes signing commits with a timestamp authority. To do this we
forked the ietf-cms library from smimesign in order to do the following:

- Fixed timestamp before/after checks when the code signing cert issue time
  matches the signed timestamp. This check should be inclusive. Keyless
  signing is particularly susceptible to this since we issue certs on
  the fly.
- Allowed for configurable cert pools for TSA and commit verification.
  Previously smimesign assumed these would be one in the same, based on
  the system pool. This allows for different verification options to be
  configued in order to prevent letting the TSA certs verify the commit
  signature cert.
- Adds GITSIGN_TIMESTAMP_CERT option to allow loading in of TSA certs.
- Renames GITSIGN_TIMESTAMP_AUTHORITY to GITSIGN_TIMESTAMP_URL.

We're choosing to fork here since upstream hasn't been responsive
to PRs.

Signed-off-by: Billy Lynch <billy@chainguard.dev>

Signed-off-by: Billy Lynch <billy@chainguard.dev>
  • Loading branch information
wlynch authored Nov 28, 2022
1 parent e9d33e8 commit ce5a1ad
Show file tree
Hide file tree
Showing 21 changed files with 3,596 additions and 8 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ jobs:
- name: Check license headers
run: |
set -e
addlicense -l apache -c 'The Sigstore Authors' -v -ignore *.yml -ignore *.yaml *
addlicense -l apache -c 'The Sigstore Authors' -v \
-ignore "*.yml" \
-ignore "*.yaml" \
-ignore "internal/fork/**" \
*
git diff --exit-code
golangci:
Expand Down
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ issues:
run:
issues-exit-code: 1
timeout: 10m
skip-dirs:
- internal/fork
534 changes: 534 additions & 0 deletions docs/timestamp.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ require (
github.com/spf13/cobra v1.6.1
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2
)

require (
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2181,6 +2181,7 @@ golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/root/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func commandSign(o *options, s *gsio.Streams, args ...string) error {

sig, cert, tlog, err := git.Sign(ctx, rekor, userIdent, dataBuf.Bytes(), signature.SignOptions{
Detached: o.FlagDetachedSignature,
TimestampAuthority: o.Config.TimestampAuthority,
TimestampAuthority: o.Config.TimestampURL,
Armor: o.FlagArmor,
IncludeCerts: o.FlagIncludeCerts,
})
Expand Down
27 changes: 26 additions & 1 deletion internal/commands/root/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package root
import (
"bytes"
"context"
"crypto/x509"
"errors"
"fmt"
"io"
Expand All @@ -29,6 +30,7 @@ import (
gsio "github.com/sigstore/gitsign/internal/io"
"github.com/sigstore/gitsign/internal/rekor"
"github.com/sigstore/gitsign/pkg/git"
"github.com/sigstore/sigstore/pkg/cryptoutils"
)

// commandSign implements gitsign commit verification.
Expand Down Expand Up @@ -72,7 +74,30 @@ func commandVerify(o *options, s *gsio.Streams, args ...string) error {
if err != nil {
return fmt.Errorf("error getting certificate root: %w", err)
}
cv, err := git.NewCertVerifier(git.WithRootPool(root), git.WithIntermediatePool(intermediate))

tsa, err := x509.SystemCertPool()
if err != nil {
return fmt.Errorf("error getting system root pool: %w", err)
}
if path := o.Config.TimestampCert; path != "" {
f, err := os.Open(path)
if err != nil {
return err
}
cert, err := cryptoutils.LoadCertificatesFromPEM(f)
if err != nil {
return fmt.Errorf("error loading certs from %s: %w", path, err)
}
for _, c := range cert {
tsa.AddCert(c)
}
}

cv, err := git.NewCertVerifier(
git.WithRootPool(root),
git.WithIntermediatePool(intermediate),
git.WithTimestampCertPool(tsa),
)
if err != nil {
return fmt.Errorf("error creating git cert verifier: %w", err)
}
Expand Down
11 changes: 9 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ type Config struct {
ConnectorID string

// Timestamp Authority address to use to get a trusted timestamp
TimestampAuthority string
TimestampURL string
// Timestamp Authority PEM encoded cert(s) to use for verification.
TimestampCert string

// Path to log status output. Helpful for debugging when no TTY is available in the environment.
LogPath string
Expand Down Expand Up @@ -95,7 +97,8 @@ func Get() (*Config, error) {
out.RedirectURL = envOrValue(fmt.Sprintf("%s_OIDC_REDIRECT_URL", prefix), out.RedirectURL)
out.Issuer = envOrValue(fmt.Sprintf("%s_OIDC_ISSUER", prefix), out.Issuer)
out.ConnectorID = envOrValue(fmt.Sprintf("%s_CONNECTOR_ID", prefix), out.ConnectorID)
out.TimestampAuthority = envOrValue(fmt.Sprintf("%s_TIMESTAMP_AUTHORITY", prefix), out.TimestampAuthority)
out.TimestampURL = envOrValue(fmt.Sprintf("%s_TIMESTAMP_URL", prefix), out.TimestampURL)
out.TimestampCert = envOrValue(fmt.Sprintf("%s_TIMESTAMP_CERT", prefix), out.TimestampCert)
}

out.LogPath = envOrValue("GITSIGN_LOG", out.LogPath)
Expand Down Expand Up @@ -160,6 +163,10 @@ func applyGitOptions(out *Config, cfg map[string]string) {
out.LogPath = v
case strings.EqualFold(k, "gitsign.connectorID"):
out.ConnectorID = v
case strings.EqualFold(k, "gitsign.timestampURL"):
out.TimestampURL = v
case strings.EqualFold(k, "gitsign.timestampCert"):
out.TimestampCert = v
}
}
}
Expand Down
21 changes: 21 additions & 0 deletions internal/fork/ietf-cms/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2017 GitHub, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
64 changes: 64 additions & 0 deletions internal/fork/ietf-cms/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# CMS

This package is forked from [github/smimesign](https://github.com/github/smimesign) with the following changes:

- Adds inclusive checking for cert timestamps in timestamp authorities (https://github.com/github/smimesign/pull/121)
- Fixes tests for MacOS due to regressions in return types in Go 1.18 crypto libraries (https://github.com/golang/go/issues/52010)
- Adds support for separate cert pools for cert validation and TSA validation.

[CMS (Cryptographic Message Syntax)](https://tools.ietf.org/html/rfc5652) is a syntax for signing, digesting, and encrypting arbitrary messages. It evolved from PKCS#7 and is the basis for higher level protocols such as S/MIME. This package implements the SignedData CMS content-type, allowing users to digitally sign data as well as verify data signed by others.

## Signing and Verifying Data

High level APIs are provided for signing a message with a certificate and key:

```go
msg := []byte("some data")
cert, _ := x509.ParseCertificate(someCertificateData)
key, _ := x509.ParseECPrivateKey(somePrivateKeyData)

der, _ := cms.Sign(msg, []*x509.Certificate{cert}, key)

////
/// At another time, in another place...
//

sd, _ := ParseSignedData(der)
if err, _ := sd.Verify(x509.VerifyOptions{}); err != nil {
panic(err)
}
```

By default, CMS SignedData includes the original message. High level APIs are also available for creating and verifying detached signatures:

```go
msg := []byte("some data")
cert, _ := x509.ParseCertificate(someCertificateData)
key, _ := x509.ParseECPrivateKey(somePrivateKeyData)

der, _ := cms.SignDetached(msg, cert, key)

////
/// At another time, in another place...
//

sd, _ := ParseSignedData(der)
if err, _ := sd.VerifyDetached(msg, x509.VerifyOptions{}); err != nil {
panic(err)
}
```

## Timestamping

Because certificates expire and can be revoked, it is may be helpful to attach certified timestamps to signatures, proving that they existed at a given time. RFC3161 timestamps can be added to signatures like so:

```go
signedData, _ := NewSignedData([]byte("Hello, world!"))
signedData.Sign(identity.Chain(), identity.PrivateKey)
signedData.AddTimestamps("http://timestamp.digicert.com")

derEncoded, _ := signedData.ToDER()
io.Copy(os.Stdout, bytes.NewReader(derEncoded))
```

Verification functions implicitly verify timestamps as well. Without a timestamp, verification will fail if the certificate is no longer valid.
166 changes: 166 additions & 0 deletions internal/fork/ietf-cms/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package cms

import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/asn1"
"io"
"io/ioutil"
"math/big"
"net/http"
"time"

"github.com/github/smimesign/fakeca"
"github.com/github/smimesign/ietf-cms/oid"
"github.com/github/smimesign/ietf-cms/protocol"
"github.com/sigstore/gitsign/internal/fork/ietf-cms/timestamp"
)

var (
// fake PKI setup
root = fakeca.New(fakeca.IsCA)
otherRoot = fakeca.New(fakeca.IsCA)

intermediateKey, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
intermediate = root.Issue(fakeca.IsCA, fakeca.PrivateKey(intermediateKey))

leaf = intermediate.Issue(
fakeca.NotBefore(time.Now().Add(-time.Hour)),
fakeca.NotAfter(time.Now().Add(time.Hour)),
)

rootOpts = x509.VerifyOptions{Roots: root.ChainPool()}
otherRootOpts = x509.VerifyOptions{Roots: otherRoot.ChainPool()}
intermediateOpts = x509.VerifyOptions{Roots: intermediate.ChainPool()}

// fake timestamp authority setup
tsa = &testTSA{ident: intermediate.Issue()}
thc = &testHTTPClient{tsa}
)

func init() {
timestamp.DefaultHTTPClient = thc
}

type testTSA struct {
ident *fakeca.Identity
sn int64
hookInfo func(timestamp.Info) timestamp.Info
hookToken func(*protocol.SignedData) *protocol.SignedData
hookResponse func(timestamp.Response) timestamp.Response
}

func (tt *testTSA) Clear() {
tt.hookInfo = nil
tt.hookToken = nil
tt.hookResponse = nil
}

func (tt *testTSA) HookInfo(hook func(timestamp.Info) timestamp.Info) {
tt.Clear()
tt.hookInfo = hook
}

func (tt *testTSA) HookToken(hook func(*protocol.SignedData) *protocol.SignedData) {
tt.Clear()
tt.hookToken = hook
}

func (tt *testTSA) HookResponse(hook func(timestamp.Response) timestamp.Response) {
tt.Clear()
tt.hookResponse = hook
}

func (tt *testTSA) nextSN() *big.Int {
defer func() { tt.sn++ }()
return big.NewInt(tt.sn)
}

func (tt *testTSA) Do(req timestamp.Request) (timestamp.Response, error) {
info := timestamp.Info{
Version: 1,
Policy: asn1.ObjectIdentifier{1, 2, 3},
SerialNumber: tt.nextSN(),
GenTime: time.Now(),
MessageImprint: req.MessageImprint,
Nonce: req.Nonce,
}

if tt.hookInfo != nil {
info = tt.hookInfo(info)
}

eciDER, err := asn1.Marshal(info)
if err != nil {
panic(err)
}

eci, err := protocol.NewEncapsulatedContentInfo(oid.ContentTypeTSTInfo, eciDER)
if err != nil {
panic(err)
}

tst, err := protocol.NewSignedData(eci)
if err != nil {
panic(err)
}

if err = tst.AddSignerInfo(tsa.ident.Chain(), tsa.ident.PrivateKey); err != nil {
panic(err)
}

if tt.hookToken != nil {
tt.hookToken(tst)
}

ci, err := tst.ContentInfo()
if err != nil {
panic(err)
}

resp := timestamp.Response{
Status: timestamp.PKIStatusInfo{Status: 0},
TimeStampToken: ci,
}

if tt.hookResponse != nil {
resp = tt.hookResponse(resp)
}

return resp, nil
}

type testHTTPClient struct {
tt *testTSA
}

func (thc *testHTTPClient) Do(httpReq *http.Request) (*http.Response, error) {
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, httpReq.Body); err != nil {
return nil, err
}

var tsReq timestamp.Request
if _, err := asn1.Unmarshal(buf.Bytes(), &tsReq); err != nil {
return nil, err
}

tsResp, err := thc.tt.Do(tsReq)
if err != nil {
return nil, err
}

respDER, err := asn1.Marshal(tsResp)
if err != nil {
return nil, err
}

return &http.Response{
StatusCode: 200,
Header: http.Header{"Content-Type": {"application/timestamp-reply"}},
Body: ioutil.NopCloser(bytes.NewReader(respDER)),
}, nil
}
Loading

0 comments on commit ce5a1ad

Please sign in to comment.