Skip to content

Commit 7c693ae

Browse files
Allow full issuing chain in response (#1082)
Fixes #1079 Per RFC3161, when the certReq field is set to true, the TSA's certificate will be present in the timestamp response, and optionally other certificates may be present. Other public TSAs provide the full issuing chain in the response. This PR adds a server configuration flag to include the full chain in the response if the certReq bit is true. Signed-off-by: Hayden B <8418760+haydentherapper@users.noreply.github.com>
1 parent 0fa3135 commit 7c693ae

File tree

5 files changed

+48
-5
lines changed

5 files changed

+48
-5
lines changed

cmd/timestamp-server/app/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ func init() {
6161
rootCmd.PersistentFlags().BoolVar(&httpPingOnly, "http-ping-only", false, "serve only /ping in the http server")
6262
rootCmd.PersistentFlags().String("timestamp-signer", "memory", "Timestamping authority signer. Valid options include: [kms, tink, memory, file]. Memory and file-based signers should only be used for testing")
6363
rootCmd.PersistentFlags().String("timestamp-signer-hash", "sha256", "Hash algorithm used by the signer. Must match the hash algorithm specified for a KMS or Tink key. Valid options include: [sha256, sha384, sha512]. Ignored for Memory signer.")
64+
rootCmd.PersistentFlags().Bool("include-chain-in-response", false, "Whether to include the issuing chain in the timestamp response when certReq is set in the timestamp request. When false, only the leaf certificate is included in the response.")
6465
// KMS flags
6566
rootCmd.PersistentFlags().String("kms-key-resource", "", "KMS key for signing timestamp responses. Valid options include: [gcpkms://resource, azurekms://resource, hashivault://resource, awskms://resource]")
6667
// Tink flags

pkg/api/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type API struct {
3838
tsaSignerHash crypto.Hash // hash algorithm used to hash pre-signed timestamps
3939
certChain []*x509.Certificate // timestamping cert chain
4040
certChainPem string // PEM encoded timestamping cert chain
41+
includeChain bool // Whether to include the full issuing chain or just the leaf certificate
4142
}
4243

4344
func NewAPI() (*API, error) {
@@ -91,6 +92,7 @@ func NewAPI() (*API, error) {
9192
tsaSignerHash: tsaSignerHash,
9293
certChain: certChain,
9394
certChainPem: string(certChainPEM),
95+
includeChain: viper.GetBool("include-chain-in-response"),
9496
}, nil
9597
}
9698

pkg/api/timestamp.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ func TimestampResponseHandler(params ts.GetTimestampResponseParams) middleware.R
171171
AddTSACertificate: req.Certificates,
172172
ExtraExtensions: req.Extensions,
173173
}
174+
if api.includeChain {
175+
tsStruct.Certificates = api.certChain[1:] // Issuing CA certificate down to root
176+
}
174177

175178
resp, err := tsStruct.CreateResponseWithOpts(api.certChain[0], api.tsaSigner, api.tsaSignerHash)
176179
if err != nil {

pkg/tests/api_test.go

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/sigstore/timestamp-authority/pkg/client"
3434
"github.com/sigstore/timestamp-authority/pkg/generated/client/timestamp"
3535
"github.com/sigstore/timestamp-authority/pkg/x509"
36+
"github.com/spf13/viper"
3637

3738
"github.com/go-openapi/runtime"
3839
"go.uber.org/goleak"
@@ -88,6 +89,7 @@ type timestampTestCase struct {
8889
includeCerts bool
8990
policyOID asn1.ObjectIdentifier
9091
hash crypto.Hash
92+
issuingChain bool
9193
}
9294

9395
func TestGetTimestampResponse(t *testing.T) {
@@ -112,6 +114,15 @@ func TestGetTimestampResponse(t *testing.T) {
112114
includeCerts: includeCerts,
113115
hash: hashFunc,
114116
},
117+
{
118+
name: "Request with Full Issuing Chain",
119+
reqMediaType: client.TimestampQueryMediaType,
120+
reqBytes: buildTimestampQueryReq(t, []byte(testArtifact), opts),
121+
nonce: testNonce,
122+
includeCerts: includeCerts,
123+
hash: hashFunc,
124+
issuingChain: true,
125+
},
115126
{
116127
name: "JSON Request",
117128
reqMediaType: client.JSONMediaType,
@@ -123,7 +134,12 @@ func TestGetTimestampResponse(t *testing.T) {
123134
}
124135

125136
for _, tc := range tests {
126-
url := createServer(t)
137+
var url string
138+
if !tc.issuingChain {
139+
url = createServer(t, func() { viper.Set("include-chain-in-response", false) })
140+
} else {
141+
url = createServer(t, func() { viper.Set("include-chain-in-response", true) })
142+
}
127143

128144
c, err := client.GetTimestampClient(url, client.WithContentType(tc.reqMediaType))
129145
if err != nil {
@@ -149,12 +165,30 @@ func TestGetTimestampResponse(t *testing.T) {
149165
}
150166

151167
// check certificate fields
152-
if len(tsr.Certificates) != 1 {
153-
t.Fatalf("test '%s': expected 1 certificate, got %d", tc.name, len(tsr.Certificates))
154-
}
155168
if !tsr.AddTSACertificate {
156169
t.Fatalf("test '%s': expected TSA certificate", tc.name)
157170
}
171+
if !tc.issuingChain {
172+
if len(tsr.Certificates) != 1 {
173+
t.Fatalf("test '%s': expected 1 certificate, got %d", tc.name, len(tsr.Certificates))
174+
}
175+
if tsr.Certificates[0].Subject.CommonName != "Test TSA Timestamping" {
176+
t.Fatalf("test '%s': expected subject to be 'Test TSA Timestamping', got %s", tc.name, tsr.Certificates[0].Subject.CommonName)
177+
}
178+
} else {
179+
if len(tsr.Certificates) != 3 {
180+
t.Fatalf("test '%s': expected 3 certificates, got %d", tc.name, len(tsr.Certificates))
181+
}
182+
if tsr.Certificates[0].Subject.CommonName != "Test TSA Timestamping" {
183+
t.Fatalf("test '%s': expected subject to be 'Test TSA Timestamping', got %s", tc.name, tsr.Certificates[0].Subject.CommonName)
184+
}
185+
if tsr.Certificates[1].Subject.CommonName != "Test TSA Intermediate" {
186+
t.Fatalf("test '%s': expected subject to be 'Test TSA Intermediate', got %s", tc.name, tsr.Certificates[1].Subject.CommonName)
187+
}
188+
if tsr.Certificates[2].Subject.CommonName != "Test TSA Root" {
189+
t.Fatalf("test '%s': expected subject to be 'Test TSA Root', got %s", tc.name, tsr.Certificates[2].Subject.CommonName)
190+
}
191+
}
158192
// check nonce
159193
if tsr.Nonce.Cmp(tc.nonce) != 0 {
160194
t.Fatalf("test '%s': expected nonce %d, got %d", tc.name, tc.nonce, tsr.Nonce)

pkg/tests/server.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ import (
2626
"github.com/sigstore/timestamp-authority/pkg/server"
2727
)
2828

29-
func createServer(t *testing.T) string {
29+
func createServer(t *testing.T, flagsToSet ...func()) string {
3030
viper.Set("timestamp-signer", "memory")
3131
viper.Set("timestamp-signer-hash", "sha256")
32+
for _, flag := range flagsToSet {
33+
flag()
34+
}
3235
// unused port
3336
apiServer := server.NewRestAPIServer("localhost", 0, []string{"http"}, false, 10*time.Second, 10*time.Second)
3437
server := httptest.NewServer(apiServer.GetHandler())

0 commit comments

Comments
 (0)