From 1d1e93a20246b8c566d45a9a28a6184673d55637 Mon Sep 17 00:00:00 2001
From: asraa <asraa@google.com>
Date: Mon, 24 May 2021 09:03:11 -0400
Subject: [PATCH] Implement RFC 3161 timestamp responses (#293)

* Add timestamps

Signed-off-by: Asra Ali <asraa@google.com>

* change

Signed-off-by: Asra Ali <asraa@google.com>

* address comments

Signed-off-by: Asra Ali <asraa@google.com>

* address comments

Signed-off-by: Asra Ali <asraa@google.com>

* fix binary writer

Signed-off-by: Asra Ali <asraa@google.com>

* add tsa

Signed-off-by: Asra Ali <asraa@google.com>

* distangle cert chain creation from new signer

Signed-off-by: Asra Ali <asraa@google.com>

* revert some now unncessary changes

Signed-off-by: Asra Ali <asraa@google.com>

* cert chain 404

Signed-off-by: Asra Ali <asraa@google.com>

* fix

Signed-off-by: Asra Ali <asraa@google.com>
---
 cmd/rekor-cli/app/root.go                     |   3 +
 cmd/rekor-cli/app/timestamp.go                | 252 ++++++++++++++++++
 cmd/rekor-cli/app/timestamp_test.go           | 130 +++++++++
 cmd/rekor-server/app/root.go                  |   1 +
 go.mod                                        |   5 +-
 go.sum                                        |  20 +-
 openapi.yaml                                  |  52 ++++
 pkg/api/api.go                                |  54 +++-
 pkg/api/error.go                              |  42 ++-
 pkg/api/timestamp.go                          |  81 ++++++
 .../get_timestamp_cert_chain_responses.go     | 129 +++++++++
 pkg/generated/client/rekor_client.go          |   5 +
 .../get_timestamp_cert_chain_parameters.go    | 142 ++++++++++
 .../get_timestamp_cert_chain_responses.go     | 156 +++++++++++
 .../get_timestamp_response_parameters.go      | 165 ++++++++++++
 .../get_timestamp_response_responses.go       | 198 ++++++++++++++
 .../client/timestamp/timestamp_client.go      | 135 ++++++++++
 .../timestamp_response_parameters.go          | 164 ++++++++++++
 .../timestamp/timestamp_response_responses.go | 169 ++++++++++++
 pkg/generated/models/timestamp_request.go     |  67 +++++
 pkg/generated/models/timestamp_response.go    |  67 +++++
 .../restapi/configure_rekor_server.go         |   9 +
 pkg/generated/restapi/doc.go                  |   3 +
 pkg/generated/restapi/embedded_spec.go        | 157 +++++++++++
 .../pubkey/get_timestamp_cert_chain.go        |  74 +++++
 .../get_timestamp_cert_chain_parameters.go    |  62 +++++
 .../get_timestamp_cert_chain_responses.go     | 130 +++++++++
 .../get_timestamp_cert_chain_urlbuilder.go    | 100 +++++++
 .../restapi/operations/rekor_server_api.go    |  58 ++++
 .../timestamp/get_timestamp_cert_chain.go     |  74 +++++
 .../get_timestamp_cert_chain_parameters.go    |  62 +++++
 .../get_timestamp_cert_chain_responses.go     | 154 +++++++++++
 .../get_timestamp_cert_chain_urlbuilder.go    | 100 +++++++
 .../timestamp/get_timestamp_response.go       |  72 +++++
 .../get_timestamp_response_parameters.go      |  75 ++++++
 .../get_timestamp_response_responses.go       | 199 ++++++++++++++
 .../get_timestamp_response_urlbuilder.go      | 100 +++++++
 .../timestamp/timestamp_response.go           |  72 +++++
 .../timestamp_response_parameters.go          | 101 +++++++
 .../timestamp/timestamp_response_responses.go | 176 ++++++++++++
 .../timestamp_response_urlbuilder.go          | 100 +++++++
 pkg/pki/x509/x509.go                          |  45 ++++
 pkg/pki/x509/x509_test.go                     |  44 +++
 pkg/signer/memory.go                          |  71 +++++
 pkg/signer/memory_test.go                     |  26 ++
 pkg/util/rfc3161.go                           | 242 +++++++++++++++++
 pkg/util/rfc3161_test.go                      | 164 ++++++++++++
 tests/e2e_test.go                             |  95 +++++++
 tests/test_request.tsq                        | Bin 0 -> 59 bytes
 49 files changed, 4573 insertions(+), 29 deletions(-)
 create mode 100644 cmd/rekor-cli/app/timestamp.go
 create mode 100644 cmd/rekor-cli/app/timestamp_test.go
 create mode 100644 pkg/api/timestamp.go
 create mode 100644 pkg/generated/client/pubkey/get_timestamp_cert_chain_responses.go
 create mode 100644 pkg/generated/client/timestamp/get_timestamp_cert_chain_parameters.go
 create mode 100644 pkg/generated/client/timestamp/get_timestamp_cert_chain_responses.go
 create mode 100644 pkg/generated/client/timestamp/get_timestamp_response_parameters.go
 create mode 100644 pkg/generated/client/timestamp/get_timestamp_response_responses.go
 create mode 100644 pkg/generated/client/timestamp/timestamp_client.go
 create mode 100644 pkg/generated/client/timestamp/timestamp_response_parameters.go
 create mode 100644 pkg/generated/client/timestamp/timestamp_response_responses.go
 create mode 100644 pkg/generated/models/timestamp_request.go
 create mode 100644 pkg/generated/models/timestamp_response.go
 create mode 100644 pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain.go
 create mode 100644 pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain_parameters.go
 create mode 100644 pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain_responses.go
 create mode 100644 pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain_urlbuilder.go
 create mode 100644 pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain.go
 create mode 100644 pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain_parameters.go
 create mode 100644 pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain_responses.go
 create mode 100644 pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain_urlbuilder.go
 create mode 100644 pkg/generated/restapi/operations/timestamp/get_timestamp_response.go
 create mode 100644 pkg/generated/restapi/operations/timestamp/get_timestamp_response_parameters.go
 create mode 100644 pkg/generated/restapi/operations/timestamp/get_timestamp_response_responses.go
 create mode 100644 pkg/generated/restapi/operations/timestamp/get_timestamp_response_urlbuilder.go
 create mode 100644 pkg/generated/restapi/operations/timestamp/timestamp_response.go
 create mode 100644 pkg/generated/restapi/operations/timestamp/timestamp_response_parameters.go
 create mode 100644 pkg/generated/restapi/operations/timestamp/timestamp_response_responses.go
 create mode 100644 pkg/generated/restapi/operations/timestamp/timestamp_response_urlbuilder.go
 create mode 100644 pkg/util/rfc3161.go
 create mode 100644 pkg/util/rfc3161_test.go
 create mode 100644 tests/test_request.tsq

diff --git a/cmd/rekor-cli/app/root.go b/cmd/rekor-cli/app/root.go
index e6fa4eb..c9fae09 100644
--- a/cmd/rekor-cli/app/root.go
+++ b/cmd/rekor-cli/app/root.go
@@ -123,7 +123,10 @@ func GetRekorClient(rekorServerURL string) (*client.Rekor, error) {
 	rt := httptransport.New(url.Host, client.DefaultBasePath, []string{url.Scheme})
 	rt.Consumers["application/yaml"] = util.YamlConsumer()
 	rt.Consumers["application/x-pem-file"] = runtime.TextConsumer()
+	rt.Consumers["application/pem-certificate-chain"] = runtime.TextConsumer()
 	rt.Producers["application/yaml"] = util.YamlProducer()
+	rt.Producers["application/timestamp-query"] = runtime.ByteStreamProducer()
+	rt.Consumers["application/timestamp-reply"] = runtime.ByteStreamConsumer()
 
 	if viper.GetString("api-key") != "" {
 		rt.DefaultAuthentication = httptransport.APIKeyAuth("apiKey", "query", viper.GetString("api-key"))
diff --git a/cmd/rekor-cli/app/timestamp.go b/cmd/rekor-cli/app/timestamp.go
new file mode 100644
index 0000000..138ba12
--- /dev/null
+++ b/cmd/rekor-cli/app/timestamp.go
@@ -0,0 +1,252 @@
+//
+// Copyright 2021 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.
+
+package app
+
+import (
+	"bytes"
+	"crypto"
+	"encoding/asn1"
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"github.com/sassoftware/relic/lib/pkcs9"
+	"github.com/sassoftware/relic/lib/x509tools"
+	"github.com/sigstore/rekor/cmd/rekor-cli/app/format"
+	"github.com/sigstore/rekor/pkg/generated/client/timestamp"
+	"github.com/sigstore/rekor/pkg/log"
+	"github.com/sigstore/rekor/pkg/util"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+type fileFlag struct {
+	value string
+}
+
+func (f *fileFlag) String() string {
+	return f.value
+}
+
+func (f *fileFlag) Set(s string) error {
+	if s == "" {
+		return errors.New("flag must be specified")
+	}
+	if _, err := os.Stat(filepath.Clean(s)); os.IsNotExist(err) {
+		return err
+	}
+	f.value = s
+	return nil
+}
+
+func (f *fileFlag) Type() string {
+	return "fileFlag"
+}
+
+type oidFlag struct {
+	value asn1.ObjectIdentifier
+}
+
+func (f *oidFlag) String() string {
+	return f.value.String()
+}
+
+func (f *oidFlag) Set(s string) error {
+	parts := strings.Split(s, ".")
+	f.value = make(asn1.ObjectIdentifier, len(parts))
+	for i, part := range parts {
+		num, err := strconv.Atoi(part)
+		if err != nil {
+			return errors.New("error parsing OID")
+		}
+		f.value[i] = num
+	}
+	return nil
+}
+
+func (f *oidFlag) Type() string {
+	return "oidFlag"
+}
+
+func addTimestampFlags(cmd *cobra.Command) error {
+	cmd.Flags().Var(&fileFlag{}, "artifact", "path to an artifact to timestamp")
+	cmd.Flags().Var(&uuidFlag{}, "artifact-hash", "hex encoded SHA256 hash of the the artifact to timestamp")
+	cmd.Flags().Bool("nonce", true, "specify a pseudo-random nonce in the request")
+	cmd.Flags().Var(&oidFlag{}, "tsa-policy", "optional dotted OID notation for the policy that the TSA should use to create the response")
+
+	cmd.Flags().String("out", "response.tsr", "path to a file to write response.")
+
+	// TODO: Add a flag to upload a pre-formed timestamp response to the log.
+	// TODO: Add a flag to indicate a JSON formatted timestamp request/response.
+	return nil
+}
+
+func validateTimestampFlags() error {
+	artifactStr := viper.GetString("artifact")
+	digestStr := viper.GetString("artifact-hash")
+
+	if artifactStr == "" && digestStr == "" {
+		return errors.New("artifact or hash to timestamp must be specified")
+	}
+
+	if digestStr != "" {
+		digest := uuidFlag{}
+		if err := digest.Set(digestStr); err != nil {
+			return err
+		}
+	}
+
+	policyStr := viper.GetString("tsa-policy")
+	if policyStr != "" {
+		oid := oidFlag{}
+		if err := oid.Set(policyStr); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func createRequestFromFlags() (*pkcs9.TimeStampReq, error) {
+	var timestampReq *pkcs9.TimeStampReq
+	digestStr := viper.GetString("artifact-hash")
+	policyStr := viper.GetString("tsa-policy")
+
+	opts := util.TimestampRequestOptions{
+		// Always use a SHA256 right now.
+		Hash: crypto.SHA256,
+	}
+	if policyStr != "" {
+		oid := oidFlag{}
+		if err := oid.Set(policyStr); err != nil {
+			return nil, err
+		}
+		opts.TSAPolicyOid = oid.value
+	}
+	if viper.GetBool("nonce") {
+		opts.Nonce = x509tools.MakeSerial()
+	}
+
+	var digest []byte
+	if digestStr != "" {
+		decoded, err := hex.DecodeString(digestStr)
+		if err != nil {
+			return nil, err
+		}
+		digest = decoded
+	}
+	if digestStr == "" {
+		artifactStr := viper.GetString("artifact")
+		artifactBytes, err := ioutil.ReadFile(filepath.Clean(artifactStr))
+		if err != nil {
+			return nil, fmt.Errorf("error reading request from file: %w", err)
+		}
+		h := opts.Hash.New()
+		if _, err := h.Write(artifactBytes); err != nil {
+			return nil, err
+		}
+		digest = h.Sum(nil)
+	}
+
+	timestampReq, err := util.TimestampRequestFromDigest(digest, opts)
+	if err != nil {
+		return nil, fmt.Errorf("error creating timestamp request: %w", err)
+	}
+
+	return timestampReq, nil
+}
+
+type timestampCmdOutput struct {
+	Location string
+}
+
+func (t *timestampCmdOutput) String() string {
+	return fmt.Sprintf(`
+Wrote response to: %v
+`, t.Location)
+}
+
+var timestampCmd = &cobra.Command{
+	Use:   "timestamp",
+	Short: "Rekor timestamp command",
+	Long:  "Generates and uploads (WIP) an RFC 3161 timestamp response to the log. The timestamp response can be verified locally using Rekor's timestamping cert chain.",
+	PreRunE: func(cmd *cobra.Command, args []string) error {
+		if err := viper.BindPFlags(cmd.Flags()); err != nil {
+			log.Logger.Fatal("Error initializing cmd line args: ", err)
+		}
+		if err := validateTimestampFlags(); err != nil {
+			log.Logger.Error(err)
+			_ = cmd.Help()
+			return err
+		}
+		return nil
+	},
+	Run: format.WrapCmd(func(args []string) (interface{}, error) {
+		rekorClient, err := GetRekorClient(viper.GetString("rekor_server"))
+		if err != nil {
+			return nil, err
+		}
+
+		timestampReq, err := createRequestFromFlags()
+		if err != nil {
+			return nil, err
+		}
+		requestBytes, err := asn1.Marshal(*timestampReq)
+		if err != nil {
+			return nil, err
+		}
+
+		params := timestamp.NewGetTimestampResponseParams()
+		params.Request = ioutil.NopCloser(bytes.NewReader(requestBytes))
+
+		var respBytes bytes.Buffer
+		_, err = rekorClient.Timestamp.GetTimestampResponse(params, &respBytes)
+		if err != nil {
+			return nil, err
+		}
+		// Sanity check response and check if the TimeStampToken was successfully created
+		if _, err = timestampReq.ParseResponse(respBytes.Bytes()); err != nil {
+			return nil, err
+		}
+
+		// Write response to file
+		outStr := viper.GetString("out")
+		if outStr == "" {
+			outStr = "response.tsr"
+		}
+		if err := ioutil.WriteFile(outStr, respBytes.Bytes(), 0600); err != nil {
+			return nil, err
+		}
+
+		// TODO: Add log index after support for uploading to transparency log is added.
+		return &timestampCmdOutput{
+			Location: outStr,
+		}, nil
+	}),
+}
+
+func init() {
+	if err := addTimestampFlags(timestampCmd); err != nil {
+		log.Logger.Fatal("Error parsing cmd line args: ", err)
+	}
+
+	rootCmd.AddCommand(timestampCmd)
+}
diff --git a/cmd/rekor-cli/app/timestamp_test.go b/cmd/rekor-cli/app/timestamp_test.go
new file mode 100644
index 0000000..957809f
--- /dev/null
+++ b/cmd/rekor-cli/app/timestamp_test.go
@@ -0,0 +1,130 @@
+//
+// Copyright 2021 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.
+
+package app
+
+import (
+	"testing"
+
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+func TestTimestampFlags(t *testing.T) {
+	type test struct {
+		caseDesc              string
+		artifact              string
+		artifactHash          string
+		oid                   string
+		expectParseSuccess    bool
+		expectValidateSuccess bool
+		expectRequestSuccess  bool
+	}
+
+	tests := []test{
+		{
+			caseDesc:              "valid local artifact",
+			artifact:              "../../../tests/test_file.txt",
+			expectParseSuccess:    true,
+			expectValidateSuccess: true,
+			expectRequestSuccess:  true,
+		},
+		{
+			caseDesc:              "nonexistant local artifact",
+			artifact:              "../../../tests/not_a_file",
+			expectParseSuccess:    false,
+			expectValidateSuccess: false,
+			expectRequestSuccess:  false,
+		},
+		{
+			caseDesc:              "valid artifact hash",
+			artifactHash:          "45c7b11fcbf07dec1694adecd8c5b85770a12a6c8dfdcf2580a2db0c47c31779",
+			expectParseSuccess:    true,
+			expectValidateSuccess: true,
+			expectRequestSuccess:  true,
+		},
+		{
+			caseDesc:              "invalid artifact hash",
+			artifactHash:          "aaa",
+			expectParseSuccess:    false,
+			expectValidateSuccess: false,
+			expectRequestSuccess:  false,
+		},
+		{
+			caseDesc:              "nonexistant request artifact",
+			artifact:              "../../../tests/not_a_request",
+			expectParseSuccess:    false,
+			expectValidateSuccess: false,
+			expectRequestSuccess:  false,
+		},
+		{
+			caseDesc:              "valid oid",
+			artifact:              "../../../tests/test_file.txt",
+			oid:                   "1.2.3.4",
+			expectParseSuccess:    true,
+			expectValidateSuccess: true,
+			expectRequestSuccess:  true,
+		},
+		{
+			caseDesc:              "invalid oid",
+			artifact:              "../../../tests/test_file.txt",
+			oid:                   "1.a.3.4",
+			expectParseSuccess:    false,
+			expectValidateSuccess: true,
+			expectRequestSuccess:  true,
+		},
+		{
+			caseDesc:              "no request or artifact specified",
+			expectParseSuccess:    true,
+			expectValidateSuccess: false,
+			expectRequestSuccess:  false,
+		},
+	}
+
+	for _, tc := range tests {
+		var blankCmd = &cobra.Command{}
+		if err := addTimestampFlags(blankCmd); err != nil {
+			t.Fatalf("unexpected error adding flags in '%v': %v", tc.caseDesc, err)
+		}
+
+		args := []string{}
+
+		if tc.artifact != "" {
+			args = append(args, "--artifact", tc.artifact)
+		}
+		if tc.artifactHash != "" {
+			args = append(args, "--artifact-hash", tc.artifactHash)
+		}
+		if tc.oid != "" {
+			args = append(args, "--tsa-policy", tc.oid)
+		}
+		if err := blankCmd.ParseFlags(args); (err == nil) != tc.expectParseSuccess {
+			t.Errorf("unexpected result parsing '%v': %v", tc.caseDesc, err)
+			continue
+		}
+
+		if err := viper.BindPFlags(blankCmd.Flags()); err != nil {
+			t.Fatalf("unexpected result initializing viper in '%v': %v", tc.caseDesc, err)
+		}
+		if err := validateTimestampFlags(); (err == nil) != tc.expectValidateSuccess {
+			t.Errorf("unexpected result validating '%v': %v", tc.caseDesc, err)
+			continue
+		}
+		if _, err := createRequestFromFlags(); (err == nil) != tc.expectRequestSuccess {
+			t.Errorf("unexpected result creating timestamp request '%v': %v", tc.caseDesc, err)
+			continue
+		}
+	}
+}
diff --git a/cmd/rekor-server/app/root.go b/cmd/rekor-server/app/root.go
index 531bf11..c8f01ac 100644
--- a/cmd/rekor-server/app/root.go
+++ b/cmd/rekor-server/app/root.go
@@ -61,6 +61,7 @@ func init() {
 	rootCmd.PersistentFlags().Uint("trillian_log_server.tlog_id", 0, "Trillian tree id")
 	rootCmd.PersistentFlags().String("rekor_server.address", "127.0.0.1", "Address to bind to")
 	rootCmd.PersistentFlags().String("rekor_server.signer", "memory", "Rekor signer to use. Current valid options include: [gcpkms, memory]")
+	rootCmd.PersistentFlags().String("rekor_server.timestamp_chain", "", "PEM encoded cert chain to use for timestamping")
 
 	rootCmd.PersistentFlags().Uint16("rekor_server.port", 3000, "Port to bind to")
 
diff --git a/go.mod b/go.mod
index 78c4f1c..a3f6580 100644
--- a/go.mod
+++ b/go.mod
@@ -19,7 +19,6 @@ require (
 	github.com/go-playground/validator v9.31.0+incompatible
 	github.com/google/rpmpack v0.0.0-20210107155803-d6befbf05148
 	github.com/google/trillian v1.3.14-0.20210413093047-5e12fb368c8f
-	github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c // indirect
 	github.com/jedisct1/go-minisign v0.0.0-20210106175330-e54e81d562c7
 	github.com/mediocregopher/radix/v4 v4.0.0-beta.1
 	github.com/mitchellh/go-homedir v1.1.0
@@ -27,7 +26,7 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.10.0
 	github.com/rs/cors v1.7.0
-	github.com/sassoftware/relic v7.2.1+incompatible
+	github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74
 	github.com/sigstore/sigstore v0.0.0-20210415112811-cb2061113e4a
 	github.com/spf13/cobra v1.1.3
 	github.com/spf13/pflag v1.0.5
@@ -48,3 +47,5 @@ require (
 	gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
 	honnef.co/go/tools v0.0.1-2020.1.6 // indirect
 )
+
+// replace github.com/sassoftware/relic => ../relic-7.2.6
diff --git a/go.sum b/go.sum
index 117fba2..c767873 100644
--- a/go.sum
+++ b/go.sum
@@ -173,6 +173,7 @@ github.com/aws/aws-sdk-go v1.38.35 h1:7AlAO0FC+8nFjxiGKEmq0QLpiA8/XFr6eIxgRTwkdT
 github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
 github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
+github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -182,6 +183,7 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
 github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw=
 github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo=
 github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
@@ -237,6 +239,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
 github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/cyberphone/json-canonicalization v0.0.0-20210303052042-6bc126869bf4 h1:7AjYfmq7AmviXsuZjV5DcE7PuhJ4dWMi8gLllpLVDQY=
 github.com/cyberphone/json-canonicalization v0.0.0-20210303052042-6bc126869bf4/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
+github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U=
 github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
 github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
 github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -477,6 +480,8 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA
 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
 github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
 github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
+github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
+github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
 github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -527,6 +532,7 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
 github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
@@ -751,6 +757,7 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx
 github.com/letsencrypt/pkcs11key v2.0.1-0.20170608213348-396559074696+incompatible/go.mod h1:iGYXKqDXt0cpBthCHdr9ZdsQwyGlYFh/+8xa4WzIQ34=
 github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag=
 github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
@@ -955,6 +962,7 @@ github.com/pseudomuto/protoc-gen-doc v1.3.2/go.mod h1:y5+P6n3iGrbKG+9O04V5ld71in
 github.com/pseudomuto/protoc-gen-doc v1.4.1/go.mod h1:exDTOVwqpp30eV/EDPFLZy3Pwr2sn6hBC1WIYH/UbIg=
 github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q=
 github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
+github.com/qur/ar v0.0.0-20130629153254-282534b91770/go.mod h1:SjlYv2m9lpV0UW6K7lDqVJwEIIvSjaHbGk7nIfY8Hxw=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
@@ -971,8 +979,10 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
 github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
 github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
-github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A=
-github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk=
+github.com/sassoftware/go-rpmutils v0.1.1/go.mod h1:euhXULoBpvAxqrBHEyJS4Tsu3hHxUmQWNymxoJbzgUY=
+github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74 h1:sUNzanSKA9z/h8xXl+ZJoxIYZL0Qx306MmxqRrvUgr0=
+github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74/go.mod h1:YlB8wFIZmFLZ1JllNBfSURzz52fBxbliNgYALk1UDmk=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/segmentio/ksuid v1.0.3/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
@@ -1038,6 +1048,7 @@ github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q
 github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
 github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
 github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
+github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -1101,6 +1112,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/zalando/go-keyring v0.1.0/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0=
 github.com/zalando/go-keyring v0.1.1 h1:w2V9lcx/Uj4l+dzAf1m9s+DJ1O8ROkEHnynonHjTcYE=
 github.com/zalando/go-keyring v0.1.1/go.mod h1:OIC+OZ28XbmwFxU/Rp9V7eKzZjamBJwRzC8UFJH9+L8=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -1183,8 +1195,10 @@ golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200930160638-afb6bcd081ae/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o=
@@ -1275,6 +1289,7 @@ golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200930145003-4acb6c075d10/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -1315,6 +1330,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200930132711-30421366ff76/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
diff --git a/openapi.yaml b/openapi.yaml
index e88ca63..5953bbe 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -224,6 +224,56 @@ paths:
           $ref: '#/responses/BadContent'
         default:
           $ref: '#/responses/InternalServerError'
+  
+  /api/v1/timestamp:
+    post:
+      # TODO: Add uploads to transparency log.
+      summary: Returns a timestamp response generated by Rekor
+      operationId: getTimestampResponse
+      tags:
+        - timestamp
+      consumes:
+        - application/timestamp-query
+      produces:
+        - application/timestamp-reply
+      parameters:
+        - in: body
+          name: request
+          required: true
+          schema:
+            type: string
+            format: binary
+      responses:
+        200:
+          description: Returns a timestamp response 
+          schema:
+            type: string
+            format: binary
+        400:
+          $ref: '#/responses/BadContent'
+        501:
+          $ref: '#/responses/NotImplemented'
+        default:
+          $ref: '#/responses/InternalServerError'
+
+  /api/v1/timestamp/certchain:
+    get:
+      summary: Retrieve the certfiicate chain for timestamping that can be used to validate trusted timestamps
+      description: Returns the certfiicate chain for timestamping that can be used to validate trusted timestamps
+      operationId: getTimestampCertChain
+      tags:
+        - timestamp
+      produces:
+        - application/pem-certificate-chain
+      responses:
+        200:
+          description: The PEM encoded cert chain
+          schema:
+            type: string
+        404:
+          $ref: '#/responses/NotFound'
+        default:
+          $ref: '#/responses/InternalServerError'
 
 definitions:
   ProposedEntry:
@@ -471,6 +521,8 @@ responses:
         format: uri
   NotFound:
     description: The content requested could not be found
+  NotImplemented:
+    description: The content requested is not implemented
   InternalServerError:
     description: There was an internal error in the server while processing the request
     schema:
diff --git a/pkg/api/api.go b/pkg/api/api.go
index d1b8fbd..2b2fc32 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -32,6 +32,7 @@ import (
 	"google.golang.org/grpc"
 
 	"github.com/sigstore/rekor/pkg/log"
+	pki "github.com/sigstore/rekor/pkg/pki/x509"
 	"github.com/sigstore/rekor/pkg/signer"
 	"github.com/sigstore/sigstore/pkg/signature"
 )
@@ -49,12 +50,14 @@ func dial(ctx context.Context, rpcServer string) (*grpc.ClientConn, error) {
 }
 
 type API struct {
-	logClient  trillian.TrillianLogClient
-	logID      int64
-	pubkey     string // PEM encoded public key
-	pubkeyHash string // SHA256 hash of DER-encoded public key
-	signer     signature.Signer
-	verifier   *client.LogVerifier
+	logClient    trillian.TrillianLogClient
+	logID        int64
+	pubkey       string // PEM encoded public key
+	pubkeyHash   string // SHA256 hash of DER-encoded public key
+	signer       signature.Signer
+	certChain    []*x509.Certificate // timestamping cert chain
+	certChainPem string              // PEM encoded timestamping cert chain
+	verifier     *client.LogVerifier
 }
 
 func NewAPI() (*API, error) {
@@ -85,11 +88,11 @@ func NewAPI() (*API, error) {
 		return nil, errors.Wrap(err, "get tree")
 	}
 
-	signer, err := signer.New(ctx, viper.GetString("rekor_server.signer"))
+	rekorSigner, err := signer.New(ctx, viper.GetString("rekor_server.signer"))
 	if err != nil {
 		return nil, errors.Wrap(err, "getting new signer")
 	}
-	pk, err := signer.PublicKey(ctx)
+	pk, err := rekorSigner.PublicKey(ctx)
 	if err != nil {
 		return nil, errors.Wrap(err, "getting public key")
 	}
@@ -113,13 +116,36 @@ func NewAPI() (*API, error) {
 		return nil, errors.Wrap(err, "new verifier")
 	}
 
+	var certChain []*x509.Certificate
+	certChainStr := viper.GetString("rekor_server.timestamp_chain")
+	if certChainStr != "" {
+		var err error
+		if certChain, err = pki.ParseTimestampCertChain([]byte(certChainStr)); err != nil {
+			return nil, errors.Wrap(err, "parsing timestamp cert chain")
+		}
+	} else if viper.GetString("rekor_server.signer") == signer.MemoryScheme {
+		// Generate a timestaming cert with a self signed CA if we are configured with an in-memory signer.
+		var err error
+		certChain, err = signer.NewTimestampingCertWithSelfSignedCA(pk)
+		if err != nil {
+			return nil, errors.Wrap(err, "generating timestaping cert chain")
+		}
+	}
+
+	certChainPem, err := pki.CertChainToPEM(certChain)
+	if err != nil {
+		return nil, errors.Wrap(err, "timestamping cert chain")
+	}
+
 	return &API{
-		logClient:  logClient,
-		logID:      tLogID,
-		pubkey:     string(pubkey),
-		pubkeyHash: hex.EncodeToString(pubkeyHashBytes),
-		signer:     signer,
-		verifier:   verifier,
+		logClient:    logClient,
+		logID:        tLogID,
+		pubkey:       string(pubkey),
+		pubkeyHash:   hex.EncodeToString(pubkeyHashBytes),
+		signer:       rekorSigner,
+		certChain:    certChain,
+		certChainPem: string(certChainPem),
+		verifier:     verifier,
 	}, nil
 }
 
diff --git a/pkg/api/error.go b/pkg/api/error.go
index 1b63af6..c614499 100644
--- a/pkg/api/error.go
+++ b/pkg/api/error.go
@@ -27,22 +27,24 @@ import (
 	"github.com/sigstore/rekor/pkg/generated/models"
 	"github.com/sigstore/rekor/pkg/generated/restapi/operations/entries"
 	"github.com/sigstore/rekor/pkg/generated/restapi/operations/index"
+	"github.com/sigstore/rekor/pkg/generated/restapi/operations/timestamp"
 	"github.com/sigstore/rekor/pkg/generated/restapi/operations/tlog"
 	"github.com/sigstore/rekor/pkg/log"
 )
 
 const (
-	trillianCommunicationError     = "Unexpected error communicating with transparency log"
-	trillianUnexpectedResult       = "Unexpected result from transparency log"
-	failedToGenerateCanonicalEntry = "Error generating canonicalized entry"
-	entryAlreadyExists             = "An equivalent entry already exists in the transparency log with UUID %v"
-	firstSizeLessThanLastSize      = "firstSize(%d) must be less than lastSize(%d)"
-	malformedUUID                  = "UUID must be a 64-character hexadecimal string"
-	malformedPublicKey             = "Public key provided could not be parsed"
-	failedToGenerateCanonicalKey   = "Error generating canonicalized public key"
-	redisUnexpectedResult          = "Unexpected result from searching index"
-	lastSizeGreaterThanKnown       = "The tree size requested(%d) was greater than what is currently observable(%d)"
-	signingError                   = "Error signing"
+	trillianCommunicationError        = "Unexpected error communicating with transparency log"
+	trillianUnexpectedResult          = "Unexpected result from transparency log"
+	failedToGenerateCanonicalEntry    = "Error generating canonicalized entry"
+	entryAlreadyExists                = "An equivalent entry already exists in the transparency log with UUID %v"
+	firstSizeLessThanLastSize         = "firstSize(%d) must be less than lastSize(%d)"
+	malformedUUID                     = "UUID must be a 64-character hexadecimal string"
+	malformedPublicKey                = "Public key provided could not be parsed"
+	failedToGenerateCanonicalKey      = "Error generating canonicalized public key"
+	redisUnexpectedResult             = "Unexpected result from searching index"
+	lastSizeGreaterThanKnown          = "The tree size requested(%d) was greater than what is currently observable(%d)"
+	signingError                      = "Error signing"
+	failedToGenerateTimestampResponse = "Error generating timestamp response"
 )
 
 func errorMsg(message string, code int) *models.Error {
@@ -138,6 +140,24 @@ func handleRekorAPIError(params interface{}, code int, err error, message string
 		default:
 			return index.NewSearchIndexDefault(code).WithPayload(errorMsg(message, code))
 		}
+	case timestamp.GetTimestampResponseParams:
+		logMsg(params.HTTPRequest)
+		switch code {
+		case http.StatusBadRequest:
+			return timestamp.NewGetTimestampResponseBadRequest().WithPayload(errorMsg(message, code))
+		case http.StatusNotImplemented:
+			return timestamp.NewGetTimestampResponseNotImplemented()
+		default:
+			return timestamp.NewGetTimestampResponseDefault(code).WithPayload(errorMsg(message, code))
+		}
+	case timestamp.GetTimestampCertChainParams:
+		logMsg(params.HTTPRequest)
+		switch code {
+		case http.StatusNotFound:
+			return timestamp.NewGetTimestampCertChainNotFound()
+		default:
+			return timestamp.NewGetTimestampCertChainDefault(code).WithPayload(errorMsg(message, code))
+		}
 	default:
 		log.Logger.Errorf("unable to find method for type %T; error: %v", params, err)
 		return middleware.Error(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
diff --git a/pkg/api/timestamp.go b/pkg/api/timestamp.go
new file mode 100644
index 0000000..b6fa46d
--- /dev/null
+++ b/pkg/api/timestamp.go
@@ -0,0 +1,81 @@
+/*
+Copyright The Rekor 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 api
+
+import (
+	"bytes"
+	"context"
+	"encoding/asn1"
+	"io/ioutil"
+	"net/http"
+
+	"github.com/go-openapi/runtime/middleware"
+	"github.com/pkg/errors"
+	"github.com/sassoftware/relic/lib/pkcs9"
+	"github.com/sigstore/rekor/pkg/generated/restapi/operations/timestamp"
+	"github.com/sigstore/rekor/pkg/log"
+	"github.com/sigstore/rekor/pkg/util"
+)
+
+func RequestFromRekor(ctx context.Context, req pkcs9.TimeStampReq) ([]byte, error) {
+	resp, err := util.CreateRfc3161Response(ctx, req, api.certChain, api.signer)
+	if err != nil {
+		return nil, err
+	}
+
+	body, err := asn1.Marshal(*resp)
+	if err != nil {
+		return nil, err
+	}
+
+	return body, nil
+}
+
+func TimestampResponseHandler(params timestamp.GetTimestampResponseParams) middleware.Responder {
+	// Fail early if we don't haven't configured rekor with a certificate for timestamping.
+	if len(api.certChain) == 0 {
+		return handleRekorAPIError(params, http.StatusNotImplemented, errors.New("rekor is not configured to serve timestamps"), "")
+	}
+
+	// TODO: Add support for in-house JSON based timestamp response.
+	requestBytes, err := ioutil.ReadAll(params.Request)
+	if err != nil {
+		return handleRekorAPIError(params, http.StatusBadRequest, err, failedToGenerateTimestampResponse)
+	}
+	req, err := util.ParseTimestampRequest(requestBytes)
+	if err != nil {
+		return handleRekorAPIError(params, http.StatusBadRequest, err, failedToGenerateTimestampResponse)
+	}
+
+	// Create response
+	ctx := params.HTTPRequest.Context()
+	resp, err := RequestFromRekor(ctx, *req)
+	if err != nil {
+		return handleRekorAPIError(params, http.StatusInternalServerError, err, failedToGenerateTimestampResponse)
+	}
+
+	// TODO: Upload to transparency log and add entry UUID to location header.
+	log.Logger.Errorf("generated OK")
+	return timestamp.NewGetTimestampResponseOK().WithPayload(ioutil.NopCloser(bytes.NewReader(resp)))
+}
+
+func GetTimestampCertChainHandler(params timestamp.GetTimestampCertChainParams) middleware.Responder {
+	if len(api.certChain) == 0 {
+		return handleRekorAPIError(params, http.StatusNotFound, errors.New("rekor is not configured with a timestamping certificate"), "")
+	}
+	return timestamp.NewGetTimestampCertChainOK().WithPayload(api.certChainPem)
+}
diff --git a/pkg/generated/client/pubkey/get_timestamp_cert_chain_responses.go b/pkg/generated/client/pubkey/get_timestamp_cert_chain_responses.go
new file mode 100644
index 0000000..b93b7ef
--- /dev/null
+++ b/pkg/generated/client/pubkey/get_timestamp_cert_chain_responses.go
@@ -0,0 +1,129 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package pubkey
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/go-openapi/runtime"
+	"github.com/go-openapi/strfmt"
+
+	"github.com/sigstore/rekor/pkg/generated/models"
+)
+
+// GetTimestampCertChainReader is a Reader for the GetTimestampCertChain structure.
+type GetTimestampCertChainReader struct {
+	formats strfmt.Registry
+}
+
+// ReadResponse reads a server response into the received o.
+func (o *GetTimestampCertChainReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
+	switch response.Code() {
+	case 200:
+		result := NewGetTimestampCertChainOK()
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		return result, nil
+	default:
+		result := NewGetTimestampCertChainDefault(response.Code())
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		if response.Code()/100 == 2 {
+			return result, nil
+		}
+		return nil, result
+	}
+}
+
+// NewGetTimestampCertChainOK creates a GetTimestampCertChainOK with default headers values
+func NewGetTimestampCertChainOK() *GetTimestampCertChainOK {
+	return &GetTimestampCertChainOK{}
+}
+
+/* GetTimestampCertChainOK describes a response with status code 200, with default header values.
+
+The PEM encoded cert chain
+*/
+type GetTimestampCertChainOK struct {
+	Payload string
+}
+
+func (o *GetTimestampCertChainOK) Error() string {
+	return fmt.Sprintf("[GET /api/v1/log/timestampCertChain][%d] getTimestampCertChainOK  %+v", 200, o.Payload)
+}
+func (o *GetTimestampCertChainOK) GetPayload() string {
+	return o.Payload
+}
+
+func (o *GetTimestampCertChainOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	// response payload
+	if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
+
+// NewGetTimestampCertChainDefault creates a GetTimestampCertChainDefault with default headers values
+func NewGetTimestampCertChainDefault(code int) *GetTimestampCertChainDefault {
+	return &GetTimestampCertChainDefault{
+		_statusCode: code,
+	}
+}
+
+/* GetTimestampCertChainDefault describes a response with status code -1, with default header values.
+
+There was an internal error in the server while processing the request
+*/
+type GetTimestampCertChainDefault struct {
+	_statusCode int
+
+	Payload *models.Error
+}
+
+// Code gets the status code for the get timestamp cert chain default response
+func (o *GetTimestampCertChainDefault) Code() int {
+	return o._statusCode
+}
+
+func (o *GetTimestampCertChainDefault) Error() string {
+	return fmt.Sprintf("[GET /api/v1/log/timestampCertChain][%d] getTimestampCertChain default  %+v", o._statusCode, o.Payload)
+}
+func (o *GetTimestampCertChainDefault) GetPayload() *models.Error {
+	return o.Payload
+}
+
+func (o *GetTimestampCertChainDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	o.Payload = new(models.Error)
+
+	// response payload
+	if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/generated/client/rekor_client.go b/pkg/generated/client/rekor_client.go
index bee3811..b25cb57 100644
--- a/pkg/generated/client/rekor_client.go
+++ b/pkg/generated/client/rekor_client.go
@@ -29,6 +29,7 @@ import (
 	"github.com/sigstore/rekor/pkg/generated/client/entries"
 	"github.com/sigstore/rekor/pkg/generated/client/index"
 	"github.com/sigstore/rekor/pkg/generated/client/pubkey"
+	"github.com/sigstore/rekor/pkg/generated/client/timestamp"
 	"github.com/sigstore/rekor/pkg/generated/client/tlog"
 )
 
@@ -77,6 +78,7 @@ func New(transport runtime.ClientTransport, formats strfmt.Registry) *Rekor {
 	cli.Entries = entries.New(transport, formats)
 	cli.Index = index.New(transport, formats)
 	cli.Pubkey = pubkey.New(transport, formats)
+	cli.Timestamp = timestamp.New(transport, formats)
 	cli.Tlog = tlog.New(transport, formats)
 	return cli
 }
@@ -128,6 +130,8 @@ type Rekor struct {
 
 	Pubkey pubkey.ClientService
 
+	Timestamp timestamp.ClientService
+
 	Tlog tlog.ClientService
 
 	Transport runtime.ClientTransport
@@ -139,5 +143,6 @@ func (c *Rekor) SetTransport(transport runtime.ClientTransport) {
 	c.Entries.SetTransport(transport)
 	c.Index.SetTransport(transport)
 	c.Pubkey.SetTransport(transport)
+	c.Timestamp.SetTransport(transport)
 	c.Tlog.SetTransport(transport)
 }
diff --git a/pkg/generated/client/timestamp/get_timestamp_cert_chain_parameters.go b/pkg/generated/client/timestamp/get_timestamp_cert_chain_parameters.go
new file mode 100644
index 0000000..5fee865
--- /dev/null
+++ b/pkg/generated/client/timestamp/get_timestamp_cert_chain_parameters.go
@@ -0,0 +1,142 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"context"
+	"net/http"
+	"time"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/runtime"
+	cr "github.com/go-openapi/runtime/client"
+	"github.com/go-openapi/strfmt"
+)
+
+// NewGetTimestampCertChainParams creates a new GetTimestampCertChainParams object,
+// with the default timeout for this client.
+//
+// Default values are not hydrated, since defaults are normally applied by the API server side.
+//
+// To enforce default values in parameter, use SetDefaults or WithDefaults.
+func NewGetTimestampCertChainParams() *GetTimestampCertChainParams {
+	return &GetTimestampCertChainParams{
+		timeout: cr.DefaultTimeout,
+	}
+}
+
+// NewGetTimestampCertChainParamsWithTimeout creates a new GetTimestampCertChainParams object
+// with the ability to set a timeout on a request.
+func NewGetTimestampCertChainParamsWithTimeout(timeout time.Duration) *GetTimestampCertChainParams {
+	return &GetTimestampCertChainParams{
+		timeout: timeout,
+	}
+}
+
+// NewGetTimestampCertChainParamsWithContext creates a new GetTimestampCertChainParams object
+// with the ability to set a context for a request.
+func NewGetTimestampCertChainParamsWithContext(ctx context.Context) *GetTimestampCertChainParams {
+	return &GetTimestampCertChainParams{
+		Context: ctx,
+	}
+}
+
+// NewGetTimestampCertChainParamsWithHTTPClient creates a new GetTimestampCertChainParams object
+// with the ability to set a custom HTTPClient for a request.
+func NewGetTimestampCertChainParamsWithHTTPClient(client *http.Client) *GetTimestampCertChainParams {
+	return &GetTimestampCertChainParams{
+		HTTPClient: client,
+	}
+}
+
+/* GetTimestampCertChainParams contains all the parameters to send to the API endpoint
+   for the get timestamp cert chain operation.
+
+   Typically these are written to a http.Request.
+*/
+type GetTimestampCertChainParams struct {
+	timeout    time.Duration
+	Context    context.Context
+	HTTPClient *http.Client
+}
+
+// WithDefaults hydrates default values in the get timestamp cert chain params (not the query body).
+//
+// All values with no default are reset to their zero value.
+func (o *GetTimestampCertChainParams) WithDefaults() *GetTimestampCertChainParams {
+	o.SetDefaults()
+	return o
+}
+
+// SetDefaults hydrates default values in the get timestamp cert chain params (not the query body).
+//
+// All values with no default are reset to their zero value.
+func (o *GetTimestampCertChainParams) SetDefaults() {
+	// no default values defined for this parameter
+}
+
+// WithTimeout adds the timeout to the get timestamp cert chain params
+func (o *GetTimestampCertChainParams) WithTimeout(timeout time.Duration) *GetTimestampCertChainParams {
+	o.SetTimeout(timeout)
+	return o
+}
+
+// SetTimeout adds the timeout to the get timestamp cert chain params
+func (o *GetTimestampCertChainParams) SetTimeout(timeout time.Duration) {
+	o.timeout = timeout
+}
+
+// WithContext adds the context to the get timestamp cert chain params
+func (o *GetTimestampCertChainParams) WithContext(ctx context.Context) *GetTimestampCertChainParams {
+	o.SetContext(ctx)
+	return o
+}
+
+// SetContext adds the context to the get timestamp cert chain params
+func (o *GetTimestampCertChainParams) SetContext(ctx context.Context) {
+	o.Context = ctx
+}
+
+// WithHTTPClient adds the HTTPClient to the get timestamp cert chain params
+func (o *GetTimestampCertChainParams) WithHTTPClient(client *http.Client) *GetTimestampCertChainParams {
+	o.SetHTTPClient(client)
+	return o
+}
+
+// SetHTTPClient adds the HTTPClient to the get timestamp cert chain params
+func (o *GetTimestampCertChainParams) SetHTTPClient(client *http.Client) {
+	o.HTTPClient = client
+}
+
+// WriteToRequest writes these params to a swagger request
+func (o *GetTimestampCertChainParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
+
+	if err := r.SetTimeout(o.timeout); err != nil {
+		return err
+	}
+	var res []error
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
diff --git a/pkg/generated/client/timestamp/get_timestamp_cert_chain_responses.go b/pkg/generated/client/timestamp/get_timestamp_cert_chain_responses.go
new file mode 100644
index 0000000..34bbf52
--- /dev/null
+++ b/pkg/generated/client/timestamp/get_timestamp_cert_chain_responses.go
@@ -0,0 +1,156 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/go-openapi/runtime"
+	"github.com/go-openapi/strfmt"
+
+	"github.com/sigstore/rekor/pkg/generated/models"
+)
+
+// GetTimestampCertChainReader is a Reader for the GetTimestampCertChain structure.
+type GetTimestampCertChainReader struct {
+	formats strfmt.Registry
+}
+
+// ReadResponse reads a server response into the received o.
+func (o *GetTimestampCertChainReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
+	switch response.Code() {
+	case 200:
+		result := NewGetTimestampCertChainOK()
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		return result, nil
+	case 404:
+		result := NewGetTimestampCertChainNotFound()
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		return nil, result
+	default:
+		result := NewGetTimestampCertChainDefault(response.Code())
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		if response.Code()/100 == 2 {
+			return result, nil
+		}
+		return nil, result
+	}
+}
+
+// NewGetTimestampCertChainOK creates a GetTimestampCertChainOK with default headers values
+func NewGetTimestampCertChainOK() *GetTimestampCertChainOK {
+	return &GetTimestampCertChainOK{}
+}
+
+/* GetTimestampCertChainOK describes a response with status code 200, with default header values.
+
+The PEM encoded cert chain
+*/
+type GetTimestampCertChainOK struct {
+	Payload string
+}
+
+func (o *GetTimestampCertChainOK) Error() string {
+	return fmt.Sprintf("[GET /api/v1/timestamp/certchain][%d] getTimestampCertChainOK  %+v", 200, o.Payload)
+}
+func (o *GetTimestampCertChainOK) GetPayload() string {
+	return o.Payload
+}
+
+func (o *GetTimestampCertChainOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	// response payload
+	if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
+
+// NewGetTimestampCertChainNotFound creates a GetTimestampCertChainNotFound with default headers values
+func NewGetTimestampCertChainNotFound() *GetTimestampCertChainNotFound {
+	return &GetTimestampCertChainNotFound{}
+}
+
+/* GetTimestampCertChainNotFound describes a response with status code 404, with default header values.
+
+The content requested could not be found
+*/
+type GetTimestampCertChainNotFound struct {
+}
+
+func (o *GetTimestampCertChainNotFound) Error() string {
+	return fmt.Sprintf("[GET /api/v1/timestamp/certchain][%d] getTimestampCertChainNotFound ", 404)
+}
+
+func (o *GetTimestampCertChainNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	return nil
+}
+
+// NewGetTimestampCertChainDefault creates a GetTimestampCertChainDefault with default headers values
+func NewGetTimestampCertChainDefault(code int) *GetTimestampCertChainDefault {
+	return &GetTimestampCertChainDefault{
+		_statusCode: code,
+	}
+}
+
+/* GetTimestampCertChainDefault describes a response with status code -1, with default header values.
+
+There was an internal error in the server while processing the request
+*/
+type GetTimestampCertChainDefault struct {
+	_statusCode int
+
+	Payload *models.Error
+}
+
+// Code gets the status code for the get timestamp cert chain default response
+func (o *GetTimestampCertChainDefault) Code() int {
+	return o._statusCode
+}
+
+func (o *GetTimestampCertChainDefault) Error() string {
+	return fmt.Sprintf("[GET /api/v1/timestamp/certchain][%d] getTimestampCertChain default  %+v", o._statusCode, o.Payload)
+}
+func (o *GetTimestampCertChainDefault) GetPayload() *models.Error {
+	return o.Payload
+}
+
+func (o *GetTimestampCertChainDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	o.Payload = new(models.Error)
+
+	// response payload
+	if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/generated/client/timestamp/get_timestamp_response_parameters.go b/pkg/generated/client/timestamp/get_timestamp_response_parameters.go
new file mode 100644
index 0000000..8965647
--- /dev/null
+++ b/pkg/generated/client/timestamp/get_timestamp_response_parameters.go
@@ -0,0 +1,165 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"context"
+	"io"
+	"net/http"
+	"time"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/runtime"
+	cr "github.com/go-openapi/runtime/client"
+	"github.com/go-openapi/strfmt"
+)
+
+// NewGetTimestampResponseParams creates a new GetTimestampResponseParams object,
+// with the default timeout for this client.
+//
+// Default values are not hydrated, since defaults are normally applied by the API server side.
+//
+// To enforce default values in parameter, use SetDefaults or WithDefaults.
+func NewGetTimestampResponseParams() *GetTimestampResponseParams {
+	return &GetTimestampResponseParams{
+		timeout: cr.DefaultTimeout,
+	}
+}
+
+// NewGetTimestampResponseParamsWithTimeout creates a new GetTimestampResponseParams object
+// with the ability to set a timeout on a request.
+func NewGetTimestampResponseParamsWithTimeout(timeout time.Duration) *GetTimestampResponseParams {
+	return &GetTimestampResponseParams{
+		timeout: timeout,
+	}
+}
+
+// NewGetTimestampResponseParamsWithContext creates a new GetTimestampResponseParams object
+// with the ability to set a context for a request.
+func NewGetTimestampResponseParamsWithContext(ctx context.Context) *GetTimestampResponseParams {
+	return &GetTimestampResponseParams{
+		Context: ctx,
+	}
+}
+
+// NewGetTimestampResponseParamsWithHTTPClient creates a new GetTimestampResponseParams object
+// with the ability to set a custom HTTPClient for a request.
+func NewGetTimestampResponseParamsWithHTTPClient(client *http.Client) *GetTimestampResponseParams {
+	return &GetTimestampResponseParams{
+		HTTPClient: client,
+	}
+}
+
+/* GetTimestampResponseParams contains all the parameters to send to the API endpoint
+   for the get timestamp response operation.
+
+   Typically these are written to a http.Request.
+*/
+type GetTimestampResponseParams struct {
+
+	// Request.
+	//
+	// Format: binary
+	Request io.ReadCloser
+
+	timeout    time.Duration
+	Context    context.Context
+	HTTPClient *http.Client
+}
+
+// WithDefaults hydrates default values in the get timestamp response params (not the query body).
+//
+// All values with no default are reset to their zero value.
+func (o *GetTimestampResponseParams) WithDefaults() *GetTimestampResponseParams {
+	o.SetDefaults()
+	return o
+}
+
+// SetDefaults hydrates default values in the get timestamp response params (not the query body).
+//
+// All values with no default are reset to their zero value.
+func (o *GetTimestampResponseParams) SetDefaults() {
+	// no default values defined for this parameter
+}
+
+// WithTimeout adds the timeout to the get timestamp response params
+func (o *GetTimestampResponseParams) WithTimeout(timeout time.Duration) *GetTimestampResponseParams {
+	o.SetTimeout(timeout)
+	return o
+}
+
+// SetTimeout adds the timeout to the get timestamp response params
+func (o *GetTimestampResponseParams) SetTimeout(timeout time.Duration) {
+	o.timeout = timeout
+}
+
+// WithContext adds the context to the get timestamp response params
+func (o *GetTimestampResponseParams) WithContext(ctx context.Context) *GetTimestampResponseParams {
+	o.SetContext(ctx)
+	return o
+}
+
+// SetContext adds the context to the get timestamp response params
+func (o *GetTimestampResponseParams) SetContext(ctx context.Context) {
+	o.Context = ctx
+}
+
+// WithHTTPClient adds the HTTPClient to the get timestamp response params
+func (o *GetTimestampResponseParams) WithHTTPClient(client *http.Client) *GetTimestampResponseParams {
+	o.SetHTTPClient(client)
+	return o
+}
+
+// SetHTTPClient adds the HTTPClient to the get timestamp response params
+func (o *GetTimestampResponseParams) SetHTTPClient(client *http.Client) {
+	o.HTTPClient = client
+}
+
+// WithRequest adds the request to the get timestamp response params
+func (o *GetTimestampResponseParams) WithRequest(request io.ReadCloser) *GetTimestampResponseParams {
+	o.SetRequest(request)
+	return o
+}
+
+// SetRequest adds the request to the get timestamp response params
+func (o *GetTimestampResponseParams) SetRequest(request io.ReadCloser) {
+	o.Request = request
+}
+
+// WriteToRequest writes these params to a swagger request
+func (o *GetTimestampResponseParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
+
+	if err := r.SetTimeout(o.timeout); err != nil {
+		return err
+	}
+	var res []error
+	if o.Request != nil {
+		if err := r.SetBodyParam(o.Request); err != nil {
+			return err
+		}
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
diff --git a/pkg/generated/client/timestamp/get_timestamp_response_responses.go b/pkg/generated/client/timestamp/get_timestamp_response_responses.go
new file mode 100644
index 0000000..7d37e3b
--- /dev/null
+++ b/pkg/generated/client/timestamp/get_timestamp_response_responses.go
@@ -0,0 +1,198 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/go-openapi/runtime"
+	"github.com/go-openapi/strfmt"
+
+	"github.com/sigstore/rekor/pkg/generated/models"
+)
+
+// GetTimestampResponseReader is a Reader for the GetTimestampResponse structure.
+type GetTimestampResponseReader struct {
+	formats strfmt.Registry
+	writer  io.Writer
+}
+
+// ReadResponse reads a server response into the received o.
+func (o *GetTimestampResponseReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
+	switch response.Code() {
+	case 200:
+		result := NewGetTimestampResponseOK(o.writer)
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		return result, nil
+	case 400:
+		result := NewGetTimestampResponseBadRequest()
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		return nil, result
+	case 501:
+		result := NewGetTimestampResponseNotImplemented()
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		return nil, result
+	default:
+		result := NewGetTimestampResponseDefault(response.Code())
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		if response.Code()/100 == 2 {
+			return result, nil
+		}
+		return nil, result
+	}
+}
+
+// NewGetTimestampResponseOK creates a GetTimestampResponseOK with default headers values
+func NewGetTimestampResponseOK(writer io.Writer) *GetTimestampResponseOK {
+	return &GetTimestampResponseOK{
+
+		Payload: writer,
+	}
+}
+
+/* GetTimestampResponseOK describes a response with status code 200, with default header values.
+
+Returns a timestamp response
+*/
+type GetTimestampResponseOK struct {
+	Payload io.Writer
+}
+
+func (o *GetTimestampResponseOK) Error() string {
+	return fmt.Sprintf("[POST /api/v1/timestamp][%d] getTimestampResponseOK  %+v", 200, o.Payload)
+}
+func (o *GetTimestampResponseOK) GetPayload() io.Writer {
+	return o.Payload
+}
+
+func (o *GetTimestampResponseOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	// response payload
+	if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
+
+// NewGetTimestampResponseBadRequest creates a GetTimestampResponseBadRequest with default headers values
+func NewGetTimestampResponseBadRequest() *GetTimestampResponseBadRequest {
+	return &GetTimestampResponseBadRequest{}
+}
+
+/* GetTimestampResponseBadRequest describes a response with status code 400, with default header values.
+
+The content supplied to the server was invalid
+*/
+type GetTimestampResponseBadRequest struct {
+	Payload *models.Error
+}
+
+func (o *GetTimestampResponseBadRequest) Error() string {
+	return fmt.Sprintf("[POST /api/v1/timestamp][%d] getTimestampResponseBadRequest  %+v", 400, o.Payload)
+}
+func (o *GetTimestampResponseBadRequest) GetPayload() *models.Error {
+	return o.Payload
+}
+
+func (o *GetTimestampResponseBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	o.Payload = new(models.Error)
+
+	// response payload
+	if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
+
+// NewGetTimestampResponseNotImplemented creates a GetTimestampResponseNotImplemented with default headers values
+func NewGetTimestampResponseNotImplemented() *GetTimestampResponseNotImplemented {
+	return &GetTimestampResponseNotImplemented{}
+}
+
+/* GetTimestampResponseNotImplemented describes a response with status code 501, with default header values.
+
+The content requested is not implemented
+*/
+type GetTimestampResponseNotImplemented struct {
+}
+
+func (o *GetTimestampResponseNotImplemented) Error() string {
+	return fmt.Sprintf("[POST /api/v1/timestamp][%d] getTimestampResponseNotImplemented ", 501)
+}
+
+func (o *GetTimestampResponseNotImplemented) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	return nil
+}
+
+// NewGetTimestampResponseDefault creates a GetTimestampResponseDefault with default headers values
+func NewGetTimestampResponseDefault(code int) *GetTimestampResponseDefault {
+	return &GetTimestampResponseDefault{
+		_statusCode: code,
+	}
+}
+
+/* GetTimestampResponseDefault describes a response with status code -1, with default header values.
+
+There was an internal error in the server while processing the request
+*/
+type GetTimestampResponseDefault struct {
+	_statusCode int
+
+	Payload *models.Error
+}
+
+// Code gets the status code for the get timestamp response default response
+func (o *GetTimestampResponseDefault) Code() int {
+	return o._statusCode
+}
+
+func (o *GetTimestampResponseDefault) Error() string {
+	return fmt.Sprintf("[POST /api/v1/timestamp][%d] getTimestampResponse default  %+v", o._statusCode, o.Payload)
+}
+func (o *GetTimestampResponseDefault) GetPayload() *models.Error {
+	return o.Payload
+}
+
+func (o *GetTimestampResponseDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	o.Payload = new(models.Error)
+
+	// response payload
+	if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/generated/client/timestamp/timestamp_client.go b/pkg/generated/client/timestamp/timestamp_client.go
new file mode 100644
index 0000000..b82d707
--- /dev/null
+++ b/pkg/generated/client/timestamp/timestamp_client.go
@@ -0,0 +1,135 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"io"
+
+	"github.com/go-openapi/runtime"
+	"github.com/go-openapi/strfmt"
+)
+
+// New creates a new timestamp API client.
+func New(transport runtime.ClientTransport, formats strfmt.Registry) ClientService {
+	return &Client{transport: transport, formats: formats}
+}
+
+/*
+Client for timestamp API
+*/
+type Client struct {
+	transport runtime.ClientTransport
+	formats   strfmt.Registry
+}
+
+// ClientOption is the option for Client methods
+type ClientOption func(*runtime.ClientOperation)
+
+// ClientService is the interface for Client methods
+type ClientService interface {
+	GetTimestampCertChain(params *GetTimestampCertChainParams, opts ...ClientOption) (*GetTimestampCertChainOK, error)
+
+	GetTimestampResponse(params *GetTimestampResponseParams, writer io.Writer, opts ...ClientOption) (*GetTimestampResponseOK, error)
+
+	SetTransport(transport runtime.ClientTransport)
+}
+
+/*
+  GetTimestampCertChain retrieves the certfiicate chain for timestamping that can be used to validate trusted timestamps
+
+  Returns the certfiicate chain for timestamping that can be used to validate trusted timestamps
+*/
+func (a *Client) GetTimestampCertChain(params *GetTimestampCertChainParams, opts ...ClientOption) (*GetTimestampCertChainOK, error) {
+	// TODO: Validate the params before sending
+	if params == nil {
+		params = NewGetTimestampCertChainParams()
+	}
+	op := &runtime.ClientOperation{
+		ID:                 "getTimestampCertChain",
+		Method:             "GET",
+		PathPattern:        "/api/v1/timestamp/certchain",
+		ProducesMediaTypes: []string{"application/pem-certificate-chain"},
+		ConsumesMediaTypes: []string{"application/json", "application/yaml"},
+		Schemes:            []string{"http"},
+		Params:             params,
+		Reader:             &GetTimestampCertChainReader{formats: a.formats},
+		Context:            params.Context,
+		Client:             params.HTTPClient,
+	}
+	for _, opt := range opts {
+		opt(op)
+	}
+
+	result, err := a.transport.Submit(op)
+	if err != nil {
+		return nil, err
+	}
+	success, ok := result.(*GetTimestampCertChainOK)
+	if ok {
+		return success, nil
+	}
+	// unexpected success response
+	unexpectedSuccess := result.(*GetTimestampCertChainDefault)
+	return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
+}
+
+/*
+  GetTimestampResponse returns a timestamp response generated by rekor
+*/
+func (a *Client) GetTimestampResponse(params *GetTimestampResponseParams, writer io.Writer, opts ...ClientOption) (*GetTimestampResponseOK, error) {
+	// TODO: Validate the params before sending
+	if params == nil {
+		params = NewGetTimestampResponseParams()
+	}
+	op := &runtime.ClientOperation{
+		ID:                 "getTimestampResponse",
+		Method:             "POST",
+		PathPattern:        "/api/v1/timestamp",
+		ProducesMediaTypes: []string{"application/timestamp-reply"},
+		ConsumesMediaTypes: []string{"application/timestamp-query"},
+		Schemes:            []string{"http"},
+		Params:             params,
+		Reader:             &GetTimestampResponseReader{formats: a.formats, writer: writer},
+		Context:            params.Context,
+		Client:             params.HTTPClient,
+	}
+	for _, opt := range opts {
+		opt(op)
+	}
+
+	result, err := a.transport.Submit(op)
+	if err != nil {
+		return nil, err
+	}
+	success, ok := result.(*GetTimestampResponseOK)
+	if ok {
+		return success, nil
+	}
+	// unexpected success response
+	unexpectedSuccess := result.(*GetTimestampResponseDefault)
+	return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
+}
+
+// SetTransport changes the transport on the client
+func (a *Client) SetTransport(transport runtime.ClientTransport) {
+	a.transport = transport
+}
diff --git a/pkg/generated/client/timestamp/timestamp_response_parameters.go b/pkg/generated/client/timestamp/timestamp_response_parameters.go
new file mode 100644
index 0000000..422a51c
--- /dev/null
+++ b/pkg/generated/client/timestamp/timestamp_response_parameters.go
@@ -0,0 +1,164 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"context"
+	"net/http"
+	"time"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/runtime"
+	cr "github.com/go-openapi/runtime/client"
+	"github.com/go-openapi/strfmt"
+
+	"github.com/sigstore/rekor/pkg/generated/models"
+)
+
+// NewTimestampResponseParams creates a new TimestampResponseParams object,
+// with the default timeout for this client.
+//
+// Default values are not hydrated, since defaults are normally applied by the API server side.
+//
+// To enforce default values in parameter, use SetDefaults or WithDefaults.
+func NewTimestampResponseParams() *TimestampResponseParams {
+	return &TimestampResponseParams{
+		timeout: cr.DefaultTimeout,
+	}
+}
+
+// NewTimestampResponseParamsWithTimeout creates a new TimestampResponseParams object
+// with the ability to set a timeout on a request.
+func NewTimestampResponseParamsWithTimeout(timeout time.Duration) *TimestampResponseParams {
+	return &TimestampResponseParams{
+		timeout: timeout,
+	}
+}
+
+// NewTimestampResponseParamsWithContext creates a new TimestampResponseParams object
+// with the ability to set a context for a request.
+func NewTimestampResponseParamsWithContext(ctx context.Context) *TimestampResponseParams {
+	return &TimestampResponseParams{
+		Context: ctx,
+	}
+}
+
+// NewTimestampResponseParamsWithHTTPClient creates a new TimestampResponseParams object
+// with the ability to set a custom HTTPClient for a request.
+func NewTimestampResponseParamsWithHTTPClient(client *http.Client) *TimestampResponseParams {
+	return &TimestampResponseParams{
+		HTTPClient: client,
+	}
+}
+
+/* TimestampResponseParams contains all the parameters to send to the API endpoint
+   for the timestamp response operation.
+
+   Typically these are written to a http.Request.
+*/
+type TimestampResponseParams struct {
+
+	// Query.
+	Query *models.TimestampRequest
+
+	timeout    time.Duration
+	Context    context.Context
+	HTTPClient *http.Client
+}
+
+// WithDefaults hydrates default values in the timestamp response params (not the query body).
+//
+// All values with no default are reset to their zero value.
+func (o *TimestampResponseParams) WithDefaults() *TimestampResponseParams {
+	o.SetDefaults()
+	return o
+}
+
+// SetDefaults hydrates default values in the timestamp response params (not the query body).
+//
+// All values with no default are reset to their zero value.
+func (o *TimestampResponseParams) SetDefaults() {
+	// no default values defined for this parameter
+}
+
+// WithTimeout adds the timeout to the timestamp response params
+func (o *TimestampResponseParams) WithTimeout(timeout time.Duration) *TimestampResponseParams {
+	o.SetTimeout(timeout)
+	return o
+}
+
+// SetTimeout adds the timeout to the timestamp response params
+func (o *TimestampResponseParams) SetTimeout(timeout time.Duration) {
+	o.timeout = timeout
+}
+
+// WithContext adds the context to the timestamp response params
+func (o *TimestampResponseParams) WithContext(ctx context.Context) *TimestampResponseParams {
+	o.SetContext(ctx)
+	return o
+}
+
+// SetContext adds the context to the timestamp response params
+func (o *TimestampResponseParams) SetContext(ctx context.Context) {
+	o.Context = ctx
+}
+
+// WithHTTPClient adds the HTTPClient to the timestamp response params
+func (o *TimestampResponseParams) WithHTTPClient(client *http.Client) *TimestampResponseParams {
+	o.SetHTTPClient(client)
+	return o
+}
+
+// SetHTTPClient adds the HTTPClient to the timestamp response params
+func (o *TimestampResponseParams) SetHTTPClient(client *http.Client) {
+	o.HTTPClient = client
+}
+
+// WithQuery adds the query to the timestamp response params
+func (o *TimestampResponseParams) WithQuery(query *models.TimestampRequest) *TimestampResponseParams {
+	o.SetQuery(query)
+	return o
+}
+
+// SetQuery adds the query to the timestamp response params
+func (o *TimestampResponseParams) SetQuery(query *models.TimestampRequest) {
+	o.Query = query
+}
+
+// WriteToRequest writes these params to a swagger request
+func (o *TimestampResponseParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
+
+	if err := r.SetTimeout(o.timeout); err != nil {
+		return err
+	}
+	var res []error
+	if o.Query != nil {
+		if err := r.SetBodyParam(o.Query); err != nil {
+			return err
+		}
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
diff --git a/pkg/generated/client/timestamp/timestamp_response_responses.go b/pkg/generated/client/timestamp/timestamp_response_responses.go
new file mode 100644
index 0000000..8805d19
--- /dev/null
+++ b/pkg/generated/client/timestamp/timestamp_response_responses.go
@@ -0,0 +1,169 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/go-openapi/runtime"
+	"github.com/go-openapi/strfmt"
+
+	"github.com/sigstore/rekor/pkg/generated/models"
+)
+
+// TimestampResponseReader is a Reader for the TimestampResponse structure.
+type TimestampResponseReader struct {
+	formats strfmt.Registry
+}
+
+// ReadResponse reads a server response into the received o.
+func (o *TimestampResponseReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
+	switch response.Code() {
+	case 200:
+		result := NewTimestampResponseOK()
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		return result, nil
+	case 400:
+		result := NewTimestampResponseBadRequest()
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		return nil, result
+	default:
+		result := NewTimestampResponseDefault(response.Code())
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		if response.Code()/100 == 2 {
+			return result, nil
+		}
+		return nil, result
+	}
+}
+
+// NewTimestampResponseOK creates a TimestampResponseOK with default headers values
+func NewTimestampResponseOK() *TimestampResponseOK {
+	return &TimestampResponseOK{}
+}
+
+/* TimestampResponseOK describes a response with status code 200, with default header values.
+
+Returns a timestamp response
+*/
+type TimestampResponseOK struct {
+	Payload *models.TimestampResponse
+}
+
+func (o *TimestampResponseOK) Error() string {
+	return fmt.Sprintf("[POST /api/v1/tsr][%d] timestampResponseOK  %+v", 200, o.Payload)
+}
+func (o *TimestampResponseOK) GetPayload() *models.TimestampResponse {
+	return o.Payload
+}
+
+func (o *TimestampResponseOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	o.Payload = new(models.TimestampResponse)
+
+	// response payload
+	if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
+
+// NewTimestampResponseBadRequest creates a TimestampResponseBadRequest with default headers values
+func NewTimestampResponseBadRequest() *TimestampResponseBadRequest {
+	return &TimestampResponseBadRequest{}
+}
+
+/* TimestampResponseBadRequest describes a response with status code 400, with default header values.
+
+The content supplied to the server was invalid
+*/
+type TimestampResponseBadRequest struct {
+	Payload *models.Error
+}
+
+func (o *TimestampResponseBadRequest) Error() string {
+	return fmt.Sprintf("[POST /api/v1/tsr][%d] timestampResponseBadRequest  %+v", 400, o.Payload)
+}
+func (o *TimestampResponseBadRequest) GetPayload() *models.Error {
+	return o.Payload
+}
+
+func (o *TimestampResponseBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	o.Payload = new(models.Error)
+
+	// response payload
+	if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
+
+// NewTimestampResponseDefault creates a TimestampResponseDefault with default headers values
+func NewTimestampResponseDefault(code int) *TimestampResponseDefault {
+	return &TimestampResponseDefault{
+		_statusCode: code,
+	}
+}
+
+/* TimestampResponseDefault describes a response with status code -1, with default header values.
+
+There was an internal error in the server while processing the request
+*/
+type TimestampResponseDefault struct {
+	_statusCode int
+
+	Payload *models.Error
+}
+
+// Code gets the status code for the timestamp response default response
+func (o *TimestampResponseDefault) Code() int {
+	return o._statusCode
+}
+
+func (o *TimestampResponseDefault) Error() string {
+	return fmt.Sprintf("[POST /api/v1/tsr][%d] timestampResponse default  %+v", o._statusCode, o.Payload)
+}
+func (o *TimestampResponseDefault) GetPayload() *models.Error {
+	return o.Payload
+}
+
+func (o *TimestampResponseDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	o.Payload = new(models.Error)
+
+	// response payload
+	if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/generated/models/timestamp_request.go b/pkg/generated/models/timestamp_request.go
new file mode 100644
index 0000000..f5014d5
--- /dev/null
+++ b/pkg/generated/models/timestamp_request.go
@@ -0,0 +1,67 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"context"
+
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+)
+
+// TimestampRequest timestamp request
+//
+// swagger:model TimestampRequest
+type TimestampRequest struct {
+
+	// RFC 3161 formatted timestamp request
+	// Format: byte
+	RfcRequest strfmt.Base64 `json:"rfcRequest,omitempty"`
+}
+
+// Validate validates this timestamp request
+func (m *TimestampRequest) Validate(formats strfmt.Registry) error {
+	return nil
+}
+
+// ContextValidate validates this timestamp request based on context it is used
+func (m *TimestampRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *TimestampRequest) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *TimestampRequest) UnmarshalBinary(b []byte) error {
+	var res TimestampRequest
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
diff --git a/pkg/generated/models/timestamp_response.go b/pkg/generated/models/timestamp_response.go
new file mode 100644
index 0000000..0f770a4
--- /dev/null
+++ b/pkg/generated/models/timestamp_response.go
@@ -0,0 +1,67 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"context"
+
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+)
+
+// TimestampResponse timestamp response
+//
+// swagger:model TimestampResponse
+type TimestampResponse struct {
+
+	// RFC 3161 formatted timestamp response
+	// Format: byte
+	RfcResponse strfmt.Base64 `json:"rfcResponse,omitempty"`
+}
+
+// Validate validates this timestamp response
+func (m *TimestampResponse) Validate(formats strfmt.Registry) error {
+	return nil
+}
+
+// ContextValidate validates this timestamp response based on context it is used
+func (m *TimestampResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *TimestampResponse) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *TimestampResponse) UnmarshalBinary(b []byte) error {
+	var res TimestampResponse
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
diff --git a/pkg/generated/restapi/configure_rekor_server.go b/pkg/generated/restapi/configure_rekor_server.go
index ef69b08..0d22cfe 100644
--- a/pkg/generated/restapi/configure_rekor_server.go
+++ b/pkg/generated/restapi/configure_rekor_server.go
@@ -35,6 +35,7 @@ import (
 	"github.com/sigstore/rekor/pkg/generated/restapi/operations/entries"
 	"github.com/sigstore/rekor/pkg/generated/restapi/operations/index"
 	"github.com/sigstore/rekor/pkg/generated/restapi/operations/pubkey"
+	"github.com/sigstore/rekor/pkg/generated/restapi/operations/timestamp"
 	"github.com/sigstore/rekor/pkg/generated/restapi/operations/tlog"
 	"github.com/sigstore/rekor/pkg/log"
 	"github.com/sigstore/rekor/pkg/util"
@@ -70,6 +71,9 @@ func configureAPI(api *operations.RekorServerAPI) http.Handler {
 	api.YamlProducer = util.YamlProducer()
 
 	api.ApplicationXPemFileProducer = runtime.TextProducer()
+	api.ApplicationPemCertificateChainProducer = runtime.TextProducer()
+	api.ApplicationTimestampQueryConsumer = runtime.ByteStreamConsumer()
+	api.ApplicationTimestampReplyProducer = runtime.ByteStreamProducer()
 
 	api.EntriesCreateLogEntryHandler = entries.CreateLogEntryHandlerFunc(pkgapi.CreateLogEntryHandler)
 	api.EntriesGetLogEntryByIndexHandler = entries.GetLogEntryByIndexHandlerFunc(pkgapi.GetLogEntryByIndexHandler)
@@ -87,6 +91,9 @@ func configureAPI(api *operations.RekorServerAPI) http.Handler {
 		api.IndexSearchIndexHandler = index.SearchIndexHandlerFunc(pkgapi.SearchIndexNotImplementedHandler)
 	}
 
+	api.TimestampGetTimestampResponseHandler = timestamp.GetTimestampResponseHandlerFunc(pkgapi.TimestampResponseHandler)
+	api.TimestampGetTimestampCertChainHandler = timestamp.GetTimestampCertChainHandlerFunc(pkgapi.GetTimestampCertChainHandler)
+
 	api.PreServerShutdown = func() {}
 
 	api.ServerShutdown = func() {}
@@ -96,9 +103,11 @@ func configureAPI(api *operations.RekorServerAPI) http.Handler {
 	api.AddMiddlewareFor("GET", "/api/v1/log/proof", middleware.NoCache)
 	api.AddMiddlewareFor("GET", "/api/v1/log/entries", middleware.NoCache)
 	api.AddMiddlewareFor("GET", "/api/v1/log/entries/{entryUUID}", middleware.NoCache)
+	api.AddMiddlewareFor("GET", "/api/v1/timestamp", middleware.NoCache)
 
 	// cache forever
 	api.AddMiddlewareFor("GET", "/api/v1/log/publicKey", cacheForever)
+	api.AddMiddlewareFor("GET", "/api/v1/log/timestamp/certchain", cacheForever)
 
 	return setupGlobalMiddleware(api.Serve(setupMiddlewares))
 }
diff --git a/pkg/generated/restapi/doc.go b/pkg/generated/restapi/doc.go
index b2462c6..8c81a2a 100644
--- a/pkg/generated/restapi/doc.go
+++ b/pkg/generated/restapi/doc.go
@@ -25,10 +25,13 @@
 //  Version: 0.0.1
 //
 //  Consumes:
+//    - application/timestamp-query
 //    - application/json
 //    - application/yaml
 //
 //  Produces:
+//    - application/pem-certificate-chain
+//    - application/timestamp-reply
 //    - application/x-pem-file
 //    - application/json
 //    - application/yaml
diff --git a/pkg/generated/restapi/embedded_spec.go b/pkg/generated/restapi/embedded_spec.go
index 98c5fd5..d5ce2d9 100644
--- a/pkg/generated/restapi/embedded_spec.go
+++ b/pkg/generated/restapi/embedded_spec.go
@@ -325,6 +325,77 @@ func init() {
           }
         }
       }
+    },
+    "/api/v1/timestamp": {
+      "post": {
+        "consumes": [
+          "application/timestamp-query"
+        ],
+        "produces": [
+          "application/timestamp-reply"
+        ],
+        "tags": [
+          "timestamp"
+        ],
+        "summary": "Returns a timestamp response generated by Rekor",
+        "operationId": "getTimestampResponse",
+        "parameters": [
+          {
+            "name": "request",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "type": "string",
+              "format": "binary"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Returns a timestamp response",
+            "schema": {
+              "type": "string",
+              "format": "binary"
+            }
+          },
+          "400": {
+            "$ref": "#/responses/BadContent"
+          },
+          "501": {
+            "$ref": "#/responses/NotImplemented"
+          },
+          "default": {
+            "$ref": "#/responses/InternalServerError"
+          }
+        }
+      }
+    },
+    "/api/v1/timestamp/certchain": {
+      "get": {
+        "description": "Returns the certfiicate chain for timestamping that can be used to validate trusted timestamps",
+        "produces": [
+          "application/pem-certificate-chain"
+        ],
+        "tags": [
+          "timestamp"
+        ],
+        "summary": "Retrieve the certfiicate chain for timestamping that can be used to validate trusted timestamps",
+        "operationId": "getTimestampCertChain",
+        "responses": {
+          "200": {
+            "description": "The PEM encoded cert chain",
+            "schema": {
+              "type": "string"
+            }
+          },
+          "404": {
+            "$ref": "#/responses/NotFound"
+          },
+          "default": {
+            "$ref": "#/responses/InternalServerError"
+          }
+        }
+      }
     }
   },
   "definitions": {
@@ -666,6 +737,9 @@ func init() {
     },
     "NotFound": {
       "description": "The content requested could not be found"
+    },
+    "NotImplemented": {
+      "description": "The content requested is not implemented"
     }
   }
 }`))
@@ -1007,6 +1081,86 @@ func init() {
           }
         }
       }
+    },
+    "/api/v1/timestamp": {
+      "post": {
+        "consumes": [
+          "application/timestamp-query"
+        ],
+        "produces": [
+          "application/timestamp-reply"
+        ],
+        "tags": [
+          "timestamp"
+        ],
+        "summary": "Returns a timestamp response generated by Rekor",
+        "operationId": "getTimestampResponse",
+        "parameters": [
+          {
+            "name": "request",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "type": "string",
+              "format": "binary"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Returns a timestamp response",
+            "schema": {
+              "type": "string",
+              "format": "binary"
+            }
+          },
+          "400": {
+            "description": "The content supplied to the server was invalid",
+            "schema": {
+              "$ref": "#/definitions/Error"
+            }
+          },
+          "501": {
+            "description": "The content requested is not implemented"
+          },
+          "default": {
+            "description": "There was an internal error in the server while processing the request",
+            "schema": {
+              "$ref": "#/definitions/Error"
+            }
+          }
+        }
+      }
+    },
+    "/api/v1/timestamp/certchain": {
+      "get": {
+        "description": "Returns the certfiicate chain for timestamping that can be used to validate trusted timestamps",
+        "produces": [
+          "application/pem-certificate-chain"
+        ],
+        "tags": [
+          "timestamp"
+        ],
+        "summary": "Retrieve the certfiicate chain for timestamping that can be used to validate trusted timestamps",
+        "operationId": "getTimestampCertChain",
+        "responses": {
+          "200": {
+            "description": "The PEM encoded cert chain",
+            "schema": {
+              "type": "string"
+            }
+          },
+          "404": {
+            "description": "The content requested could not be found"
+          },
+          "default": {
+            "description": "There was an internal error in the server while processing the request",
+            "schema": {
+              "$ref": "#/definitions/Error"
+            }
+          }
+        }
+      }
     }
   },
   "definitions": {
@@ -2169,6 +2323,9 @@ func init() {
     },
     "NotFound": {
       "description": "The content requested could not be found"
+    },
+    "NotImplemented": {
+      "description": "The content requested is not implemented"
     }
   }
 }`))
diff --git a/pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain.go b/pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain.go
new file mode 100644
index 0000000..6d57996
--- /dev/null
+++ b/pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain.go
@@ -0,0 +1,74 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package pubkey
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+	"net/http"
+
+	"github.com/go-openapi/runtime/middleware"
+)
+
+// GetTimestampCertChainHandlerFunc turns a function with the right signature into a get timestamp cert chain handler
+type GetTimestampCertChainHandlerFunc func(GetTimestampCertChainParams) middleware.Responder
+
+// Handle executing the request and returning a response
+func (fn GetTimestampCertChainHandlerFunc) Handle(params GetTimestampCertChainParams) middleware.Responder {
+	return fn(params)
+}
+
+// GetTimestampCertChainHandler interface for that can handle valid get timestamp cert chain params
+type GetTimestampCertChainHandler interface {
+	Handle(GetTimestampCertChainParams) middleware.Responder
+}
+
+// NewGetTimestampCertChain creates a new http.Handler for the get timestamp cert chain operation
+func NewGetTimestampCertChain(ctx *middleware.Context, handler GetTimestampCertChainHandler) *GetTimestampCertChain {
+	return &GetTimestampCertChain{Context: ctx, Handler: handler}
+}
+
+/* GetTimestampCertChain swagger:route GET /api/v1/log/timestampCertChain pubkey getTimestampCertChain
+
+Retrieve the certfiicate chain for timestamping that can be used to validate trusted timestamps
+
+Returns the certfiicate chain for timestamping that can be used to validate trusted timestamps
+
+*/
+type GetTimestampCertChain struct {
+	Context *middleware.Context
+	Handler GetTimestampCertChainHandler
+}
+
+func (o *GetTimestampCertChain) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
+	route, rCtx, _ := o.Context.RouteInfo(r)
+	if rCtx != nil {
+		*r = *rCtx
+	}
+	var Params = NewGetTimestampCertChainParams()
+	if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
+		o.Context.Respond(rw, r, route.Produces, route, err)
+		return
+	}
+
+	res := o.Handler.Handle(Params) // actually handle the request
+	o.Context.Respond(rw, r, route.Produces, route, res)
+
+}
diff --git a/pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain_parameters.go b/pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain_parameters.go
new file mode 100644
index 0000000..3d4baa3
--- /dev/null
+++ b/pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain_parameters.go
@@ -0,0 +1,62 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package pubkey
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"net/http"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/runtime/middleware"
+)
+
+// NewGetTimestampCertChainParams creates a new GetTimestampCertChainParams object
+//
+// There are no default values defined in the spec.
+func NewGetTimestampCertChainParams() GetTimestampCertChainParams {
+
+	return GetTimestampCertChainParams{}
+}
+
+// GetTimestampCertChainParams contains all the bound params for the get timestamp cert chain operation
+// typically these are obtained from a http.Request
+//
+// swagger:parameters getTimestampCertChain
+type GetTimestampCertChainParams struct {
+
+	// HTTP Request Object
+	HTTPRequest *http.Request `json:"-"`
+}
+
+// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
+// for simple values it will use straight method calls.
+//
+// To ensure default values, the struct must have been initialized with NewGetTimestampCertChainParams() beforehand.
+func (o *GetTimestampCertChainParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
+	var res []error
+
+	o.HTTPRequest = r
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
diff --git a/pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain_responses.go b/pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain_responses.go
new file mode 100644
index 0000000..e439fe9
--- /dev/null
+++ b/pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain_responses.go
@@ -0,0 +1,130 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package pubkey
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"net/http"
+
+	"github.com/go-openapi/runtime"
+
+	"github.com/sigstore/rekor/pkg/generated/models"
+)
+
+// GetTimestampCertChainOKCode is the HTTP code returned for type GetTimestampCertChainOK
+const GetTimestampCertChainOKCode int = 200
+
+/*GetTimestampCertChainOK The PEM encoded cert chain
+
+swagger:response getTimestampCertChainOK
+*/
+type GetTimestampCertChainOK struct {
+
+	/*
+	  In: Body
+	*/
+	Payload string `json:"body,omitempty"`
+}
+
+// NewGetTimestampCertChainOK creates GetTimestampCertChainOK with default headers values
+func NewGetTimestampCertChainOK() *GetTimestampCertChainOK {
+
+	return &GetTimestampCertChainOK{}
+}
+
+// WithPayload adds the payload to the get timestamp cert chain o k response
+func (o *GetTimestampCertChainOK) WithPayload(payload string) *GetTimestampCertChainOK {
+	o.Payload = payload
+	return o
+}
+
+// SetPayload sets the payload to the get timestamp cert chain o k response
+func (o *GetTimestampCertChainOK) SetPayload(payload string) {
+	o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *GetTimestampCertChainOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+	rw.WriteHeader(200)
+	payload := o.Payload
+	if err := producer.Produce(rw, payload); err != nil {
+		panic(err) // let the recovery middleware deal with this
+	}
+}
+
+/*GetTimestampCertChainDefault There was an internal error in the server while processing the request
+
+swagger:response getTimestampCertChainDefault
+*/
+type GetTimestampCertChainDefault struct {
+	_statusCode int
+
+	/*
+	  In: Body
+	*/
+	Payload *models.Error `json:"body,omitempty"`
+}
+
+// NewGetTimestampCertChainDefault creates GetTimestampCertChainDefault with default headers values
+func NewGetTimestampCertChainDefault(code int) *GetTimestampCertChainDefault {
+	if code <= 0 {
+		code = 500
+	}
+
+	return &GetTimestampCertChainDefault{
+		_statusCode: code,
+	}
+}
+
+// WithStatusCode adds the status to the get timestamp cert chain default response
+func (o *GetTimestampCertChainDefault) WithStatusCode(code int) *GetTimestampCertChainDefault {
+	o._statusCode = code
+	return o
+}
+
+// SetStatusCode sets the status to the get timestamp cert chain default response
+func (o *GetTimestampCertChainDefault) SetStatusCode(code int) {
+	o._statusCode = code
+}
+
+// WithPayload adds the payload to the get timestamp cert chain default response
+func (o *GetTimestampCertChainDefault) WithPayload(payload *models.Error) *GetTimestampCertChainDefault {
+	o.Payload = payload
+	return o
+}
+
+// SetPayload sets the payload to the get timestamp cert chain default response
+func (o *GetTimestampCertChainDefault) SetPayload(payload *models.Error) {
+	o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *GetTimestampCertChainDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+	rw.WriteHeader(o._statusCode)
+	if o.Payload != nil {
+		payload := o.Payload
+		if err := producer.Produce(rw, payload); err != nil {
+			panic(err) // let the recovery middleware deal with this
+		}
+	}
+}
diff --git a/pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain_urlbuilder.go b/pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain_urlbuilder.go
new file mode 100644
index 0000000..7f66be8
--- /dev/null
+++ b/pkg/generated/restapi/operations/pubkey/get_timestamp_cert_chain_urlbuilder.go
@@ -0,0 +1,100 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package pubkey
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+	"errors"
+	"net/url"
+	golangswaggerpaths "path"
+)
+
+// GetTimestampCertChainURL generates an URL for the get timestamp cert chain operation
+type GetTimestampCertChainURL struct {
+	_basePath string
+}
+
+// WithBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *GetTimestampCertChainURL) WithBasePath(bp string) *GetTimestampCertChainURL {
+	o.SetBasePath(bp)
+	return o
+}
+
+// SetBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *GetTimestampCertChainURL) SetBasePath(bp string) {
+	o._basePath = bp
+}
+
+// Build a url path and query string
+func (o *GetTimestampCertChainURL) Build() (*url.URL, error) {
+	var _result url.URL
+
+	var _path = "/api/v1/log/timestampCertChain"
+
+	_basePath := o._basePath
+	_result.Path = golangswaggerpaths.Join(_basePath, _path)
+
+	return &_result, nil
+}
+
+// Must is a helper function to panic when the url builder returns an error
+func (o *GetTimestampCertChainURL) Must(u *url.URL, err error) *url.URL {
+	if err != nil {
+		panic(err)
+	}
+	if u == nil {
+		panic("url can't be nil")
+	}
+	return u
+}
+
+// String returns the string representation of the path with query string
+func (o *GetTimestampCertChainURL) String() string {
+	return o.Must(o.Build()).String()
+}
+
+// BuildFull builds a full url with scheme, host, path and query string
+func (o *GetTimestampCertChainURL) BuildFull(scheme, host string) (*url.URL, error) {
+	if scheme == "" {
+		return nil, errors.New("scheme is required for a full url on GetTimestampCertChainURL")
+	}
+	if host == "" {
+		return nil, errors.New("host is required for a full url on GetTimestampCertChainURL")
+	}
+
+	base, err := o.Build()
+	if err != nil {
+		return nil, err
+	}
+
+	base.Scheme = scheme
+	base.Host = host
+	return base, nil
+}
+
+// StringFull returns the string representation of a complete url
+func (o *GetTimestampCertChainURL) StringFull(scheme, host string) string {
+	return o.Must(o.BuildFull(scheme, host)).String()
+}
diff --git a/pkg/generated/restapi/operations/rekor_server_api.go b/pkg/generated/restapi/operations/rekor_server_api.go
index bbe6dd4..1e69e40 100644
--- a/pkg/generated/restapi/operations/rekor_server_api.go
+++ b/pkg/generated/restapi/operations/rekor_server_api.go
@@ -40,6 +40,7 @@ import (
 	"github.com/sigstore/rekor/pkg/generated/restapi/operations/entries"
 	"github.com/sigstore/rekor/pkg/generated/restapi/operations/index"
 	"github.com/sigstore/rekor/pkg/generated/restapi/operations/pubkey"
+	"github.com/sigstore/rekor/pkg/generated/restapi/operations/timestamp"
 	"github.com/sigstore/rekor/pkg/generated/restapi/operations/tlog"
 )
 
@@ -61,9 +62,18 @@ func NewRekorServerAPI(spec *loads.Document) *RekorServerAPI {
 		APIKeyAuthenticator: security.APIKeyAuth,
 		BearerAuthenticator: security.BearerAuth,
 
+		ApplicationTimestampQueryConsumer: runtime.ConsumerFunc(func(r io.Reader, target interface{}) error {
+			return errors.NotImplemented("applicationTimestampQuery consumer has not yet been implemented")
+		}),
 		JSONConsumer: runtime.JSONConsumer(),
 		YamlConsumer: yamlpc.YAMLConsumer(),
 
+		ApplicationPemCertificateChainProducer: runtime.ProducerFunc(func(w io.Writer, data interface{}) error {
+			return errors.NotImplemented("applicationPemCertificateChain producer has not yet been implemented")
+		}),
+		ApplicationTimestampReplyProducer: runtime.ProducerFunc(func(w io.Writer, data interface{}) error {
+			return errors.NotImplemented("applicationTimestampReply producer has not yet been implemented")
+		}),
 		ApplicationXPemFileProducer: runtime.ProducerFunc(func(w io.Writer, data interface{}) error {
 			return errors.NotImplemented("applicationXPemFile producer has not yet been implemented")
 		}),
@@ -88,6 +98,12 @@ func NewRekorServerAPI(spec *loads.Document) *RekorServerAPI {
 		PubkeyGetPublicKeyHandler: pubkey.GetPublicKeyHandlerFunc(func(params pubkey.GetPublicKeyParams) middleware.Responder {
 			return middleware.NotImplemented("operation pubkey.GetPublicKey has not yet been implemented")
 		}),
+		TimestampGetTimestampCertChainHandler: timestamp.GetTimestampCertChainHandlerFunc(func(params timestamp.GetTimestampCertChainParams) middleware.Responder {
+			return middleware.NotImplemented("operation timestamp.GetTimestampCertChain has not yet been implemented")
+		}),
+		TimestampGetTimestampResponseHandler: timestamp.GetTimestampResponseHandlerFunc(func(params timestamp.GetTimestampResponseParams) middleware.Responder {
+			return middleware.NotImplemented("operation timestamp.GetTimestampResponse has not yet been implemented")
+		}),
 		IndexSearchIndexHandler: index.SearchIndexHandlerFunc(func(params index.SearchIndexParams) middleware.Responder {
 			return middleware.NotImplemented("operation index.SearchIndex has not yet been implemented")
 		}),
@@ -122,6 +138,9 @@ type RekorServerAPI struct {
 	// It has a default implementation in the security package, however you can replace it for your particular usage.
 	BearerAuthenticator func(string, security.ScopedTokenAuthentication) runtime.Authenticator
 
+	// ApplicationTimestampQueryConsumer registers a consumer for the following mime types:
+	//   - application/timestamp-query
+	ApplicationTimestampQueryConsumer runtime.Consumer
 	// JSONConsumer registers a consumer for the following mime types:
 	//   - application/json
 	JSONConsumer runtime.Consumer
@@ -129,6 +148,12 @@ type RekorServerAPI struct {
 	//   - application/yaml
 	YamlConsumer runtime.Consumer
 
+	// ApplicationPemCertificateChainProducer registers a producer for the following mime types:
+	//   - application/pem-certificate-chain
+	ApplicationPemCertificateChainProducer runtime.Producer
+	// ApplicationTimestampReplyProducer registers a producer for the following mime types:
+	//   - application/timestamp-reply
+	ApplicationTimestampReplyProducer runtime.Producer
 	// ApplicationXPemFileProducer registers a producer for the following mime types:
 	//   - application/x-pem-file
 	ApplicationXPemFileProducer runtime.Producer
@@ -151,6 +176,10 @@ type RekorServerAPI struct {
 	TlogGetLogProofHandler tlog.GetLogProofHandler
 	// PubkeyGetPublicKeyHandler sets the operation handler for the get public key operation
 	PubkeyGetPublicKeyHandler pubkey.GetPublicKeyHandler
+	// TimestampGetTimestampCertChainHandler sets the operation handler for the get timestamp cert chain operation
+	TimestampGetTimestampCertChainHandler timestamp.GetTimestampCertChainHandler
+	// TimestampGetTimestampResponseHandler sets the operation handler for the get timestamp response operation
+	TimestampGetTimestampResponseHandler timestamp.GetTimestampResponseHandler
 	// IndexSearchIndexHandler sets the operation handler for the search index operation
 	IndexSearchIndexHandler index.SearchIndexHandler
 	// EntriesSearchLogQueryHandler sets the operation handler for the search log query operation
@@ -224,6 +253,9 @@ func (o *RekorServerAPI) RegisterFormat(name string, format strfmt.Format, valid
 func (o *RekorServerAPI) Validate() error {
 	var unregistered []string
 
+	if o.ApplicationTimestampQueryConsumer == nil {
+		unregistered = append(unregistered, "ApplicationTimestampQueryConsumer")
+	}
 	if o.JSONConsumer == nil {
 		unregistered = append(unregistered, "JSONConsumer")
 	}
@@ -231,6 +263,12 @@ func (o *RekorServerAPI) Validate() error {
 		unregistered = append(unregistered, "YamlConsumer")
 	}
 
+	if o.ApplicationPemCertificateChainProducer == nil {
+		unregistered = append(unregistered, "ApplicationPemCertificateChainProducer")
+	}
+	if o.ApplicationTimestampReplyProducer == nil {
+		unregistered = append(unregistered, "ApplicationTimestampReplyProducer")
+	}
 	if o.ApplicationXPemFileProducer == nil {
 		unregistered = append(unregistered, "ApplicationXPemFileProducer")
 	}
@@ -259,6 +297,12 @@ func (o *RekorServerAPI) Validate() error {
 	if o.PubkeyGetPublicKeyHandler == nil {
 		unregistered = append(unregistered, "pubkey.GetPublicKeyHandler")
 	}
+	if o.TimestampGetTimestampCertChainHandler == nil {
+		unregistered = append(unregistered, "timestamp.GetTimestampCertChainHandler")
+	}
+	if o.TimestampGetTimestampResponseHandler == nil {
+		unregistered = append(unregistered, "timestamp.GetTimestampResponseHandler")
+	}
 	if o.IndexSearchIndexHandler == nil {
 		unregistered = append(unregistered, "index.SearchIndexHandler")
 	}
@@ -294,6 +338,8 @@ func (o *RekorServerAPI) ConsumersFor(mediaTypes []string) map[string]runtime.Co
 	result := make(map[string]runtime.Consumer, len(mediaTypes))
 	for _, mt := range mediaTypes {
 		switch mt {
+		case "application/timestamp-query":
+			result["application/timestamp-query"] = o.ApplicationTimestampQueryConsumer
 		case "application/json":
 			result["application/json"] = o.JSONConsumer
 		case "application/yaml":
@@ -313,6 +359,10 @@ func (o *RekorServerAPI) ProducersFor(mediaTypes []string) map[string]runtime.Pr
 	result := make(map[string]runtime.Producer, len(mediaTypes))
 	for _, mt := range mediaTypes {
 		switch mt {
+		case "application/pem-certificate-chain":
+			result["application/pem-certificate-chain"] = o.ApplicationPemCertificateChainProducer
+		case "application/timestamp-reply":
+			result["application/timestamp-reply"] = o.ApplicationTimestampReplyProducer
 		case "application/x-pem-file":
 			result["application/x-pem-file"] = o.ApplicationXPemFileProducer
 		case "application/json":
@@ -383,6 +433,14 @@ func (o *RekorServerAPI) initHandlerCache() {
 		o.handlers["GET"] = make(map[string]http.Handler)
 	}
 	o.handlers["GET"]["/api/v1/log/publicKey"] = pubkey.NewGetPublicKey(o.context, o.PubkeyGetPublicKeyHandler)
+	if o.handlers["GET"] == nil {
+		o.handlers["GET"] = make(map[string]http.Handler)
+	}
+	o.handlers["GET"]["/api/v1/timestamp/certchain"] = timestamp.NewGetTimestampCertChain(o.context, o.TimestampGetTimestampCertChainHandler)
+	if o.handlers["POST"] == nil {
+		o.handlers["POST"] = make(map[string]http.Handler)
+	}
+	o.handlers["POST"]["/api/v1/timestamp"] = timestamp.NewGetTimestampResponse(o.context, o.TimestampGetTimestampResponseHandler)
 	if o.handlers["POST"] == nil {
 		o.handlers["POST"] = make(map[string]http.Handler)
 	}
diff --git a/pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain.go b/pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain.go
new file mode 100644
index 0000000..de58dd3
--- /dev/null
+++ b/pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain.go
@@ -0,0 +1,74 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+	"net/http"
+
+	"github.com/go-openapi/runtime/middleware"
+)
+
+// GetTimestampCertChainHandlerFunc turns a function with the right signature into a get timestamp cert chain handler
+type GetTimestampCertChainHandlerFunc func(GetTimestampCertChainParams) middleware.Responder
+
+// Handle executing the request and returning a response
+func (fn GetTimestampCertChainHandlerFunc) Handle(params GetTimestampCertChainParams) middleware.Responder {
+	return fn(params)
+}
+
+// GetTimestampCertChainHandler interface for that can handle valid get timestamp cert chain params
+type GetTimestampCertChainHandler interface {
+	Handle(GetTimestampCertChainParams) middleware.Responder
+}
+
+// NewGetTimestampCertChain creates a new http.Handler for the get timestamp cert chain operation
+func NewGetTimestampCertChain(ctx *middleware.Context, handler GetTimestampCertChainHandler) *GetTimestampCertChain {
+	return &GetTimestampCertChain{Context: ctx, Handler: handler}
+}
+
+/* GetTimestampCertChain swagger:route GET /api/v1/timestamp/certchain timestamp getTimestampCertChain
+
+Retrieve the certfiicate chain for timestamping that can be used to validate trusted timestamps
+
+Returns the certfiicate chain for timestamping that can be used to validate trusted timestamps
+
+*/
+type GetTimestampCertChain struct {
+	Context *middleware.Context
+	Handler GetTimestampCertChainHandler
+}
+
+func (o *GetTimestampCertChain) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
+	route, rCtx, _ := o.Context.RouteInfo(r)
+	if rCtx != nil {
+		*r = *rCtx
+	}
+	var Params = NewGetTimestampCertChainParams()
+	if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
+		o.Context.Respond(rw, r, route.Produces, route, err)
+		return
+	}
+
+	res := o.Handler.Handle(Params) // actually handle the request
+	o.Context.Respond(rw, r, route.Produces, route, res)
+
+}
diff --git a/pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain_parameters.go b/pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain_parameters.go
new file mode 100644
index 0000000..e177de4
--- /dev/null
+++ b/pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain_parameters.go
@@ -0,0 +1,62 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"net/http"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/runtime/middleware"
+)
+
+// NewGetTimestampCertChainParams creates a new GetTimestampCertChainParams object
+//
+// There are no default values defined in the spec.
+func NewGetTimestampCertChainParams() GetTimestampCertChainParams {
+
+	return GetTimestampCertChainParams{}
+}
+
+// GetTimestampCertChainParams contains all the bound params for the get timestamp cert chain operation
+// typically these are obtained from a http.Request
+//
+// swagger:parameters getTimestampCertChain
+type GetTimestampCertChainParams struct {
+
+	// HTTP Request Object
+	HTTPRequest *http.Request `json:"-"`
+}
+
+// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
+// for simple values it will use straight method calls.
+//
+// To ensure default values, the struct must have been initialized with NewGetTimestampCertChainParams() beforehand.
+func (o *GetTimestampCertChainParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
+	var res []error
+
+	o.HTTPRequest = r
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
diff --git a/pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain_responses.go b/pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain_responses.go
new file mode 100644
index 0000000..6611894
--- /dev/null
+++ b/pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain_responses.go
@@ -0,0 +1,154 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"net/http"
+
+	"github.com/go-openapi/runtime"
+
+	"github.com/sigstore/rekor/pkg/generated/models"
+)
+
+// GetTimestampCertChainOKCode is the HTTP code returned for type GetTimestampCertChainOK
+const GetTimestampCertChainOKCode int = 200
+
+/*GetTimestampCertChainOK The PEM encoded cert chain
+
+swagger:response getTimestampCertChainOK
+*/
+type GetTimestampCertChainOK struct {
+
+	/*
+	  In: Body
+	*/
+	Payload string `json:"body,omitempty"`
+}
+
+// NewGetTimestampCertChainOK creates GetTimestampCertChainOK with default headers values
+func NewGetTimestampCertChainOK() *GetTimestampCertChainOK {
+
+	return &GetTimestampCertChainOK{}
+}
+
+// WithPayload adds the payload to the get timestamp cert chain o k response
+func (o *GetTimestampCertChainOK) WithPayload(payload string) *GetTimestampCertChainOK {
+	o.Payload = payload
+	return o
+}
+
+// SetPayload sets the payload to the get timestamp cert chain o k response
+func (o *GetTimestampCertChainOK) SetPayload(payload string) {
+	o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *GetTimestampCertChainOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+	rw.WriteHeader(200)
+	payload := o.Payload
+	if err := producer.Produce(rw, payload); err != nil {
+		panic(err) // let the recovery middleware deal with this
+	}
+}
+
+// GetTimestampCertChainNotFoundCode is the HTTP code returned for type GetTimestampCertChainNotFound
+const GetTimestampCertChainNotFoundCode int = 404
+
+/*GetTimestampCertChainNotFound The content requested could not be found
+
+swagger:response getTimestampCertChainNotFound
+*/
+type GetTimestampCertChainNotFound struct {
+}
+
+// NewGetTimestampCertChainNotFound creates GetTimestampCertChainNotFound with default headers values
+func NewGetTimestampCertChainNotFound() *GetTimestampCertChainNotFound {
+
+	return &GetTimestampCertChainNotFound{}
+}
+
+// WriteResponse to the client
+func (o *GetTimestampCertChainNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+	rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses
+
+	rw.WriteHeader(404)
+}
+
+/*GetTimestampCertChainDefault There was an internal error in the server while processing the request
+
+swagger:response getTimestampCertChainDefault
+*/
+type GetTimestampCertChainDefault struct {
+	_statusCode int
+
+	/*
+	  In: Body
+	*/
+	Payload *models.Error `json:"body,omitempty"`
+}
+
+// NewGetTimestampCertChainDefault creates GetTimestampCertChainDefault with default headers values
+func NewGetTimestampCertChainDefault(code int) *GetTimestampCertChainDefault {
+	if code <= 0 {
+		code = 500
+	}
+
+	return &GetTimestampCertChainDefault{
+		_statusCode: code,
+	}
+}
+
+// WithStatusCode adds the status to the get timestamp cert chain default response
+func (o *GetTimestampCertChainDefault) WithStatusCode(code int) *GetTimestampCertChainDefault {
+	o._statusCode = code
+	return o
+}
+
+// SetStatusCode sets the status to the get timestamp cert chain default response
+func (o *GetTimestampCertChainDefault) SetStatusCode(code int) {
+	o._statusCode = code
+}
+
+// WithPayload adds the payload to the get timestamp cert chain default response
+func (o *GetTimestampCertChainDefault) WithPayload(payload *models.Error) *GetTimestampCertChainDefault {
+	o.Payload = payload
+	return o
+}
+
+// SetPayload sets the payload to the get timestamp cert chain default response
+func (o *GetTimestampCertChainDefault) SetPayload(payload *models.Error) {
+	o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *GetTimestampCertChainDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+	rw.WriteHeader(o._statusCode)
+	if o.Payload != nil {
+		payload := o.Payload
+		if err := producer.Produce(rw, payload); err != nil {
+			panic(err) // let the recovery middleware deal with this
+		}
+	}
+}
diff --git a/pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain_urlbuilder.go b/pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain_urlbuilder.go
new file mode 100644
index 0000000..25cb613
--- /dev/null
+++ b/pkg/generated/restapi/operations/timestamp/get_timestamp_cert_chain_urlbuilder.go
@@ -0,0 +1,100 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+	"errors"
+	"net/url"
+	golangswaggerpaths "path"
+)
+
+// GetTimestampCertChainURL generates an URL for the get timestamp cert chain operation
+type GetTimestampCertChainURL struct {
+	_basePath string
+}
+
+// WithBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *GetTimestampCertChainURL) WithBasePath(bp string) *GetTimestampCertChainURL {
+	o.SetBasePath(bp)
+	return o
+}
+
+// SetBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *GetTimestampCertChainURL) SetBasePath(bp string) {
+	o._basePath = bp
+}
+
+// Build a url path and query string
+func (o *GetTimestampCertChainURL) Build() (*url.URL, error) {
+	var _result url.URL
+
+	var _path = "/api/v1/timestamp/certchain"
+
+	_basePath := o._basePath
+	_result.Path = golangswaggerpaths.Join(_basePath, _path)
+
+	return &_result, nil
+}
+
+// Must is a helper function to panic when the url builder returns an error
+func (o *GetTimestampCertChainURL) Must(u *url.URL, err error) *url.URL {
+	if err != nil {
+		panic(err)
+	}
+	if u == nil {
+		panic("url can't be nil")
+	}
+	return u
+}
+
+// String returns the string representation of the path with query string
+func (o *GetTimestampCertChainURL) String() string {
+	return o.Must(o.Build()).String()
+}
+
+// BuildFull builds a full url with scheme, host, path and query string
+func (o *GetTimestampCertChainURL) BuildFull(scheme, host string) (*url.URL, error) {
+	if scheme == "" {
+		return nil, errors.New("scheme is required for a full url on GetTimestampCertChainURL")
+	}
+	if host == "" {
+		return nil, errors.New("host is required for a full url on GetTimestampCertChainURL")
+	}
+
+	base, err := o.Build()
+	if err != nil {
+		return nil, err
+	}
+
+	base.Scheme = scheme
+	base.Host = host
+	return base, nil
+}
+
+// StringFull returns the string representation of a complete url
+func (o *GetTimestampCertChainURL) StringFull(scheme, host string) string {
+	return o.Must(o.BuildFull(scheme, host)).String()
+}
diff --git a/pkg/generated/restapi/operations/timestamp/get_timestamp_response.go b/pkg/generated/restapi/operations/timestamp/get_timestamp_response.go
new file mode 100644
index 0000000..56fcc6b
--- /dev/null
+++ b/pkg/generated/restapi/operations/timestamp/get_timestamp_response.go
@@ -0,0 +1,72 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+	"net/http"
+
+	"github.com/go-openapi/runtime/middleware"
+)
+
+// GetTimestampResponseHandlerFunc turns a function with the right signature into a get timestamp response handler
+type GetTimestampResponseHandlerFunc func(GetTimestampResponseParams) middleware.Responder
+
+// Handle executing the request and returning a response
+func (fn GetTimestampResponseHandlerFunc) Handle(params GetTimestampResponseParams) middleware.Responder {
+	return fn(params)
+}
+
+// GetTimestampResponseHandler interface for that can handle valid get timestamp response params
+type GetTimestampResponseHandler interface {
+	Handle(GetTimestampResponseParams) middleware.Responder
+}
+
+// NewGetTimestampResponse creates a new http.Handler for the get timestamp response operation
+func NewGetTimestampResponse(ctx *middleware.Context, handler GetTimestampResponseHandler) *GetTimestampResponse {
+	return &GetTimestampResponse{Context: ctx, Handler: handler}
+}
+
+/* GetTimestampResponse swagger:route POST /api/v1/timestamp timestamp getTimestampResponse
+
+Returns a timestamp response generated by Rekor
+
+*/
+type GetTimestampResponse struct {
+	Context *middleware.Context
+	Handler GetTimestampResponseHandler
+}
+
+func (o *GetTimestampResponse) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
+	route, rCtx, _ := o.Context.RouteInfo(r)
+	if rCtx != nil {
+		*r = *rCtx
+	}
+	var Params = NewGetTimestampResponseParams()
+	if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
+		o.Context.Respond(rw, r, route.Produces, route, err)
+		return
+	}
+
+	res := o.Handler.Handle(Params) // actually handle the request
+	o.Context.Respond(rw, r, route.Produces, route, res)
+
+}
diff --git a/pkg/generated/restapi/operations/timestamp/get_timestamp_response_parameters.go b/pkg/generated/restapi/operations/timestamp/get_timestamp_response_parameters.go
new file mode 100644
index 0000000..231cfe0
--- /dev/null
+++ b/pkg/generated/restapi/operations/timestamp/get_timestamp_response_parameters.go
@@ -0,0 +1,75 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"io"
+	"net/http"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/runtime"
+	"github.com/go-openapi/runtime/middleware"
+)
+
+// NewGetTimestampResponseParams creates a new GetTimestampResponseParams object
+//
+// There are no default values defined in the spec.
+func NewGetTimestampResponseParams() GetTimestampResponseParams {
+
+	return GetTimestampResponseParams{}
+}
+
+// GetTimestampResponseParams contains all the bound params for the get timestamp response operation
+// typically these are obtained from a http.Request
+//
+// swagger:parameters getTimestampResponse
+type GetTimestampResponseParams struct {
+
+	// HTTP Request Object
+	HTTPRequest *http.Request `json:"-"`
+
+	/*
+	  Required: true
+	  In: body
+	*/
+	Request io.ReadCloser
+}
+
+// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
+// for simple values it will use straight method calls.
+//
+// To ensure default values, the struct must have been initialized with NewGetTimestampResponseParams() beforehand.
+func (o *GetTimestampResponseParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
+	var res []error
+
+	o.HTTPRequest = r
+
+	if runtime.HasBody(r) {
+		o.Request = r.Body
+	} else {
+		res = append(res, errors.Required("request", "body", ""))
+	}
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
diff --git a/pkg/generated/restapi/operations/timestamp/get_timestamp_response_responses.go b/pkg/generated/restapi/operations/timestamp/get_timestamp_response_responses.go
new file mode 100644
index 0000000..be7911a
--- /dev/null
+++ b/pkg/generated/restapi/operations/timestamp/get_timestamp_response_responses.go
@@ -0,0 +1,199 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"io"
+	"net/http"
+
+	"github.com/go-openapi/runtime"
+
+	"github.com/sigstore/rekor/pkg/generated/models"
+)
+
+// GetTimestampResponseOKCode is the HTTP code returned for type GetTimestampResponseOK
+const GetTimestampResponseOKCode int = 200
+
+/*GetTimestampResponseOK Returns a timestamp response
+
+swagger:response getTimestampResponseOK
+*/
+type GetTimestampResponseOK struct {
+
+	/*
+	  In: Body
+	*/
+	Payload io.ReadCloser `json:"body,omitempty"`
+}
+
+// NewGetTimestampResponseOK creates GetTimestampResponseOK with default headers values
+func NewGetTimestampResponseOK() *GetTimestampResponseOK {
+
+	return &GetTimestampResponseOK{}
+}
+
+// WithPayload adds the payload to the get timestamp response o k response
+func (o *GetTimestampResponseOK) WithPayload(payload io.ReadCloser) *GetTimestampResponseOK {
+	o.Payload = payload
+	return o
+}
+
+// SetPayload sets the payload to the get timestamp response o k response
+func (o *GetTimestampResponseOK) SetPayload(payload io.ReadCloser) {
+	o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *GetTimestampResponseOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+	rw.WriteHeader(200)
+	payload := o.Payload
+	if err := producer.Produce(rw, payload); err != nil {
+		panic(err) // let the recovery middleware deal with this
+	}
+}
+
+// GetTimestampResponseBadRequestCode is the HTTP code returned for type GetTimestampResponseBadRequest
+const GetTimestampResponseBadRequestCode int = 400
+
+/*GetTimestampResponseBadRequest The content supplied to the server was invalid
+
+swagger:response getTimestampResponseBadRequest
+*/
+type GetTimestampResponseBadRequest struct {
+
+	/*
+	  In: Body
+	*/
+	Payload *models.Error `json:"body,omitempty"`
+}
+
+// NewGetTimestampResponseBadRequest creates GetTimestampResponseBadRequest with default headers values
+func NewGetTimestampResponseBadRequest() *GetTimestampResponseBadRequest {
+
+	return &GetTimestampResponseBadRequest{}
+}
+
+// WithPayload adds the payload to the get timestamp response bad request response
+func (o *GetTimestampResponseBadRequest) WithPayload(payload *models.Error) *GetTimestampResponseBadRequest {
+	o.Payload = payload
+	return o
+}
+
+// SetPayload sets the payload to the get timestamp response bad request response
+func (o *GetTimestampResponseBadRequest) SetPayload(payload *models.Error) {
+	o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *GetTimestampResponseBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+	rw.WriteHeader(400)
+	if o.Payload != nil {
+		payload := o.Payload
+		if err := producer.Produce(rw, payload); err != nil {
+			panic(err) // let the recovery middleware deal with this
+		}
+	}
+}
+
+// GetTimestampResponseNotImplementedCode is the HTTP code returned for type GetTimestampResponseNotImplemented
+const GetTimestampResponseNotImplementedCode int = 501
+
+/*GetTimestampResponseNotImplemented The content requested is not implemented
+
+swagger:response getTimestampResponseNotImplemented
+*/
+type GetTimestampResponseNotImplemented struct {
+}
+
+// NewGetTimestampResponseNotImplemented creates GetTimestampResponseNotImplemented with default headers values
+func NewGetTimestampResponseNotImplemented() *GetTimestampResponseNotImplemented {
+
+	return &GetTimestampResponseNotImplemented{}
+}
+
+// WriteResponse to the client
+func (o *GetTimestampResponseNotImplemented) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+	rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses
+
+	rw.WriteHeader(501)
+}
+
+/*GetTimestampResponseDefault There was an internal error in the server while processing the request
+
+swagger:response getTimestampResponseDefault
+*/
+type GetTimestampResponseDefault struct {
+	_statusCode int
+
+	/*
+	  In: Body
+	*/
+	Payload *models.Error `json:"body,omitempty"`
+}
+
+// NewGetTimestampResponseDefault creates GetTimestampResponseDefault with default headers values
+func NewGetTimestampResponseDefault(code int) *GetTimestampResponseDefault {
+	if code <= 0 {
+		code = 500
+	}
+
+	return &GetTimestampResponseDefault{
+		_statusCode: code,
+	}
+}
+
+// WithStatusCode adds the status to the get timestamp response default response
+func (o *GetTimestampResponseDefault) WithStatusCode(code int) *GetTimestampResponseDefault {
+	o._statusCode = code
+	return o
+}
+
+// SetStatusCode sets the status to the get timestamp response default response
+func (o *GetTimestampResponseDefault) SetStatusCode(code int) {
+	o._statusCode = code
+}
+
+// WithPayload adds the payload to the get timestamp response default response
+func (o *GetTimestampResponseDefault) WithPayload(payload *models.Error) *GetTimestampResponseDefault {
+	o.Payload = payload
+	return o
+}
+
+// SetPayload sets the payload to the get timestamp response default response
+func (o *GetTimestampResponseDefault) SetPayload(payload *models.Error) {
+	o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *GetTimestampResponseDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+	rw.WriteHeader(o._statusCode)
+	if o.Payload != nil {
+		payload := o.Payload
+		if err := producer.Produce(rw, payload); err != nil {
+			panic(err) // let the recovery middleware deal with this
+		}
+	}
+}
diff --git a/pkg/generated/restapi/operations/timestamp/get_timestamp_response_urlbuilder.go b/pkg/generated/restapi/operations/timestamp/get_timestamp_response_urlbuilder.go
new file mode 100644
index 0000000..9a3b72f
--- /dev/null
+++ b/pkg/generated/restapi/operations/timestamp/get_timestamp_response_urlbuilder.go
@@ -0,0 +1,100 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+	"errors"
+	"net/url"
+	golangswaggerpaths "path"
+)
+
+// GetTimestampResponseURL generates an URL for the get timestamp response operation
+type GetTimestampResponseURL struct {
+	_basePath string
+}
+
+// WithBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *GetTimestampResponseURL) WithBasePath(bp string) *GetTimestampResponseURL {
+	o.SetBasePath(bp)
+	return o
+}
+
+// SetBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *GetTimestampResponseURL) SetBasePath(bp string) {
+	o._basePath = bp
+}
+
+// Build a url path and query string
+func (o *GetTimestampResponseURL) Build() (*url.URL, error) {
+	var _result url.URL
+
+	var _path = "/api/v1/timestamp"
+
+	_basePath := o._basePath
+	_result.Path = golangswaggerpaths.Join(_basePath, _path)
+
+	return &_result, nil
+}
+
+// Must is a helper function to panic when the url builder returns an error
+func (o *GetTimestampResponseURL) Must(u *url.URL, err error) *url.URL {
+	if err != nil {
+		panic(err)
+	}
+	if u == nil {
+		panic("url can't be nil")
+	}
+	return u
+}
+
+// String returns the string representation of the path with query string
+func (o *GetTimestampResponseURL) String() string {
+	return o.Must(o.Build()).String()
+}
+
+// BuildFull builds a full url with scheme, host, path and query string
+func (o *GetTimestampResponseURL) BuildFull(scheme, host string) (*url.URL, error) {
+	if scheme == "" {
+		return nil, errors.New("scheme is required for a full url on GetTimestampResponseURL")
+	}
+	if host == "" {
+		return nil, errors.New("host is required for a full url on GetTimestampResponseURL")
+	}
+
+	base, err := o.Build()
+	if err != nil {
+		return nil, err
+	}
+
+	base.Scheme = scheme
+	base.Host = host
+	return base, nil
+}
+
+// StringFull returns the string representation of a complete url
+func (o *GetTimestampResponseURL) StringFull(scheme, host string) string {
+	return o.Must(o.BuildFull(scheme, host)).String()
+}
diff --git a/pkg/generated/restapi/operations/timestamp/timestamp_response.go b/pkg/generated/restapi/operations/timestamp/timestamp_response.go
new file mode 100644
index 0000000..4b895f2
--- /dev/null
+++ b/pkg/generated/restapi/operations/timestamp/timestamp_response.go
@@ -0,0 +1,72 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+	"net/http"
+
+	"github.com/go-openapi/runtime/middleware"
+)
+
+// TimestampResponseHandlerFunc turns a function with the right signature into a timestamp response handler
+type TimestampResponseHandlerFunc func(TimestampResponseParams) middleware.Responder
+
+// Handle executing the request and returning a response
+func (fn TimestampResponseHandlerFunc) Handle(params TimestampResponseParams) middleware.Responder {
+	return fn(params)
+}
+
+// TimestampResponseHandler interface for that can handle valid timestamp response params
+type TimestampResponseHandler interface {
+	Handle(TimestampResponseParams) middleware.Responder
+}
+
+// NewTimestampResponse creates a new http.Handler for the timestamp response operation
+func NewTimestampResponse(ctx *middleware.Context, handler TimestampResponseHandler) *TimestampResponse {
+	return &TimestampResponse{Context: ctx, Handler: handler}
+}
+
+/* TimestampResponse swagger:route POST /api/v1/tsr timestamp timestampResponse
+
+Generates a timestamp response
+
+*/
+type TimestampResponse struct {
+	Context *middleware.Context
+	Handler TimestampResponseHandler
+}
+
+func (o *TimestampResponse) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
+	route, rCtx, _ := o.Context.RouteInfo(r)
+	if rCtx != nil {
+		*r = *rCtx
+	}
+	var Params = NewTimestampResponseParams()
+	if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
+		o.Context.Respond(rw, r, route.Produces, route, err)
+		return
+	}
+
+	res := o.Handler.Handle(Params) // actually handle the request
+	o.Context.Respond(rw, r, route.Produces, route, res)
+
+}
diff --git a/pkg/generated/restapi/operations/timestamp/timestamp_response_parameters.go b/pkg/generated/restapi/operations/timestamp/timestamp_response_parameters.go
new file mode 100644
index 0000000..a4d6710
--- /dev/null
+++ b/pkg/generated/restapi/operations/timestamp/timestamp_response_parameters.go
@@ -0,0 +1,101 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"context"
+	"io"
+	"net/http"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/runtime"
+	"github.com/go-openapi/runtime/middleware"
+	"github.com/go-openapi/validate"
+
+	"github.com/sigstore/rekor/pkg/generated/models"
+)
+
+// NewTimestampResponseParams creates a new TimestampResponseParams object
+//
+// There are no default values defined in the spec.
+func NewTimestampResponseParams() TimestampResponseParams {
+
+	return TimestampResponseParams{}
+}
+
+// TimestampResponseParams contains all the bound params for the timestamp response operation
+// typically these are obtained from a http.Request
+//
+// swagger:parameters timestampResponse
+type TimestampResponseParams struct {
+
+	// HTTP Request Object
+	HTTPRequest *http.Request `json:"-"`
+
+	/*
+	  Required: true
+	  In: body
+	*/
+	Query *models.TimestampRequest
+}
+
+// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
+// for simple values it will use straight method calls.
+//
+// To ensure default values, the struct must have been initialized with NewTimestampResponseParams() beforehand.
+func (o *TimestampResponseParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
+	var res []error
+
+	o.HTTPRequest = r
+
+	if runtime.HasBody(r) {
+		defer r.Body.Close()
+		var body models.TimestampRequest
+		if err := route.Consumer.Consume(r.Body, &body); err != nil {
+			if err == io.EOF {
+				res = append(res, errors.Required("query", "body", ""))
+			} else {
+				res = append(res, errors.NewParseError("query", "body", "", err))
+			}
+		} else {
+			// validate body object
+			if err := body.Validate(route.Formats); err != nil {
+				res = append(res, err)
+			}
+
+			ctx := validate.WithOperationRequest(context.Background())
+			if err := body.ContextValidate(ctx, route.Formats); err != nil {
+				res = append(res, err)
+			}
+
+			if len(res) == 0 {
+				o.Query = &body
+			}
+		}
+	} else {
+		res = append(res, errors.Required("query", "body", ""))
+	}
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
diff --git a/pkg/generated/restapi/operations/timestamp/timestamp_response_responses.go b/pkg/generated/restapi/operations/timestamp/timestamp_response_responses.go
new file mode 100644
index 0000000..88eb8e0
--- /dev/null
+++ b/pkg/generated/restapi/operations/timestamp/timestamp_response_responses.go
@@ -0,0 +1,176 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"net/http"
+
+	"github.com/go-openapi/runtime"
+
+	"github.com/sigstore/rekor/pkg/generated/models"
+)
+
+// TimestampResponseOKCode is the HTTP code returned for type TimestampResponseOK
+const TimestampResponseOKCode int = 200
+
+/*TimestampResponseOK Returns a timestamp response
+
+swagger:response timestampResponseOK
+*/
+type TimestampResponseOK struct {
+
+	/*
+	  In: Body
+	*/
+	Payload *models.TimestampResponse `json:"body,omitempty"`
+}
+
+// NewTimestampResponseOK creates TimestampResponseOK with default headers values
+func NewTimestampResponseOK() *TimestampResponseOK {
+
+	return &TimestampResponseOK{}
+}
+
+// WithPayload adds the payload to the timestamp response o k response
+func (o *TimestampResponseOK) WithPayload(payload *models.TimestampResponse) *TimestampResponseOK {
+	o.Payload = payload
+	return o
+}
+
+// SetPayload sets the payload to the timestamp response o k response
+func (o *TimestampResponseOK) SetPayload(payload *models.TimestampResponse) {
+	o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *TimestampResponseOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+	rw.WriteHeader(200)
+	if o.Payload != nil {
+		payload := o.Payload
+		if err := producer.Produce(rw, payload); err != nil {
+			panic(err) // let the recovery middleware deal with this
+		}
+	}
+}
+
+// TimestampResponseBadRequestCode is the HTTP code returned for type TimestampResponseBadRequest
+const TimestampResponseBadRequestCode int = 400
+
+/*TimestampResponseBadRequest The content supplied to the server was invalid
+
+swagger:response timestampResponseBadRequest
+*/
+type TimestampResponseBadRequest struct {
+
+	/*
+	  In: Body
+	*/
+	Payload *models.Error `json:"body,omitempty"`
+}
+
+// NewTimestampResponseBadRequest creates TimestampResponseBadRequest with default headers values
+func NewTimestampResponseBadRequest() *TimestampResponseBadRequest {
+
+	return &TimestampResponseBadRequest{}
+}
+
+// WithPayload adds the payload to the timestamp response bad request response
+func (o *TimestampResponseBadRequest) WithPayload(payload *models.Error) *TimestampResponseBadRequest {
+	o.Payload = payload
+	return o
+}
+
+// SetPayload sets the payload to the timestamp response bad request response
+func (o *TimestampResponseBadRequest) SetPayload(payload *models.Error) {
+	o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *TimestampResponseBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+	rw.WriteHeader(400)
+	if o.Payload != nil {
+		payload := o.Payload
+		if err := producer.Produce(rw, payload); err != nil {
+			panic(err) // let the recovery middleware deal with this
+		}
+	}
+}
+
+/*TimestampResponseDefault There was an internal error in the server while processing the request
+
+swagger:response timestampResponseDefault
+*/
+type TimestampResponseDefault struct {
+	_statusCode int
+
+	/*
+	  In: Body
+	*/
+	Payload *models.Error `json:"body,omitempty"`
+}
+
+// NewTimestampResponseDefault creates TimestampResponseDefault with default headers values
+func NewTimestampResponseDefault(code int) *TimestampResponseDefault {
+	if code <= 0 {
+		code = 500
+	}
+
+	return &TimestampResponseDefault{
+		_statusCode: code,
+	}
+}
+
+// WithStatusCode adds the status to the timestamp response default response
+func (o *TimestampResponseDefault) WithStatusCode(code int) *TimestampResponseDefault {
+	o._statusCode = code
+	return o
+}
+
+// SetStatusCode sets the status to the timestamp response default response
+func (o *TimestampResponseDefault) SetStatusCode(code int) {
+	o._statusCode = code
+}
+
+// WithPayload adds the payload to the timestamp response default response
+func (o *TimestampResponseDefault) WithPayload(payload *models.Error) *TimestampResponseDefault {
+	o.Payload = payload
+	return o
+}
+
+// SetPayload sets the payload to the timestamp response default response
+func (o *TimestampResponseDefault) SetPayload(payload *models.Error) {
+	o.Payload = payload
+}
+
+// WriteResponse to the client
+func (o *TimestampResponseDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
+
+	rw.WriteHeader(o._statusCode)
+	if o.Payload != nil {
+		payload := o.Payload
+		if err := producer.Produce(rw, payload); err != nil {
+			panic(err) // let the recovery middleware deal with this
+		}
+	}
+}
diff --git a/pkg/generated/restapi/operations/timestamp/timestamp_response_urlbuilder.go b/pkg/generated/restapi/operations/timestamp/timestamp_response_urlbuilder.go
new file mode 100644
index 0000000..89d3d76
--- /dev/null
+++ b/pkg/generated/restapi/operations/timestamp/timestamp_response_urlbuilder.go
@@ -0,0 +1,100 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 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.
+//
+
+package timestamp
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the generate command
+
+import (
+	"errors"
+	"net/url"
+	golangswaggerpaths "path"
+)
+
+// TimestampResponseURL generates an URL for the timestamp response operation
+type TimestampResponseURL struct {
+	_basePath string
+}
+
+// WithBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *TimestampResponseURL) WithBasePath(bp string) *TimestampResponseURL {
+	o.SetBasePath(bp)
+	return o
+}
+
+// SetBasePath sets the base path for this url builder, only required when it's different from the
+// base path specified in the swagger spec.
+// When the value of the base path is an empty string
+func (o *TimestampResponseURL) SetBasePath(bp string) {
+	o._basePath = bp
+}
+
+// Build a url path and query string
+func (o *TimestampResponseURL) Build() (*url.URL, error) {
+	var _result url.URL
+
+	var _path = "/api/v1/tsr"
+
+	_basePath := o._basePath
+	_result.Path = golangswaggerpaths.Join(_basePath, _path)
+
+	return &_result, nil
+}
+
+// Must is a helper function to panic when the url builder returns an error
+func (o *TimestampResponseURL) Must(u *url.URL, err error) *url.URL {
+	if err != nil {
+		panic(err)
+	}
+	if u == nil {
+		panic("url can't be nil")
+	}
+	return u
+}
+
+// String returns the string representation of the path with query string
+func (o *TimestampResponseURL) String() string {
+	return o.Must(o.Build()).String()
+}
+
+// BuildFull builds a full url with scheme, host, path and query string
+func (o *TimestampResponseURL) BuildFull(scheme, host string) (*url.URL, error) {
+	if scheme == "" {
+		return nil, errors.New("scheme is required for a full url on TimestampResponseURL")
+	}
+	if host == "" {
+		return nil, errors.New("host is required for a full url on TimestampResponseURL")
+	}
+
+	base, err := o.Build()
+	if err != nil {
+		return nil, err
+	}
+
+	base.Scheme = scheme
+	base.Host = host
+	return base, nil
+}
+
+// StringFull returns the string representation of a complete url
+func (o *TimestampResponseURL) StringFull(scheme, host string) string {
+	return o.Must(o.BuildFull(scheme, host)).String()
+}
diff --git a/pkg/pki/x509/x509.go b/pkg/pki/x509/x509.go
index 6be398c..7bf2922 100644
--- a/pkg/pki/x509/x509.go
+++ b/pkg/pki/x509/x509.go
@@ -188,3 +188,48 @@ func (k PublicKey) EmailAddresses() []string {
 	}
 	return names
 }
