Skip to content

Commit

Permalink
feat: Timestamp (#207)
Browse files Browse the repository at this point in the history
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
  • Loading branch information
Two-Hearts committed Jul 8, 2024
1 parent a1c0af6 commit faac9b7
Show file tree
Hide file tree
Showing 35 changed files with 1,936 additions and 526 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.21
require (
github.com/fxamacker/cbor/v2 v2.7.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/notaryproject/tspclient-go v0.0.0-20240702050734-d91848411058
github.com/veraison/go-cose v1.1.0
golang.org/x/crypto v0.24.0
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/notaryproject/tspclient-go v0.0.0-20240702050734-d91848411058 h1:FlGmQAwbf78rw12fXT4+9EkmD9+ZWuqH08v0fE3sqHc=
github.com/notaryproject/tspclient-go v0.0.0-20240702050734-d91848411058/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs=
github.com/veraison/go-cose v1.1.0 h1:AalPS4VGiKavpAzIlBjrn7bhqXiXi4jbMYY/2+UC+4o=
github.com/veraison/go-cose v1.1.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
Expand Down
31 changes: 31 additions & 0 deletions internal/oid/oid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright The Notary Project 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.

package oid

import "encoding/asn1"

// KeyUsage (id-ce-keyUsage) is defined in RFC 5280
//
// Reference: https://www.rfc-editor.org/rfc/rfc5280.html#section-4.2.1.3
var KeyUsage = asn1.ObjectIdentifier{2, 5, 29, 15}

// ExtKeyUsage (id-ce-extKeyUsage) is defined in RFC 5280
//
// Reference: https://www.rfc-editor.org/rfc/rfc5280.html#section-4.2.1.12
var ExtKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37}

// Timestamping (id-kp-timeStamping) is defined in RFC 3161 2.3
//
// Reference: https://datatracker.ietf.org/doc/html/rfc3161#section-2.3
var Timestamping = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8}
Binary file added internal/timestamp/testdata/TimeStampToken.p7s
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added internal/timestamp/testdata/tsaRootCert.crt
Binary file not shown.
65 changes: 65 additions & 0 deletions internal/timestamp/timestamp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright The Notary Project 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.

// Package timestamp provides functionalities of timestamp countersignature
package timestamp

import (
"crypto/x509"

"github.com/notaryproject/notation-core-go/signature"
nx509 "github.com/notaryproject/notation-core-go/x509"
"github.com/notaryproject/tspclient-go"
)

// Timestamp generates a timestamp request and sends to TSA. It then validates
// the TSA certificate chain against Notary Project certificate and signature
// algorithm requirements.
// On success, it returns the full bytes of the timestamp token received from
// TSA.
//
// Reference: https://github.com/notaryproject/specifications/blob/v1.0.0/specs/signature-specification.md#leaf-certificates
func Timestamp(req *signature.SignRequest, opts tspclient.RequestOptions) ([]byte, error) {
tsaRequest, err := tspclient.NewRequest(opts)
if err != nil {
return nil, err
}
ctx := req.Context()
resp, err := req.Timestamper.Timestamp(ctx, tsaRequest)
if err != nil {
return nil, err
}
token, err := resp.SignedToken()
if err != nil {
return nil, err
}
info, err := token.Info()
if err != nil {
return nil, err
}
timestamp, err := info.Validate(opts.Content)
if err != nil {
return nil, err
}
tsaCertChain, err := token.Verify(ctx, x509.VerifyOptions{
CurrentTime: timestamp.Value,
Roots: req.TSARootCAs,
})
if err != nil {
return nil, err
}
if err := nx509.ValidateTimestampingCertChain(tsaCertChain); err != nil {
return nil, err
}
return resp.TimestampToken.FullBytes, nil
}
200 changes: 200 additions & 0 deletions internal/timestamp/timestamp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Copyright The Notary Project 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.

package timestamp

import (
"context"
"crypto"
"crypto/x509"
"encoding/asn1"
"errors"
"os"
"strings"
"testing"

"github.com/notaryproject/notation-core-go/signature"
nx509 "github.com/notaryproject/notation-core-go/x509"
"github.com/notaryproject/tspclient-go"
"github.com/notaryproject/tspclient-go/pki"
)

const rfc3161TSAurl = "http://rfc3161timestamp.globalsign.com/advanced"

