//
// Copyright 2022 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 api
import (
"bytes"
"context"
"crypto"
"crypto/x509"
"fmt"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/timestamp-authority/v2/pkg/log"
"github.com/sigstore/timestamp-authority/v2/pkg/signer"
tsx509 "github.com/sigstore/timestamp-authority/v2/pkg/x509"
)
type API struct {
tsaSigner crypto.Signer // the signer to use for timestamping
tsaSignerHash crypto.Hash // hash algorithm used to hash pre-signed timestamps
certChain []*x509.Certificate // timestamping cert chain
certChainPem string // PEM encoded timestamping cert chain
includeChain bool // Whether to include the full issuing chain or just the leaf certificate
useHTTP201 bool // Whether to use HTTP 201 Created instead of HTTP 200 OK for timestamp responses
}
func NewAPI() (*API, error) {
ctx := context.Background()
tsaSignerHash, err := signer.HashToAlg(viper.GetString("timestamp-signer-hash"))
if err != nil {
return nil, errors.Wrap(err, "error getting hash")
}
tsaSigner, err := signer.NewCryptoSigner(ctx, tsaSignerHash,
viper.GetString("timestamp-signer"),
viper.GetString("kms-key-resource"),
viper.GetString("tink-key-resource"), viper.GetString("tink-keyset-path"),
viper.GetString("tink-hcvault-token"),
viper.GetString("file-signer-key-path"), viper.GetString("file-signer-passwd"))
if err != nil {
return nil, errors.Wrap(err, "getting new tsa signer")
}
var certChain []*x509.Certificate
// KMS, Tink and File signers require a provided certificate chain
if viper.GetString("timestamp-signer") != signer.MemoryScheme {
certChainPath := viper.GetString("certificate-chain-path")
data, err := os.ReadFile(filepath.Clean(certChainPath))
if err != nil {
return nil, err
}
certChain, err = cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(data))
if err != nil {
return nil, err
}
if err := tsx509.VerifyCertChain(certChain, tsaSigner, viper.GetBool("enforce-intermediate-eku")); err != nil {
return nil, err
}
} else {
// Generate an in-memory TSA certificate chain
certChain, err = signer.NewTimestampingCertWithChain(tsaSigner)
if err != nil {
return nil, errors.Wrap(err, "generating timestamping cert chain")
}
}
certChainPEM, err := cryptoutils.MarshalCertificatesToPEM(certChain)
if err != nil {
return nil, fmt.Errorf("marshal certificates to PEM: %w", err)
}
return &API{
tsaSigner: tsaSigner,
tsaSignerHash: tsaSignerHash,
certChain: certChain,
certChainPem: string(certChainPEM),
includeChain: viper.GetBool("include-chain-in-response"),
useHTTP201: viper.GetBool("use-http-201"),
}, nil
}
var (
api *API
)
func ConfigureAPI() {
var err error
api, err = NewAPI()
if err != nil {
log.Logger.Panic(err)
}
}
// Copyright 2022 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 api
import (
"fmt"
"net/http"
"regexp"
"github.com/go-openapi/runtime/middleware"
"github.com/mitchellh/mapstructure"
"github.com/sigstore/timestamp-authority/v2/pkg/generated/models"
"github.com/sigstore/timestamp-authority/v2/pkg/generated/restapi/operations/timestamp"
"github.com/sigstore/timestamp-authority/v2/pkg/log"
)
const (
failedToGenerateTimestampResponse = "Error generating timestamp response"
WeakHashAlgorithmTimestampRequest = "Weak hash algorithm in timestamp request"
InconsistentDigestLengthTimestampRequest = "Message digest has incorrect length for specified algorithm"
)
func errorMsg(message string, code int) *models.Error {
return &models.Error{
Code: int64(code),
Message: message,
}
}
func handleTimestampAPIError(params interface{}, code int, err error, message string, fields ...interface{}) middleware.Responder {
if message == "" {
message = http.StatusText(code)
}
re := regexp.MustCompile("^(.*)Params$")
typeStr := fmt.Sprintf("%T", params)
handler := re.FindStringSubmatch(typeStr)[1]
logMsg := func(r *http.Request) {
if code < http.StatusInternalServerError {
log.RequestIDLogger(r).Warnw(message, append([]interface{}{"handler", handler, "statusCode", code, "error", err}, fields...)...)
} else {
log.RequestIDLogger(r).Errorw(message, append([]interface{}{"handler", handler, "statusCode", code, "error", err}, fields...)...)
}
paramsFields := map[string]interface{}{}
if err := mapstructure.Decode(params, ¶msFields); err == nil {
log.RequestIDLogger(r).Debug(paramsFields)
}
}
switch params := params.(type) {
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))
}
}
//
// Copyright 2022 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 api
import (
"math"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"sigs.k8s.io/release-utils/version"
)
var (
MetricLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "timestamp_authority_api_latency",
Help: "API Latency on calls",
}, []string{"path", "code"})
MetricLatencySummary = promauto.NewSummaryVec(prometheus.SummaryOpts{
Name: "timestamp_authority_api_latency_summary",
Help: "API Latency on calls",
}, []string{"path", "code"})
MetricRequestLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "timestamp_authority_latency_by_api",
Help: "API Latency (in ns) by path and method",
Buckets: prometheus.ExponentialBucketsRange(
float64(time.Millisecond),
float64(4*time.Second),
10),
}, []string{"path", "method"})
MetricRequestCount = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "timestamp_authority_http_requests_total",
Help: "Total number of HTTP requests by status code, path, and method.",
}, []string{"code", "path", "method"})
MetricNTPLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "timestamp_authority_ntp_latency",
Help: "NTP request latency",
}, []string{"host"})
MetricNTPSyncCount = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "timestamp_authority_ntp_sync_total",
Help: "Total number of NTP requests against a remote server",
}, []string{"host", "failed"})
MetricNTPErrorCount = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "timestamp_authority_ntp_errors_total",
Help: "Total number of NTP related errors",
}, []string{"reason"})
_ = promauto.NewGaugeFunc(
prometheus.GaugeOpts{
Name: "timestamp_authority_certificate_valid_days_remaining",
Help: "Number of days remaining in validity period of signing certificate",
},
func() float64 {
// if api hasn't been initialized yet, then we can't know the validity period;
// so we return MaxFloat64 to not cause an alarm if someone fetches the metric
// before the initialization has completed
if api == nil {
return math.MaxFloat64
}
// compute minimum validity inclusive of leaf, any intermediates (if present), and root
minValidity := api.certChain[0].NotAfter
for _, cert := range api.certChain[1:] {
if cert.NotAfter.Before(minValidity) {
minValidity = cert.NotAfter
}
}
return time.Until(minValidity).Hours() / 24
})
_ = promauto.NewGaugeFunc(
prometheus.GaugeOpts{
Namespace: "timestamp_authority",
Name: "build_info",
Help: "A metric with a constant '1' value labeled by version, revision, branch, and goversion from which timestamp-authority was built.",
ConstLabels: prometheus.Labels{
"version": version.GetVersionInfo().GitVersion,
"revision": version.GetVersionInfo().GitCommit,
"build_date": version.GetVersionInfo().BuildDate,
"goversion": version.GetVersionInfo().GoVersion,
},
},
func() float64 { return 1 },
)
)
// Copyright 2022 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 api
import (
"bytes"
"crypto"
"encoding/asn1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"strconv"
"strings"
"time"
"github.com/digitorus/timestamp"
"github.com/go-openapi/runtime/middleware"
"github.com/pkg/errors"
ts "github.com/sigstore/timestamp-authority/v2/pkg/generated/restapi/operations/timestamp"
"github.com/sigstore/timestamp-authority/v2/pkg/verification"
)
type JSONRequest struct {
ArtifactHash string `json:"artifactHash"`
Certificates bool `json:"certificates"`
HashAlgorithm string `json:"hashAlgorithm"`
Nonce *big.Int `json:"nonce"`
TSAPolicyOID string `json:"tsaPolicyOID"`
}
func getHashAlg(alg string) (crypto.Hash, string, error) {
lowercaseAlg := strings.ToLower(alg)
switch lowercaseAlg {
case "sha256":
return crypto.SHA256, "", nil
case "sha384":
return crypto.SHA384, "", nil
case "sha512":
return crypto.SHA512, "", nil
case "sha1":
return 0, WeakHashAlgorithmTimestampRequest, verification.ErrWeakHashAlg
default:
return 0, failedToGenerateTimestampResponse, fmt.Errorf("unsupported hash algorithm: %s", alg)
}
}
// ParseJSONRequest parses a JSON request into a timestamp.Request struct
func ParseJSONRequest(reqBytes []byte) (*timestamp.Request, string, error) {
// unmarshal the request bytes into a JSONRequest struct
var req JSONRequest
if err := json.Unmarshal(reqBytes, &req); err != nil {
return nil, failedToGenerateTimestampResponse, fmt.Errorf("failed to parse JSON into request: %v", err)
}
// after unmarshalling, parse the JSONRequest.Artifact into a Reader and parse the remaining
// fields into a a timestamp.RequestOptions struct
hashAlgo, errMsg, err := getHashAlg(req.HashAlgorithm)
if err != nil {
return nil, errMsg, fmt.Errorf("failed to parse hash algorithm: %v", err)
}
var oidInts []int
if req.TSAPolicyOID == "" {
oidInts = nil
} else {
for _, v := range strings.Split(req.TSAPolicyOID, ".") {
i, _ := strconv.Atoi(v)
oidInts = append(oidInts, i)
}
}
// decode the base64 encoded artifact hash
decoded, err := base64.StdEncoding.DecodeString(req.ArtifactHash)
if err != nil {
return nil, failedToGenerateTimestampResponse, fmt.Errorf("failed to decode base64 encoded artifact hash: %v", err)
}
// create a timestamp request from the request's JSON body
tsReq := timestamp.Request{
HashAlgorithm: hashAlgo,
HashedMessage: decoded,
Certificates: req.Certificates,
Nonce: req.Nonce,
TSAPolicyOID: oidInts,
}
return verifyTimestampRequest(&tsReq)
}
func parseDERRequest(reqBytes []byte) (*timestamp.Request, string, error) {
parsed, err := timestamp.ParseRequest(reqBytes)
if err != nil {
return nil, failedToGenerateTimestampResponse, err
}
return verifyTimestampRequest(parsed)
}
func getContentType(r *http.Request) (string, error) {
contentTypeHeader := r.Header.Get("Content-Type")
splitHeader := strings.Split(contentTypeHeader, "application/")
if len(splitHeader) != 2 {
return "", errors.New("expected header value to be split into two pieces")
}
return splitHeader[1], nil
}
func requestBodyToTimestampReq(reqBytes []byte, contentType string) (*timestamp.Request, string, error) {
switch contentType {
case "json":
return ParseJSONRequest(reqBytes)
case "timestamp-query":
return parseDERRequest(reqBytes)
default:
return nil, failedToGenerateTimestampResponse, fmt.Errorf("unsupported content type")
}
}
func TimestampResponseHandler(params ts.GetTimestampResponseParams) middleware.Responder {
requestBytes, err := io.ReadAll(params.Request)
if err != nil {
return handleTimestampAPIError(params, http.StatusBadRequest, err, failedToGenerateTimestampResponse)
}
contentType, err := getContentType(params.HTTPRequest)
if err != nil {
return handleTimestampAPIError(params, http.StatusUnsupportedMediaType, err, failedToGenerateTimestampResponse)
}
req, errMsg, err := requestBodyToTimestampReq(requestBytes, contentType)
if err != nil {
return handleTimestampAPIError(params, http.StatusBadRequest, err, errMsg)
}
policyID := req.TSAPolicyOID
if policyID.String() == "" {
policyID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 2}
}
duration, _ := time.ParseDuration("1s")
tsStruct := timestamp.Timestamp{
HashAlgorithm: req.HashAlgorithm,
HashedMessage: req.HashedMessage,
// The field here is going to be serialized as a GeneralizedTime, and RFC5280
// states that the GeneralizedTime values MUST be expressed in Greenwich Mean Time.
// However, go asn1/marshal will happily accept other formats. So we force it directly here.
// https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.5.2
Time: time.Now().UTC(),
Nonce: req.Nonce,
Policy: policyID,
Ordering: false,
Accuracy: duration,
// Not qualified for the european directive
Qualified: false,
AddTSACertificate: req.Certificates,
ExtraExtensions: req.Extensions,
}
if api.includeChain {
tsStruct.Certificates = api.certChain[1:] // Issuing CA certificate down to root
}
resp, err := tsStruct.CreateResponseWithOpts(api.certChain[0], api.tsaSigner, api.tsaSignerHash)
if err != nil {
return handleTimestampAPIError(params, http.StatusInternalServerError, err, failedToGenerateTimestampResponse)
}
if api.useHTTP201 {
return ts.NewGetTimestampResponseCreated().WithPayload(io.NopCloser(bytes.NewReader(resp)))
}
return ts.NewGetTimestampResponseOK().WithPayload(io.NopCloser(bytes.NewReader(resp)))
}
func GetTimestampCertChainHandler(_ ts.GetTimestampCertChainParams) middleware.Responder {
return ts.NewGetTimestampCertChainOK().WithPayload(api.certChainPem)
}
func verifyTimestampRequest(tsReq *timestamp.Request) (*timestamp.Request, string, error) {
if err := verification.VerifyRequest(tsReq); err != nil {
// verify that the request's hash algorithm is not weak
if errors.Is(err, verification.ErrWeakHashAlg) {
return nil, WeakHashAlgorithmTimestampRequest, err
}
// verify that the request's hash algorithm is supported
if errors.Is(err, verification.ErrUnsupportedHashAlg) {
return nil, failedToGenerateTimestampResponse, err
}
// verify that the request's digest length is consistent with the request's hash algorithm
if errors.Is(err, verification.ErrInconsistentDigestLength) {
return nil, InconsistentDigestLengthTimestampRequest, err
}
return nil, failedToGenerateTimestampResponse, err
}
return tsReq, "", nil
}