+
+func CertChainToPEM(certChain []*x509.Certificate) ([]byte, error) {
+	var pemBytes bytes.Buffer
+	for _, cert := range certChain {
+		if err := pem.Encode(&pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil {
+			return nil, err
+		}
+	}
+	return pemBytes.Bytes(), nil
+}
+
+func ParseTimestampCertChain(pemBytes []byte) ([]*x509.Certificate, error) {
+	certChain := []*x509.Certificate{}
+	var block *pem.Block
+	block, pemBytes = pem.Decode(pemBytes)
+	for ; block != nil; block, pemBytes = pem.Decode(pemBytes) {
+		if block.Type == "CERTIFICATE" {
+			cert, err := x509.ParseCertificate(block.Bytes)
+			if err != nil {
+				return nil, err
+			}
+			certChain = append(certChain, cert)
+		} else {
+			return nil, errors.New("invalid block type")
+		}
+	}
+	if len(certChain) == 0 {
+		return nil, errors.New("no valid certificates in chain")
+	}
+	// Verify cert chain for timestamping
+	roots := x509.NewCertPool()
+	intermediates := x509.NewCertPool()
+	for _, cert := range certChain[1:(len(certChain) - 1)] {
+		intermediates.AddCert(cert)
+	}
+	roots.AddCert(certChain[len(certChain)-1])
+	if _, err := certChain[0].Verify(x509.VerifyOptions{
+		Roots:         roots,
+		KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping},
+		Intermediates: intermediates,
+	}); err != nil {
+		return nil, err
+	}
+	return certChain, nil
+}
diff --git a/pkg/pki/x509/x509_test.go b/pkg/pki/x509/x509_test.go
index 19bf50d..3f81762 100644
--- a/pkg/pki/x509/x509_test.go
+++ b/pkg/pki/x509/x509_test.go
@@ -17,6 +17,7 @@ package x509
 
 import (
 	"bytes"
+	"context"
 	"crypto"
 	"crypto/ecdsa"
 	"crypto/ed25519"
@@ -27,6 +28,8 @@ import (
 	"encoding/pem"
 	"strings"
 	"testing"
+
+	"github.com/sigstore/rekor/pkg/signer"
 )
 
 // Generated with:
@@ -206,3 +209,44 @@ func TestSignature_VerifyFail(t *testing.T) {
 		})
 	}
 }