func TestTimestamp(t *testing.T) {
rootCerts, err := nx509.ReadCertificateFile("testdata/tsaRootCert.crt")
if err != nil || len(rootCerts) == 0 {
t.Fatal("failed to read root CA certificate:", err)
}
rootCert := rootCerts[0]
rootCAs := x509.NewCertPool()
rootCAs.AddCert(rootCert)

// --------------- Success case ----------------------------------
timestamper, err := tspclient.NewHTTPTimestamper(nil, rfc3161TSAurl)
if err != nil {
t.Fatal(err)
}
req := &signature.SignRequest{
Timestamper: timestamper,
TSARootCAs: rootCAs,
}
opts := tspclient.RequestOptions{
Content: []byte("notation"),
HashAlgorithm: crypto.SHA256,
}
_, err = Timestamp(req, opts)
if err != nil {
t.Fatal(err)
}

// ------------- Failure cases ------------------------
opts = tspclient.RequestOptions{
Content: []byte("notation"),
HashAlgorithm: crypto.SHA1,
}
expectedErr := "malformed timestamping request: unsupported hashing algorithm: SHA-1"
_, err = Timestamp(req, opts)
assertErrorEqual(expectedErr, err, t)

req = &signature.SignRequest{
Timestamper: dummyTimestamper{},
TSARootCAs: rootCAs,
}
opts = tspclient.RequestOptions{
Content: []byte("notation"),
HashAlgorithm: crypto.SHA256,
NoNonce: true,
}
expectedErr = "failed to timestamp"
_, err = Timestamp(req, opts)
if err == nil || !strings.Contains(err.Error(), expectedErr) {
t.Fatalf("expected error message to contain %s, but got %v", expectedErr, err)
}

req = &signature.SignRequest{
Timestamper: dummyTimestamper{
respWithRejectedStatus: true,
},
TSARootCAs: rootCAs,
}
expectedErr = "invalid timestamping response: invalid response with status code 2: rejected"
_, err = Timestamp(req, opts)
assertErrorEqual(expectedErr, err, t)

req = &signature.SignRequest{
Timestamper: dummyTimestamper{
invalidTSTInfo: true,
},
TSARootCAs: rootCAs,
}
expectedErr = "cannot unmarshal TSTInfo from timestamp token: asn1: structure error: tags don't match (23 vs {class:0 tag:16 length:3 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue:<nil> tag:<nil> stringType:0 timeType:24 set:false omitEmpty:false} Time @89"
_, err = Timestamp(req, opts)
assertErrorEqual(expectedErr, err, t)

opts = tspclient.RequestOptions{
Content: []byte("mismatch"),
HashAlgorithm: crypto.SHA256,
NoNonce: true,
}
req = &signature.SignRequest{
Timestamper: dummyTimestamper{
failValidate: true,
},
TSARootCAs: rootCAs,
}
expectedErr = "invalid TSTInfo: mismatched message"
_, err = Timestamp(req, opts)
assertErrorEqual(expectedErr, err, t)

opts = tspclient.RequestOptions{
Content: []byte("notation"),
HashAlgorithm: crypto.SHA256,
NoNonce: true,
}
req = &signature.SignRequest{
Timestamper: dummyTimestamper{
invalidSignature: true,
},
TSARootCAs: rootCAs,
}
expectedErr = "failed to verify signed token: cms verification failure: crypto/rsa: verification error"
_, err = Timestamp(req, opts)
assertErrorEqual(expectedErr, err, t)
}

func assertErrorEqual(expected string, err error, t *testing.T) {
if err == nil || expected != err.Error() {
t.Fatalf("Expected error \"%v\" but was \"%v\"", expected, err)
}
}

type dummyTimestamper struct {
respWithRejectedStatus bool
invalidTSTInfo bool
failValidate bool
invalidSignature bool
}

func (d dummyTimestamper) Timestamp(context.Context, *tspclient.Request) (*tspclient.Response, error) {
if d.respWithRejectedStatus {
return &tspclient.Response{
Status: pki.StatusInfo{
Status: pki.StatusRejection,
},
}, nil
}
if d.invalidTSTInfo {
token, err := os.ReadFile("testdata/TimeStampTokenWithInvalidTSTInfo.p7s")
if err != nil {
return nil, err
}
return &tspclient.Response{
Status: pki.StatusInfo{
Status: pki.StatusGranted,
},
TimestampToken: asn1.RawValue{
FullBytes: token,
},
}, nil
}
if d.failValidate {
token, err := os.ReadFile("testdata/TimeStampToken.p7s")
if err != nil {
return nil, err
}
return &tspclient.Response{
Status: pki.StatusInfo{
Status: pki.StatusGranted,
},
TimestampToken: asn1.RawValue{
FullBytes: token,
},
}, nil
}
if d.invalidSignature {
token, err := os.ReadFile("testdata/TimeStampTokenWithInvalidSignature.p7s")
if err != nil {
return nil, err
}
return &tspclient.Response{
Status: pki.StatusInfo{
Status: pki.StatusGranted,
},
TimestampToken: asn1.RawValue{
FullBytes: token,
},
}, nil
}
return nil, errors.New("failed to timestamp")
}
32 changes: 27 additions & 5 deletions revocation/ocsp/ocsp.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,24 @@ import (
"golang.org/x/crypto/ocsp"
)

// Purpose is an enum for purpose of the certificate chain whose OCSP status
// is checked
type Purpose int

const (
// PurposeCodeSigning means the certificate chain is a code signing chain
PurposeCodeSigning Purpose = iota

// PurposeTimestamping means the certificate chain is a timestamping chain
PurposeTimestamping
)

// Options specifies values that are needed to check OCSP revocation
type Options struct {
CertChain []*x509.Certificate
SigningTime time.Time
HTTPClient *http.Client
CertChain []*x509.Certificate
CertChainPurpose Purpose // default value is `PurposeCodeSigning`
SigningTime time.Time
HTTPClient *http.Client
}

const (
Expand All @@ -64,8 +77,17 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) {
// Since this is using authentic signing time, signing time may be zero.
// Thus, it is better to pass nil here than fail for a cert's NotBefore
// being after zero time
if err := coreX509.ValidateCodeSigningCertChain(opts.CertChain, nil); err != nil {
return nil, result.InvalidChainError{Err: err}
switch opts.CertChainPurpose {
case PurposeCodeSigning:
if err := coreX509.ValidateCodeSigningCertChain(opts.CertChain, nil); err != nil {
return nil, result.InvalidChainError{Err: err}
}
case PurposeTimestamping:
if err := coreX509.ValidateTimestampingCertChain(opts.CertChain); err != nil {
return nil, result.InvalidChainError{Err: err}
}
default:
return nil, result.InvalidChainError{Err: fmt.Errorf("unknown certificate chain purpose %v", opts.CertChainPurpose)}
}

certResults := make([]*result.CertRevocationResult, len(opts.CertChain))
Expand Down
Loading

0 comments on commit faac9b7

Please sign in to comment.