// // 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/pkg/log" "github.com/sigstore/timestamp-authority/pkg/signer" tsx509 "github.com/sigstore/timestamp-authority/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 } 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); 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"), }, 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/pkg/generated/models" "github.com/sigstore/timestamp-authority/pkg/generated/restapi/operations/timestamp" "github.com/sigstore/timestamp-authority/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) { log.RequestIDLogger(r).Errorw("exiting with error", append([]interface{}{"handler", handler, "statusCode", code, "clientMessage", message, "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/pkg/generated/restapi/operations/timestamp" "github.com/sigstore/timestamp-authority/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) } return ts.NewGetTimestampResponseCreated().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 }