1# Copyright 2022 The Sigstore Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""
16Utilities to deal with sources of signed time.
17"""
18
19import enum
20from dataclasses import dataclass
21from datetime import datetime
22
23import requests
24from rfc3161_client import (
25 TimestampRequestBuilder,
26 TimeStampResponse,
27 decode_timestamp_response,
28)
29
30from sigstore._internal import USER_AGENT
31
32CLIENT_TIMEOUT: int = 5
33
34
35class TimestampSource(enum.Enum):
36 """Represents the source of a timestamp."""
37
38 TIMESTAMP_AUTHORITY = enum.auto()
39 TRANSPARENCY_SERVICE = enum.auto()
40
41
42@dataclass
43class TimestampVerificationResult:
44 """Represents a timestamp used by the Verifier.
45
46 A Timestamp either comes from a Timestamping Service (RFC3161) or the Transparency
47 Service.
48 """
49
50 source: TimestampSource
51 time: datetime
52
53
54class TimestampError(Exception):
55 """
56 A generic error in the TimestampAuthority client.
57 """
58
59 pass
60
61
62class TimestampAuthorityClient:
63 """Internal client to deal with a Timestamp Authority"""
64
65 def __init__(self, url: str) -> None:
66 """
67 Create a new `TimestampAuthorityClient` from the given URL.
68 """
69 self.url = url
70 self.session = requests.Session()
71 self.session.headers.update(
72 {
73 "Content-Type": "application/timestamp-query",
74 "User-Agent": USER_AGENT,
75 }
76 )
77
78 def __del__(self) -> None:
79 """
80 Terminates the underlying network session.
81 """
82 self.session.close()
83
84 def request_timestamp(self, signature: bytes) -> TimeStampResponse:
85 """
86 Timestamp the signature using the configured Timestamp Authority.
87
88 This method generates a RFC3161 Timestamp Request and sends it to a TSA.
89 The received response is parsed but *not* cryptographically verified.
90
91 Raises a TimestampError on failure.
92 """
93 # Build the timestamp request
94 try:
95 timestamp_request = (
96 TimestampRequestBuilder().data(signature).nonce(nonce=True).build()
97 )
98 except ValueError as error:
99 msg = f"invalid request: {error}"
100 raise TimestampError(msg)
101
102 # Send it to the TSA for signing
103 try:
104 response = self.session.post(
105 self.url,
106 data=timestamp_request.as_bytes(),
107 timeout=CLIENT_TIMEOUT,
108 )
109 response.raise_for_status()
110 except requests.RequestException as error:
111 msg = f"error while sending the request to the TSA: {error}"
112 raise TimestampError(msg)
113
114 # Check that we can parse the response but do not *verify* it
115 try:
116 timestamp_response = decode_timestamp_response(response.content)
117 except ValueError as e:
118 msg = f"invalid response: {e}"
119 raise TimestampError(msg)
120
121 return timestamp_response