+
+func TestNilCertChainToPEM(t *testing.T) {
+	certChain := []*x509.Certificate{}
+	if _, err := CertChainToPEM(certChain); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestCertChain_Verify(t *testing.T) {
+	mem, err := signer.NewMemory()
+	if err != nil {
+		t.Fatal(err)
+	}
+	// A properly created cert chain should encode to PEM OK.
+	ctx := context.Background()
+	pk, err := mem.PublicKey(ctx)
+	if err != nil {
+		t.Fatal(err)
+	}
+	certChain, err := signer.NewTimestampingCertWithSelfSignedCA(pk)
+	if err != nil {
+		t.Fatal(err)
+	}
+	certChainBytes, err := CertChainToPEM(certChain)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Parse and verify timestamping cert chain
+	parsedCertChain, err := ParseTimestampCertChain(certChainBytes)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Compare with original
+	for idx, cert := range parsedCertChain {
+		if !cert.Equal(certChain[idx]) {
+			t.Fatal("unexpected error comparing cert chain")
+		}
+	}
+}
diff --git a/pkg/signer/memory.go b/pkg/signer/memory.go
index b9689e3..4cfe5e9 100644
--- a/pkg/signer/memory.go
+++ b/pkg/signer/memory.go
@@ -21,6 +21,12 @@ import (
 	"crypto/ecdsa"
 	"crypto/elliptic"
 	"crypto/rand"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/asn1"
+	"math/big"
+	"net"
+	"time"
 
 	"github.com/pkg/errors"
 	"github.com/sigstore/sigstore/pkg/signature"
@@ -33,6 +39,71 @@ type Memory struct {
 	signature.ECDSASignerVerifier
 }
 
+// create a self-signed CA and generate a timestamping certificate to rekor
+func NewTimestampingCertWithSelfSignedCA(pub crypto.PublicKey) ([]*x509.Certificate, error) {
+	// generate self-signed CA
+	caPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		return nil, errors.Wrap(err, "generating private key")
+	}
+	ca := &x509.Certificate{
+		SerialNumber: big.NewInt(2019),
+		Subject: pkix.Name{
+			Organization:  []string{"Root CA Test"},
+			Country:       []string{"US"},
+			Province:      []string{""},
+			Locality:      []string{"San Francisco"},
+			StreetAddress: []string{"Golden Gate Bridge"},
+			PostalCode:    []string{"94016"},
+		},
+		NotBefore:             time.Now(),
+		NotAfter:              time.Now().AddDate(10, 0, 0),
+		IsCA:                  true,
+		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageContentCommitment | x509.KeyUsageCertSign,
+		BasicConstraintsValid: true,
+	}
+	caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
+	if err != nil {
+		return nil, err
+	}
+	timestampExt, err := asn1.Marshal([]asn1.ObjectIdentifier{{1, 3, 6, 1, 5, 5, 7, 3, 8}})
+	if err != nil {
+		return nil, err
+	}
+
+	cert := &x509.Certificate{
+		SerialNumber: big.NewInt(1658),
+		Subject: pkix.Name{
+			Organization:  []string{"Rekor Test"},
+			Country:       []string{"US"},
+			Province:      []string{""},
+			Locality:      []string{"San Francisco"},
+			StreetAddress: []string{"Golden Gate Bridge"},
+			PostalCode:    []string{"94016"},
+		},
+		IPAddresses:  []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
+		NotBefore:    time.Now(),
+		NotAfter:     time.Now().AddDate(10, 0, 0),
+		SubjectKeyId: []byte{1, 2, 3, 4, 6},
+		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping},
+		KeyUsage:     x509.KeyUsageContentCommitment,
+		IsCA:         false,
+		ExtraExtensions: []pkix.Extension{
+			{
+				Id:       asn1.ObjectIdentifier{2, 5, 29, 37},
+				Critical: true,
+				Value:    timestampExt,
+			},
+		},
+		BasicConstraintsValid: true,
+	}
+	certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, pub.(*ecdsa.PublicKey), caPrivKey)
+	if err != nil {
+		return nil, err
+	}
+	return x509.ParseCertificates(append(certBytes, caBytes...))
+}
+
 func NewMemory() (*Memory, error) {
 	// generate a keypair
 	privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
diff --git a/pkg/signer/memory_test.go b/pkg/signer/memory_test.go
index b5de0d1..83bcc20 100644
--- a/pkg/signer/memory_test.go
+++ b/pkg/signer/memory_test.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"crypto"
 	"crypto/ecdsa"
+	"crypto/x509"
 	"testing"
 )
 
