8000 verify: Add method that accepts unhashed message as arg (#144) · trailofbits/rfc3161-client@10dd28e · GitHub
[go: up one dir, main page]

Skip to content

Commit 10dd28e

Browse files
authored
verify: Add method that accepts unhashed message as arg (#144)
1 parent c331b83 commit 10dd28e

File tree

9 files changed

+110
-12
lines changed

9 files changed

+110
-12
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Added `HashAlgorithm` to exports of the base package module.
10+
- Added `HashAlgorithm` to exports of the base package module ([#143](https://github.com/trailofbits/rfc3161-client/pull/143))
11+
12+
- Added `verify_message` method to `Verifier` class ([#144](https://github.com/trailofbits/rfc3161-client/pull/144))
1113

1214
## Fixed
1315

README.md

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ message = b"Hello, World!"
5555
timestamp_request = (
5656
TimestampRequestBuilder().data(message).build()
5757
# Note: you could also add .hash_algorithm(XXX) to specify a specific hash algorithm
58-
# this means the algorithm check in the next section is not necessary
5958
)
6059

6160
# TSA servers must be RFC 3161 compliant (see https://github.com/trailofbits/rfc3161-client/issues/46
@@ -88,14 +87,6 @@ from cryptography import x509
8887
import hashlib
8988

9089

91-
# get the message hash (hash method depends on what the TSA used)
92-
message_hash = None
93-
hash_algorithm = timestamp_response.tst_info.message_imprint.hash_algorithm
94-
if hash_algorithm == x509.ObjectIdentifier(value="2.16.840.1.101.3.4.2.3"):
95-
message_hash = hashlib.sha512(message).digest()
96-
elif hash_algorithm == x509.ObjectIdentifier(value="2.16.840.1.101.3.4.2.1"):
97-
message_hash = hashlib.sha256(message).digest()
98-
9990
# get trusted root certs from certifi
10091
with open(certifi.where(), "rb") as f:
10192
cert_authorities = x509.load_pem_x509_certificates(f.read())
@@ -105,7 +96,7 @@ root_cert = None
10596
for certificate in cert_authorities:
10697
verifier = VerifierBuilder().add_root_certificate(certificate).build()
10798
try:
108-
verifier.verify(timestamp_response, message_hash)
99+
verifier.verify_message(timestamp_response, message)
109100
root_cert = certificate
110101
break
111102
except VerificationError:

src/rfc3161_client/verify.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import abc
6+
import hashlib
67
from copy import copy
78

89
import cryptography.x509
@@ -12,6 +13,11 @@
1213
from rfc3161_client.errors import VerificationError
1314
from rfc3161_client.tsp import PKIStatus, TimeStampRequest, TimeStampResponse
1415

16+
# See https://www.iana.org/assignments/hash-function-text-names/hash-function-text-names.xhtml
17+
SHA256_OID = "2.16.840.1.101.3.4.2.1"
18+
SHA384_OID = "2.16.840.1.101.3.4.2.2"
19+
SHA512_OID = "2.16.840.1.101.3.4.2.3"
20+
1521

1622
class VerifierBuilder:
1723
"""Builder for a Verifier."""
@@ -156,8 +162,29 @@ def __init__(
156162
self._nonce: int | None = nonce
157163
self._common_name: str | None = common_name
158164

165+
def verify_message(self, timestamp_response: TimeStampResponse, message: bytes) -> bool:
166+
"""Verify a Timestamp Response over a given message
167+
168+
Supports timestamp responses with SHA-256, SHA-384 or SHA-512 hash algorithms.
169+
"""
170+
171+
algo = timestamp_response.tst_info.message_imprint.hash_algorithm
172+
if algo == cryptography.x509.ObjectIdentifier(value=SHA256_OID):
173+
hashed_message = hashlib.sha256(message).digest()
174+
elif algo == cryptography.x509.ObjectIdentifier(value=SHA384_OID):
175+
hashed_message = hashlib.sha384(message).digest()
176+
elif algo == cryptography.x509.ObjectIdentifier(value=SHA512_OID):
177+
hashed_message = hashlib.sha512(message).digest()
178+
else:
179+
raise VerificationError(f"Unsupported hash algorithm {algo}")
180+
181+
return self.verify(timestamp_response, hashed_message)
182+
159183
def verify(self, timestamp_response: TimeStampResponse, hashed_message: bytes) -> bool:
160-
"""Verify a Timestamp Response.
184+
"""Verify a Timestamp Response over given message digest
185+
186+
Note that caller is responsible for hashing the message appropriately for the
187+
given timestamp response.
161188
162189
Inspired by:
163190
https://github.com/sigstore/timestamp-authority/blob/main/pkg/verification/verify.go#L209

test/fixtures/sigstage/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
The timestamp responses in this directory were generated
2+
* with CLI tool from https://github.com/sigstore/timestamp-authority/
3+
* using the timestamp.sigstage.dev TSA (The relevant certificates can be
4+
found in ./ts_chain.pem)
5+
6+
```bash
7+
echo -n "hello" > f
8+
for HASH in sha256 sha384 sha512; do
9+
./bin/timestamp-cli --timestamp_server https://timestamp.sigstage.dev/ timestamp --artifact f --hash $HASH --out response-$HASH.tsr
10+
done
11+
```
1.24 KB
Binary file not shown.
1.29 KB
Binary file not shown.
1.33 KB
Binary file not shown.

test/fixtures/sigstage/ts_chain.pem

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIICDzCCAZagAwIBAgIUCjWhBmHV4kFzxomWp/J98n4DfKcwCgYIKoZIzj0EAwMw
3+
OTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Et
4+
c2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMC4xFTAT
5+
BgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYH
6+
KoZIzj0CAQYFK4EEACIDYgAEx1v5F3HpD9egHuknpBFlRz7QBRDJu4aeVzt9zJLR
7+
Y0lvmx1lF7WBM2c9AN8ZGPQsmDqHlJN2R/7+RxLkvlLzkc19IOx38t7mGGEcB7ag
8+
UDdCF/Ky3RTLSK0Xo/0AgHQdo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYE
9+
FKj8ZPYo3i7mO3NPVIxSxOGc3VOlMB8GA1UdIwQYMBaAFDsgRlletTJNRzDObmPu
10+
c3RH8gR9MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2cAMGQC
11+
MESvVS6GGtF33+J19TfwENWJXjRv4i0/HQFwLUSkX6TfV7g0nG8VnqNHJLvEpAtO
12+
jQIwUD3uywTXorQP1DgbV09rF9Yen+CEqs/iEpieJWPst280SSOZ5Na+dyPVk9/8
13+
SFk6
14+
-----END CERTIFICATE-----
15+
-----BEGIN CERTIFICATE-----
16+
MIIB9zCCAXygAwIBAgIUCPExEFKiQh0dP4sp5ltmSYSSkFUwCgYIKoZIzj0EAwMw
17+
OTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Et
18+
c2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMDkxFTAT
19+
BgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZz
20+
aWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATt0tIDWyo4ARfL9BaSo0W5bJQE
21+
bKJTU/u7llvdjSI5aTkOAJa8tixn2+LEfPG4dMFdsMPtsIuU1qn2OqFiuMk6vHv/
22+
c+az25RQVY1oo50iMb0jIL3N4FgwhPFpZnCbQPOjRTBDMA4GA1UdDwEB/wQEAwIB
23+
BjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQ7IEZZXrUyTUcwzm5j7nN0
24+
R/IEfTAKBggqhkjOPQQDAwNpADBmAjEA2MI1VXgbf3dUOSc95hSRypBKOab18eh2
25+
xzQtxUsHvWeY+1iFgyMluUuNR6taoSmFAjEA31m2czguZhKYX+4JSKu5pRYhBTXA
26+
d8KKQ3xdPRX/qCaLvT2qJAEQ1YQM3EJRrtI7
27+
-----END CERTIFICATE-----

test/test_verify.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,19 @@
1818
_FIXTURE = _HERE / "fixtures"
1919

2020

21+
# test assets come from two different TSAs: provide two certificate methods
2122
@pytest.fixture
2223
def certificates() -> list[cryptography.x509.Certificate]:
2324
return cryptography.x509.load_pem_x509_certificates((_FIXTURE / "ts_chain.pem").read_bytes())
2425

2526

27+
@pytest.fixture
28+
def sigstage_certificates() -> list[cryptography.x509.Certificate]:
29+
return cryptography.x509.load_pem_x509_certificates(
30+
(_FIXTURE / "sigstage" / "ts_chain.pem").read_bytes()
31+
)
32+
33+
2634
@pytest.fixture
2735
def ts_request() -> TimeStampRequest:
2836
return parse_timestamp_request((_FIXTURE / "request.der").read_bytes())
@@ -33,6 +41,11 @@ def ts_response() -> TimeStampResponse:
3341
return decode_timestamp_response((_FIXTURE / "response.tsr").read_bytes())
3442

3543

44+
@pytest.fixture
45+
def ts_response_by_filename(request) -> TimeStampResponse:
46+
return decode_timestamp_response((_FIXTURE / "sigstage" / request.param).read_bytes())
47+
48+
3649
@pytest.fixture
3750
def verifier(
3851
ts_request: TimeStampRequest, certificates: list[cryptography.x509.Certificate]
@@ -304,6 +317,33 @@ def test_verify_succeeds(self, ts_response: TimeStampResponse, verifier: Verifie
304317
is True
305318
)
306319

320+
ts_response_files = ["response-sha256.tsr", "response-sha384.tsr", "response-sha512.tsr"]
321+
322+
@pytest.mark.parametrize("ts_response_by_filename", ts_response_files, indirect=True)
323+
def test_verify_message_with_algo(
324+
self, ts_response_by_filename: TimeStampResponse, sigstage_certificates
325+
) -> None:
326+
verifier = (
327+
VerifierBuilder()
328+
.add_root_certificate(sigstage_certificates[1])
329+
.tsa_certificate(sigstage_certificates[0])
330+
.build()
331+
)
332+
333+
assert verifier.verify_message(ts_response_by_filename, b"hello") is True
334+
335+
def test_verify_message_with_unsupported_algo(
336+
self, ts_response: TimeStampResponse, verifier: Verifier, monkeypatch: MonkeyPatch
337+
) -> None:
338+
# tweak OID so the timestamp response hash algorithm won't match it
339+
monkeypatch.setattr(rfc3161_client.verify, "SHA512_OID", rfc3161_client.verify.SHA384_OID)
340+
341+
with pytest.raises(VerificationError, match="Unsupported hash"):
342+
verifier.verify_message(
343+
timestamp_response=ts_response,
344+
message=b"hello",
345+
)
346+
307347

308348
def test_verify_succeeds_when_leaf_cert_is_not_first():
309349
"""This is a regression test for a bug where the leaf certificate was not

0 commit comments

Comments
 (0)
0