@@ -55,4 +56,29 @@ func TestMemory(t *testing.T) {
 	if !ecdsa.VerifyASN1(pk, h.Sum(nil), signature) {
 		t.Fatalf("unable to verify signature")
 	}
+
+	// verify signature using the cert's public key
+	certChain, err := NewTimestampingCertWithSelfSignedCA(pk)
+	pkCert, ok := certChain[0].PublicKey.(*ecdsa.PublicKey)
+	if !ok {
+		t.Fatalf("cert ecdsa public key: %v", err)
+	}
+	if !ecdsa.VerifyASN1(pkCert, h.Sum(nil), signature) {
+		t.Fatalf("unable to verify signature")
+	}
+	// verify that the cert chain is configured for timestamping
+	roots := x509.NewCertPool()
+	intermediates := x509.NewCertPool()
+	for _, cert := range certChain[1:(len(certChain) - 1)] {
+		intermediates.AddCert(cert)
+	}
+	roots.AddCert(certChain[len(certChain)-1])
+	_, err = certChain[0].Verify(x509.VerifyOptions{
+		Roots:         roots,
+		KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping},
+		Intermediates: intermediates,
+	})
+	if err != nil {
+		t.Fatalf("invalid timestamping cert chain")
+	}
 }
diff --git a/pkg/util/rfc3161.go b/pkg/util/rfc3161.go
new file mode 100644
index 0000000..6d80cd0
--- /dev/null
+++ b/pkg/util/rfc3161.go
@@ -0,0 +1,242 @@
+//
+// Copyright 2021 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.
+
+package util
+
+import (
+	"context"
+	"crypto"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/asn1"
+	"fmt"
+	"math/big"
+	"time"
+
+	"github.com/sassoftware/relic/lib/pkcs7"
+	"github.com/sassoftware/relic/lib/pkcs9"
+	"github.com/sassoftware/relic/lib/x509tools"
+	"github.com/sigstore/sigstore/pkg/signature"
+)
+
+type GeneralName struct {
+	Name asn1.RawValue `asn1:"optional,tag:4"`
+}
+
+type IssuerNameAndSerial struct {
+	IssuerName   GeneralName
+	SerialNumber *big.Int
+}
+
+type EssCertIDv2 struct {
+	HashAlgorithm       pkix.AlgorithmIdentifier `asn1:"optional"` // SHA256
+	CertHash            []byte
+	IssuerNameAndSerial IssuerNameAndSerial `asn1:"optional"`
+}
+
+type SigningCertificateV2 struct {
+	Certs []EssCertIDv2
+}
+
+func createSigningCertificate(certificate *x509.Certificate) ([]byte, error) {
+	h := crypto.SHA256.New() // TODO: Get from certificate, defaults to 256
+	_, err := h.Write(certificate.Raw)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create hash")
+	}
+	signingCert := SigningCertificateV2{
+		Certs: []EssCertIDv2{{
+			CertHash: h.Sum(nil),
+			IssuerNameAndSerial: IssuerNameAndSerial{
+				IssuerName:   GeneralName{Name: asn1.RawValue{Tag: 4, Class: 2, IsCompound: true, Bytes: certificate.RawIssuer}},
+				SerialNumber: certificate.SerialNumber,
+			},
+		}},
+	}
+	signingCertBytes, err := asn1.Marshal(signingCert)
+	if err != nil {
+		return nil, err
+	}
+	return signingCertBytes, nil
+}
+
+func marshalCertificates(certs []*x509.Certificate) pkcs7.RawCertificates {
+	c := make(pkcs7.RawCertificates, len(certs))
+	for i, cert := range certs {
+		c[i] = asn1.RawValue{FullBytes: cert.Raw}
+	}
+	return c
+}
+
+func getPKIXPublicKeyAlgorithm(cert x509.Certificate) (*pkix.AlgorithmIdentifier, error) {
+	identifier := pkix.AlgorithmIdentifier{
+		Parameters: asn1.NullRawValue,
+	}
+	switch alg := cert.PublicKeyAlgorithm; alg {
+	case x509.RSA:
+		identifier.Algorithm = x509tools.OidPublicKeyRSA
+	case x509.ECDSA:
+		identifier.Algorithm = x509tools.OidPublicKeyECDSA
+	case x509.Ed25519:
+		identifier.Algorithm = asn1.ObjectIdentifier{1, 3, 101, 112}
+	default:
+		return nil, fmt.Errorf("unknown public key algorithm")
+	}
+
+	return &identifier, nil
+}
+
+type TimestampRequestOptions struct {
+	// The policy that the client expects the TSA to use for creating the timestamp token.
+	// If no policy is specified the TSA uses its default policy.
+	TSAPolicyOid asn1.ObjectIdentifier
+
+	// The nonce to specify in the request.
+	Nonce *big.Int
+
+	// Hash function to use when constructing the timestamp request. Defaults to SHA-256.
+	Hash crypto.Hash
+}
+
+func TimestampRequestFromDigest(digest []byte, opts TimestampRequestOptions) (*pkcs9.TimeStampReq, error) {
+	alg, _ := x509tools.PkixDigestAlgorithm(opts.Hash)
+	msg := pkcs9.TimeStampReq{
+		Version: 1,
+		MessageImprint: pkcs9.MessageImprint{
+			HashAlgorithm: alg,
+			HashedMessage: digest,
+		},
+		CertReq: true,
+	}
+	if opts.Nonce != nil {
+		msg.Nonce = opts.Nonce
+	}
+	if opts.TSAPolicyOid != nil {
+		msg.ReqPolicy = opts.TSAPolicyOid
+	}
+
+	return &msg, nil
+}
+
+func ParseTimestampRequest(data []byte) (*pkcs9.TimeStampReq, error) {
+	msg := new(pkcs9.TimeStampReq)
+	if rest, err := asn1.Unmarshal(data, msg); err != nil {
+		return nil, fmt.Errorf("error umarshalling request")
+	} else if len(rest) != 0 {
+		return nil, fmt.Errorf("error umarshalling request, trailing bytes")
+	}
+	return msg, nil
+}
+
+func CreateRfc3161Response(ctx context.Context, req pkcs9.TimeStampReq, certChain []*x509.Certificate, signer signature.Signer) (*pkcs9.TimeStampResp, error) {
+	// Populate TSTInfo.
+	genTimeBytes, err := asn1.MarshalWithParams(time.Now(), "generalized")
+	if err != nil {
+		return nil, err
+	}
+	policy := asn1.ObjectIdentifier{1, 2, 3, 4, 1}
+	if req.ReqPolicy.String() != "" {
+		policy = req.ReqPolicy
+	}
+
+	info := pkcs9.TSTInfo{
+		Version:        req.Version,
+		MessageImprint: req.MessageImprint,
+		// directoryName is tag 4 https://datatracker.ietf.org/doc/html/rfc3280#section-4.2.1.7
+		TSA: pkcs9.GeneralName{Value: asn1.RawValue{Tag: 4, Class: 2, IsCompound: true, Bytes: certChain[0].RawSubject}},
+		// TODO: Ensure that every (SerialNumber, TSA name) identifies a unique token.
+		SerialNumber: x509tools.MakeSerial(),
+		GenTime:      asn1.RawValue{FullBytes: genTimeBytes},
+		Nonce:        req.Nonce,
+		Policy:       policy,
+		Extensions:   req.Extensions,
+	}
+
+	encoded, err := asn1.Marshal(info)
+	if err != nil {
+		return nil, err
+	}
+	contentInfo, err := pkcs7.NewContentInfo(pkcs9.OidTSTInfo, encoded)
+	if err != nil {
+		return nil, err
+	}
+
+	// TODO: Does this need to match the hash algorithm in the request?
+	h := crypto.SHA256.New()
+	alg, _ := x509tools.PkixDigestAlgorithm(crypto.SHA256)
+	contentInfoBytes, _ := contentInfo.Bytes()
+	h.Write(contentInfoBytes)
+	digest := h.Sum(nil)
+
+	// Create SignerInfo and signature.
+	signingCert, err := createSigningCertificate(certChain[0])
+	if err != nil {
+		return nil, err
+	}
+	attributes := new(pkcs7.AttributeList)
+	if err := attributes.Add(pkcs7.OidAttributeContentType, contentInfo.ContentType); err != nil {
+		return nil, err
+	}
+	if err := attributes.Add(pkcs7.OidAttributeMessageDigest, digest); err != nil {
+		return nil, err
+	}
+	if err := attributes.Add(asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 47}, signingCert); err != nil {
+		return nil, err
+	}
+
+	// The signature is over the entire authenticated attributes, not just the TstInfo.
+	attrBytes, err := attributes.Bytes()
+	if err != nil {
+		return nil, err
+	}
+	// Get signature.
+	signature, _, err := signer.Sign(ctx, attrBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	sigAlg, err := getPKIXPublicKeyAlgorithm(*certChain[0])
+	if err != nil {
+		return nil, err
+	}
+
+	response := pkcs9.TimeStampResp{
+		Status: pkcs9.PKIStatusInfo{
+			Status: 0,
+		},
+		TimeStampToken: pkcs7.ContentInfoSignedData{
+			ContentType: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}, // id-signedData
+			Content: pkcs7.SignedData{
+				Version:                    1,
+				DigestAlgorithmIdentifiers: []pkix.AlgorithmIdentifier{alg},
+				ContentInfo:                contentInfo,
+				Certificates:               marshalCertificates(certChain),
+				CRLs:                       nil,
+				SignerInfos: []pkcs7.SignerInfo{{
+					Version: 1,
+					IssuerAndSerialNumber: pkcs7.IssuerAndSerial{
+						IssuerName:   asn1.RawValue{FullBytes: certChain[0].RawIssuer},
+						SerialNumber: certChain[0].SerialNumber,
+					},
+					DigestAlgorithm:           alg,
+					DigestEncryptionAlgorithm: *sigAlg,
+					AuthenticatedAttributes:   *attributes,
+					EncryptedDigest:           signature,
+				}},
+			},
+		},
+	}
+	return &response, nil
+}
diff --git a/pkg/util/rfc3161_test.go b/pkg/util/rfc3161_test.go
new file mode 100644
index 0000000..98c9dfa
--- /dev/null
+++ b/pkg/util/rfc3161_test.go
@@ -0,0 +1,164 @@
+//
+// Copyright 2021 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.
+
+package util
+
+import (
+	"bytes"
+	"context"
+	"crypto"
+	"encoding/asn1"
+	"fmt"
+	"io/ioutil"
+	"math/big"
+	"testing"
+
+	"github.com/sassoftware/relic/lib/pkcs9"
+	"github.com/sassoftware/relic/lib/x509tools"
+	"github.com/sigstore/rekor/pkg/signer"
+)
+
+func TestCreateTimestampRequest(t *testing.T) {
+	type TestCase struct {
+		caseDesc      string
+		entry         []byte
+		expectSuccess bool
+		nonce         *big.Int
+		policy        asn1.ObjectIdentifier
+	}
+
+	fileBytes, _ := ioutil.ReadFile("../../tests/test_file.txt")
+	testCases := []TestCase{
+		{
+			caseDesc:      "valid timestamp request",
+			entry:         fileBytes,
+			expectSuccess: true,
+			nonce:         x509tools.MakeSerial(),
+		},
+		{
+			caseDesc:      "valid timestamp request no nonce",
+			entry:         fileBytes,
+			expectSuccess: true,
+		},
+		{
+			caseDesc:      "valid timestamp request with TSA policy id",
+			entry:         fileBytes,
+			expectSuccess: true,
+			policy:        asn1.ObjectIdentifier{1, 2, 3, 4, 5},
+		},
+	}
+	for _, tc := range testCases {
+		opts := TimestampRequestOptions{
+			Hash:         crypto.SHA256,
+			Nonce:        tc.nonce,
+			TSAPolicyOid: tc.policy,
+		}
+		h := opts.Hash.New()
+		h.Write(tc.entry)
+		digest := h.Sum(nil)
+		req, err := TimestampRequestFromDigest(digest, opts)
+		if (err == nil) != tc.expectSuccess {
+			t.Errorf("unexpected error in test case '%v': %v", tc.caseDesc, err)
+		}
+		// Validate that the message hash matches the original file has.
+		if !bytes.Equal(digest, req.MessageImprint.HashedMessage) {
+			t.Errorf("unexpected error in test case '%v': %v", tc.caseDesc, "hashes do not match")
+		}
+		if tc.nonce != nil {
+			if tc.nonce.Cmp(req.Nonce) != 0 {
+				t.Errorf("unexpected error in test case '%v': %v", tc.caseDesc, "nonce does not match")
+			}
+		} else if req.Nonce != nil {
+			t.Errorf("unexpected error in test case '%v': %v", tc.caseDesc, fmt.Sprintf("nonce does not match got (%s) expected nil", req.Nonce.String()))
+		}
+		if tc.policy != nil {
+			if !tc.policy.Equal(req.ReqPolicy) {
+				t.Errorf("unexpected error in test case '%v': %v", tc.caseDesc, "policy does not match")
+			}
+		} else if req.ReqPolicy != nil {
+			t.Errorf("unexpected error in test case '%v': %v", tc.caseDesc, "policy does not match")
+		}
+	}
+}
+
+func TestParseTimestampRequest(t *testing.T) {
+	type TestCase struct {
+		caseDesc      string
+		entry         []byte
+		expectSuccess bool
+	}
+
+	requestBytes, _ := ioutil.ReadFile("../../tests/test_request.tsq")
+	fileBytes, _ := ioutil.ReadFile("../../tests/test_file.txt")
+
+	testCases := []TestCase{
+		{
+			caseDesc:      "valid timestamp request",
+			entry:         requestBytes,
+			expectSuccess: true,
+		},
+		{
+			caseDesc:      "invalid timestamp request",
+			entry:         fileBytes,
+			expectSuccess: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		if _, err := ParseTimestampRequest(tc.entry); (err == nil) != tc.expectSuccess {
+			t.Errorf("unexpected error in test case '%v': %v", tc.caseDesc, err)
+		}
+	}
+}
+
+// Create an in-memory CA and TSA and verify the response.
+func TestCreateRFC3161Response(t *testing.T) {
+	ctx := context.Background()
+	mem, err := signer.NewMemory()
+	if err != nil {
+		t.Error(err)
+	}
+	pk, err := mem.PublicKey(ctx)
+	if err != nil {
+		t.Fatal(err)
+	}
+	certChain, err := signer.NewTimestampingCertWithSelfSignedCA(pk)
+	if err != nil {
+		t.Error(err)
+	}
+
+	fileBytes, _ := ioutil.ReadFile("../../tests/test_file.txt")
+	opts := TimestampRequestOptions{
+		Hash:  crypto.SHA256,
+		Nonce: x509tools.MakeSerial(),
+	}
+	h := opts.Hash.New()
+	h.Write(fileBytes)
+	digest := h.Sum(nil)
+	req, err := TimestampRequestFromDigest(digest, opts)
+	if err != nil {
+		t.Error(err)
+	}
+
+	resp, err := CreateRfc3161Response(ctx, *req, certChain, mem)
+	if err != nil {
+		t.Error(err)
+	}
+
+	_, err = pkcs9.Verify(&resp.TimeStampToken, fileBytes, certChain)
+	if err != nil {
+		t.Error(err)
+	}
+}
diff --git a/tests/e2e_test.go b/tests/e2e_test.go
index 8bb0ee1..7510ebc 100644
--- a/tests/e2e_test.go
+++ b/tests/e2e_test.go
@@ -18,6 +18,7 @@
 package e2e
 
 import (
+	"bytes"
 	"context"
 	"crypto"
 	"crypto/ecdsa"
@@ -40,7 +41,9 @@ import (
 	"github.com/go-openapi/strfmt"
 	"github.com/go-openapi/swag"
 	"github.com/sigstore/rekor/cmd/rekor-cli/app"
+	"github.com/sigstore/rekor/pkg/generated/client"
 	"github.com/sigstore/rekor/pkg/generated/client/entries"
+	"github.com/sigstore/rekor/pkg/generated/client/timestamp"
 	"github.com/sigstore/rekor/pkg/generated/models"
 	"github.com/sigstore/rekor/pkg/signer"
 	rekord "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1"
@@ -476,6 +479,70 @@ func TestSignedEntryTimestamp(t *testing.T) {
 	}
 }
 
+func TestTimestampResponseCLI(t *testing.T) {
+	ctx := context.Background()
+	payload := []byte("i am a cat")
+	// Create files for data, response, and CA.
+
+	filePath := filepath.Join(t.TempDir(), "file.txt")
+	CAPath := filepath.Join(t.TempDir(), "ca.pem")
+	responsePath := filepath.Join(t.TempDir(), "response.tsr")
+	if err := ioutil.WriteFile(filePath, payload, 0644); err != nil {
+		t.Fatal(err)
+	}
+
+	out := runCli(t, "timestamp", "--artifact", filePath, "--out", responsePath)
+	outputContains(t, out, "Wrote response to")
+
+	rekorClient, err := app.GetRekorClient("http://localhost:3000")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	certChain := rekorTimestampCertChain(t, ctx, rekorClient)
+	var rootCABytes bytes.Buffer
+	if err := pem.Encode(&rootCABytes, &pem.Block{Type: "CERTIFICATE", Bytes: certChain[len(certChain)-1].Raw}); err != nil {
+		t.Fatal(err)
+	}
+	if err := ioutil.WriteFile(CAPath, rootCABytes.Bytes(), 0644); err != nil {
+		t.Fatal(err)
+	}
+
+	// Use openssl to verify
+	cmd := exec.Command("openssl", "ts", "-verify", "-data", filePath, "-in", responsePath, "-CAfile", CAPath)
+	errs := &bytes.Buffer{}
+
+	cmd.Stderr = errs
+	if err := cmd.Run(); err != nil {
+		// Check that the result was OK.
+		if len(errs.Bytes()) > 0 {
+			t.Fatalf("error verifying with openssl %s", errs.String())
+		}
+
+	}
+
+	// Now try with the digest.
+	h := crypto.SHA256.New()
+	if _, err := h.Write(payload); err != nil {
+		t.Fatalf("error creating digest")
+	}
+	digest := h.Sum(nil)
+	hexDigest := hex.EncodeToString(digest)
+	out = runCli(t, "timestamp", "--artifact-hash", hexDigest, "--out", responsePath)
+	outputContains(t, out, "Wrote response to")
+	cmd = exec.Command("openssl", "ts", "-verify", "-digest", hexDigest, "-in", responsePath, "-CAfile", CAPath)
+	errs = &bytes.Buffer{}
+
+	cmd.Stderr = errs
+	if err := cmd.Run(); err != nil {
+		// Check that the result was OK.
+		if len(errs.Bytes()) > 0 {
+			t.Fatalf("error verifying with openssl %s", errs.String())
+		}
+
+	}
+}
+
 func TestGetNonExistantIndex(t *testing.T) {
 	// this index is extremely likely to not exist
 	out := runCliErr(t, "get", "--log-index", "100000000")
@@ -487,3 +554,31 @@ func TestGetNonExistantUUID(t *testing.T) {
 	out := runCliErr(t, "get", "--uuid", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
 	outputContains(t, out, "404")
 }
+
+func rekorTimestampCertChain(t *testing.T, ctx context.Context, c *client.Rekor) []*x509.Certificate {
+	resp, err := c.Timestamp.GetTimestampCertChain(&timestamp.GetTimestampCertChainParams{Context: ctx})
+	if err != nil {
+		t.Fatal(err)
+	}
+	certChainBytes := []byte(resp.GetPayload())
+
+	var block *pem.Block
+	block, certChainBytes = pem.Decode(certChainBytes)
+	certificates := []*x509.Certificate{}
+	for ; block != nil; block, certChainBytes = pem.Decode(certChainBytes) {
+		if block.Type == "CERTIFICATE" {
+			cert, err := x509.ParseCertificate(block.Bytes)
+			if err != nil {
+				t.Fatal(err)
+			}
+			certificates = append(certificates, cert)
+		} else {
+			t.Fatal(err)
+		}
+	}
+
+	if len(certificates) == 0 {
+		t.Fatal("could not find certificates")
+	}
+	return certificates
+}
diff --git a/tests/test_request.tsq b/tests/test_request.tsq
new file mode 100644
index 0000000000000000000000000000000000000000..134012f642a2087f73c3e4842e3ee8c436d0a38d
GIT binary patch
literal 59
zcmV-B0L1?=IRXIzFflL<1_@w>NC9O71OfpC00bcI>aX+hcS_QThI4leZDa|4IUB=b
R*Vg+qm@VH9{TRyu0srvx6(axu

literal 0
HcmV?d00001

-- 
GitLab