autograph-utils 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autograph_utils/__init__.py +510 -0
- autograph_utils/__main__.py +6 -0
- autograph_utils/main.py +13 -0
- autograph_utils-0.2.0.dist-info/LICENSE +15 -0
- autograph_utils-0.2.0.dist-info/METADATA +82 -0
- autograph_utils-0.2.0.dist-info/RECORD +9 -0
- autograph_utils-0.2.0.dist-info/WHEEL +5 -0
- autograph_utils-0.2.0.dist-info/entry_points.txt +2 -0
- autograph_utils-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""Top-level package for Python Autograph Utilities."""
|
|
4
|
+
|
|
5
|
+
__author__ = """Ethan Glasser-Camp"""
|
|
6
|
+
__email__ = "eglassercamp@mozilla.com"
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import binascii
|
|
12
|
+
import re
|
|
13
|
+
from abc import ABC
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
import cryptography
|
|
17
|
+
import ecdsa.util
|
|
18
|
+
from cryptography import x509
|
|
19
|
+
from cryptography.hazmat.backends import default_backend
|
|
20
|
+
from cryptography.hazmat.primitives.asymmetric import ec as cryptography_ec
|
|
21
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
22
|
+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
|
23
|
+
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
|
|
24
|
+
from cryptography.hazmat.primitives.hashes import SHA256, SHA384
|
|
25
|
+
from cryptography.x509.oid import NameOID
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Cache(ABC):
|
|
29
|
+
"""An interface for caching x5u validity checks."""
|
|
30
|
+
|
|
31
|
+
def get(self, url):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
def set(self, url, result):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MemoryCache:
|
|
39
|
+
"""A simple Cache implementation that just uses a dictionary.
|
|
40
|
+
|
|
41
|
+
This cache does not expire data and can therefore grow without
|
|
42
|
+
bound. This may make it vulnerable to a denial-of-service attack.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
self.data = {}
|
|
48
|
+
|
|
49
|
+
def get(self, url):
|
|
50
|
+
return self.data.get(url)
|
|
51
|
+
|
|
52
|
+
def set(self, url, result):
|
|
53
|
+
self.data[url] = result
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
Cache.register(MemoryCache)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SubjectNameCheck(ABC):
|
|
60
|
+
"""An interface for predicates that verify the subject name."""
|
|
61
|
+
|
|
62
|
+
def check(self, subject_name):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
def describe(self):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EndsWith:
|
|
70
|
+
def __init__(self, domain):
|
|
71
|
+
self.domain = domain
|
|
72
|
+
|
|
73
|
+
def check(self, subject_name):
|
|
74
|
+
return subject_name.endswith(self.domain)
|
|
75
|
+
|
|
76
|
+
def describe(self):
|
|
77
|
+
return f"ends with {self.domain!r}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ExactMatch:
|
|
81
|
+
def __init__(self, domain):
|
|
82
|
+
self.domain = domain
|
|
83
|
+
|
|
84
|
+
def check(self, subject_name):
|
|
85
|
+
return subject_name == self.domain
|
|
86
|
+
|
|
87
|
+
def describe(self):
|
|
88
|
+
return f"matches exactly {self.domain!r}"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
BASE64_WRONG_LENGTH_RE = re.compile(
|
|
92
|
+
r"Invalid base64-encoded string: number of data characters \(\d+\) cannot "
|
|
93
|
+
r"be [123] more than a multiple of 4"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class BadCertificate(Exception):
|
|
98
|
+
def __init__(self, extra):
|
|
99
|
+
self.extra = extra
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def detail(self):
|
|
103
|
+
return f"Bad certificate: {self.extra}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class CertificateParseError(BadCertificate):
|
|
107
|
+
def __init__(self, extra):
|
|
108
|
+
self.extra = extra
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def detail(self):
|
|
112
|
+
return f"Could not parse certificate: {self.extra}"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class CertificateNotYetValid(BadCertificate):
|
|
116
|
+
def __init__(self, not_before):
|
|
117
|
+
self.not_before = not_before
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def detail(self):
|
|
121
|
+
return f"Certificate is not valid until {self.not_before}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class CertificateExpired(BadCertificate):
|
|
125
|
+
def __init__(self, not_after):
|
|
126
|
+
self.not_after = not_after
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def detail(self):
|
|
130
|
+
return f"Certificate expired on {self.not_after}"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class CertificateHasWrongRoot(BadCertificate):
|
|
134
|
+
def __init__(self, *, expected, actual):
|
|
135
|
+
self.expected = binascii.hexlify(expected).decode()
|
|
136
|
+
self.actual = binascii.hexlify(actual).decode()
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def detail(self):
|
|
140
|
+
return (
|
|
141
|
+
"Certificate is not based on expected root hash. "
|
|
142
|
+
f"Got {self.actual!r} expected {self.expected!r}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class CertificateHasWrongSubject(BadCertificate):
|
|
147
|
+
def __init__(self, actual, check_description):
|
|
148
|
+
self.check_description = check_description
|
|
149
|
+
self.actual = actual
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def detail(self):
|
|
153
|
+
return (
|
|
154
|
+
f"Certificate does not have the expected subject. "
|
|
155
|
+
f"Got {self.actual!r}, checking for {self.check_description}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class CertificateChainBroken(BadCertificate):
|
|
160
|
+
def __init__(self, previous_cert, next_cert):
|
|
161
|
+
self.previous_cert = previous_cert
|
|
162
|
+
self.next_cert = next_cert
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def detail(self):
|
|
166
|
+
return (
|
|
167
|
+
"Certificate chain is not continuous. "
|
|
168
|
+
f"Expected {self.previous_cert!r} to sign {self.next_cert!r}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class CertificateUnsupportedKeyType(BadCertificate):
|
|
173
|
+
"""An internal error indicating that support for some type of key is missing."""
|
|
174
|
+
|
|
175
|
+
def __init__(self, cert, key):
|
|
176
|
+
self.cert = cert
|
|
177
|
+
self.key = key
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def detail(self):
|
|
181
|
+
return f"Unknown public key type for {self.cert!r}: {self.key!r}"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class CertificateChainNameNotPermitted(BadCertificate):
|
|
185
|
+
def __init__(self, permitted_subtrees, current, next):
|
|
186
|
+
self.permitted_subtrees = permitted_subtrees
|
|
187
|
+
self.current = current
|
|
188
|
+
self.next = next
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def detail(self):
|
|
192
|
+
return (
|
|
193
|
+
f"Certificate name of {self.next!r} does not match the permitted names "
|
|
194
|
+
f"for {self.current!r}: {self.permitted_subtrees!r}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class CertificateCannotSign(BadCertificate):
|
|
199
|
+
"""For intermediate/root certificates that do not have the proper
|
|
200
|
+
metadata bits saying that they can be used to sign signatures.
|
|
201
|
+
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def __init__(self, cert, extra):
|
|
205
|
+
self.cert = cert
|
|
206
|
+
self.extra = extra
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def detail(self):
|
|
210
|
+
return "Certificate cannot be used for signing " f"because {self.extra}: {self.cert!r}"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class CertificateLeafHasWrongKeyUsage(BadCertificate):
|
|
214
|
+
def __init__(self, cert, key_usage):
|
|
215
|
+
self.cert = cert
|
|
216
|
+
self.key_usage = key_usage
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def detail(self):
|
|
220
|
+
return (
|
|
221
|
+
f"Leaf certificate {self.cert!r} should have extended key usage of just "
|
|
222
|
+
f"Code Signing. Got {self.key_usage!r}"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class CertificateChainNameExcluded(BadCertificate):
|
|
227
|
+
def __init__(self, excluded_subtrees, current, next):
|
|
228
|
+
self.excluded_subtrees = excluded_subtrees
|
|
229
|
+
self.current = current
|
|
230
|
+
self.next = next
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def detail(self):
|
|
234
|
+
return (
|
|
235
|
+
f"Certificate name of {self.next!r} matches the excluded names "
|
|
236
|
+
f"for {self.current!r}: {self.excluded_subtrees!r}"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class BadSignature(Exception):
|
|
241
|
+
detail = "Unknown signature problem"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class SignatureDoesNotMatch(BadSignature):
|
|
245
|
+
detail = "Signature does not correspond to this data"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class WrongSignatureSize(BadSignature):
|
|
249
|
+
detail = "Signature is not the right number of bytes"
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class SignatureVerifier:
|
|
253
|
+
"""A utility to verify the provenance of data.
|
|
254
|
+
|
|
255
|
+
This class lets you verify that data is signed by a collection of
|
|
256
|
+
certificates that chains back up to some well-known root
|
|
257
|
+
hash. Certificate chains are identified by x5u. x5us are assumed
|
|
258
|
+
to be static and so can be cached to save network traffic.
|
|
259
|
+
|
|
260
|
+
:params ClientSession session: An aiohttp session, used to retrieve x5us.
|
|
261
|
+
:params Cache cache: A cache used to store results for x5u verification.
|
|
262
|
+
:params bytes root_hash: The expected hash for the first
|
|
263
|
+
certificate in a chain. This should not be encoded in any
|
|
264
|
+
way. Hashes can be decoded using decode_mozilla_hash.
|
|
265
|
+
:params SubjectNameCheck subject_name_check: Predicate to use to
|
|
266
|
+
validate cert subject names. Defaults to
|
|
267
|
+
EndsWith(".content-signature.mozilla.org").
|
|
268
|
+
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def __init__(self, session, cache, root_hash, subject_name_check=None):
|
|
272
|
+
self.session = session
|
|
273
|
+
self.cache = cache
|
|
274
|
+
self.root_hash = root_hash
|
|
275
|
+
self.subject_name_check = subject_name_check or EndsWith(".content-signature.mozilla.org")
|
|
276
|
+
|
|
277
|
+
algorithm = cryptography_ec.ECDSA(SHA384())
|
|
278
|
+
|
|
279
|
+
async def verify(self, data, signature, x5u):
|
|
280
|
+
"""Verify that the data is signed by certs that chain up to the root hash.
|
|
281
|
+
|
|
282
|
+
Returns True if the signature checks out and raises an
|
|
283
|
+
exception otherwise.
|
|
284
|
+
|
|
285
|
+
:param bytes data: Data that was signed.
|
|
286
|
+
:param signature: Signature in Autograph format (described in
|
|
287
|
+
Autograph docs as "DL/ECSSA representation of the R and S
|
|
288
|
+
values (IEEE Std 1363-2000)"). This can be bytes or str.
|
|
289
|
+
:param str x5u: URL of a certificate chain which was allegedly
|
|
290
|
+
used to sign the data.
|
|
291
|
+
:raises: BadCertificate if the certificate is bad;
|
|
292
|
+
BadSignature if signature verification fails
|
|
293
|
+
|
|
294
|
+
"""
|
|
295
|
+
cert = await self.verify_x5u(x5u)
|
|
296
|
+
# Decode signature into the (r, s) components
|
|
297
|
+
try:
|
|
298
|
+
signature = base64.urlsafe_b64decode(signature)
|
|
299
|
+
except binascii.Error as e:
|
|
300
|
+
if BASE64_WRONG_LENGTH_RE.match(e.args[0]):
|
|
301
|
+
raise WrongSignatureSize("Base64 encoded signature was not a multiple of 4")
|
|
302
|
+
raise
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
r, s = ecdsa.util.sigdecode_string(signature, order=ecdsa.curves.NIST384p.order)
|
|
306
|
+
except ecdsa.util.MalformedSignature:
|
|
307
|
+
raise WrongSignatureSize()
|
|
308
|
+
|
|
309
|
+
# Encode as DER for cryptography
|
|
310
|
+
signature = encode_dss_signature(r, s)
|
|
311
|
+
|
|
312
|
+
# Content signature implicitly adds a prefix to signed data
|
|
313
|
+
data = b"Content-Signature:\x00" + data
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
cert.public_key().verify(signature, data, self.algorithm)
|
|
317
|
+
except cryptography.exceptions.InvalidSignature:
|
|
318
|
+
raise SignatureDoesNotMatch()
|
|
319
|
+
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
async def verify_x5u(self, url):
|
|
323
|
+
cached = self.cache.get(url)
|
|
324
|
+
if cached:
|
|
325
|
+
return cached
|
|
326
|
+
|
|
327
|
+
async with self.session.get(url) as response:
|
|
328
|
+
response.raise_for_status()
|
|
329
|
+
content = await response.read()
|
|
330
|
+
pems = split_pem(content)
|
|
331
|
+
certs = [x509.load_pem_x509_certificate(pem, backend=default_backend()) for pem in pems]
|
|
332
|
+
|
|
333
|
+
now = _now()
|
|
334
|
+
for cert in certs:
|
|
335
|
+
if cert.not_valid_before > cert.not_valid_after:
|
|
336
|
+
raise BadCertificate(
|
|
337
|
+
f"not_before ({cert.not_valid_before}) after "
|
|
338
|
+
f"not_after ({cert.not_valid_after})"
|
|
339
|
+
)
|
|
340
|
+
if now < cert.not_valid_before:
|
|
341
|
+
raise CertificateNotYetValid(cert.not_valid_before)
|
|
342
|
+
if now > cert.not_valid_after:
|
|
343
|
+
raise CertificateExpired(cert.not_valid_after)
|
|
344
|
+
|
|
345
|
+
# Verify chain of trust.
|
|
346
|
+
chain = certs[::-1]
|
|
347
|
+
root_hash = chain[0].fingerprint(SHA256())
|
|
348
|
+
if root_hash != self.root_hash:
|
|
349
|
+
raise CertificateHasWrongRoot(expected=self.root_hash, actual=root_hash)
|
|
350
|
+
|
|
351
|
+
current_cert = chain[0]
|
|
352
|
+
for next_cert in chain[1:]:
|
|
353
|
+
self._check_can_sign_other_certs(current_cert)
|
|
354
|
+
self._verify_cert_link(current_cert, next_cert)
|
|
355
|
+
self._check_name_constraints(current_cert, next_cert)
|
|
356
|
+
|
|
357
|
+
current_cert = next_cert
|
|
358
|
+
|
|
359
|
+
leaf_subject_name = certs[0].subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
|
|
360
|
+
if not self.subject_name_check.check(leaf_subject_name):
|
|
361
|
+
raise CertificateHasWrongSubject(
|
|
362
|
+
leaf_subject_name, check_description=self.subject_name_check.describe()
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
code_signing = cryptography.x509.oid.ExtendedKeyUsageOID.CODE_SIGNING
|
|
366
|
+
extended_key_usage = (
|
|
367
|
+
certs[0].extensions.get_extension_for_class(cryptography.x509.ExtendedKeyUsage).value
|
|
368
|
+
)
|
|
369
|
+
if list(extended_key_usage) != [code_signing]:
|
|
370
|
+
raise CertificateLeafHasWrongKeyUsage(certs[0], extended_key_usage)
|
|
371
|
+
|
|
372
|
+
res = certs[0]
|
|
373
|
+
self.cache.set(url, res)
|
|
374
|
+
return res
|
|
375
|
+
|
|
376
|
+
def _verify_cert_link(self, current_cert, next_cert):
|
|
377
|
+
"""Verify a single link in a cert chain."""
|
|
378
|
+
key = current_cert.public_key()
|
|
379
|
+
if isinstance(key, RSAPublicKey):
|
|
380
|
+
try:
|
|
381
|
+
key.verify(
|
|
382
|
+
next_cert.signature,
|
|
383
|
+
next_cert.tbs_certificate_bytes,
|
|
384
|
+
padding.PKCS1v15(),
|
|
385
|
+
next_cert.signature_hash_algorithm,
|
|
386
|
+
)
|
|
387
|
+
except cryptography.exceptions.InvalidSignature:
|
|
388
|
+
raise CertificateChainBroken(current_cert, next_cert)
|
|
389
|
+
elif isinstance(key, cryptography_ec.EllipticCurvePublicKey):
|
|
390
|
+
try:
|
|
391
|
+
key.verify(
|
|
392
|
+
next_cert.signature,
|
|
393
|
+
next_cert.tbs_certificate_bytes,
|
|
394
|
+
cryptography_ec.ECDSA(next_cert.signature_hash_algorithm),
|
|
395
|
+
)
|
|
396
|
+
except cryptography.exceptions.InvalidSignature:
|
|
397
|
+
raise CertificateChainBroken(current_cert, next_cert)
|
|
398
|
+
else:
|
|
399
|
+
raise CertificateUnsupportedKeyType(current_cert, key)
|
|
400
|
+
|
|
401
|
+
def _check_name_constraints(self, current_cert, next_cert):
|
|
402
|
+
try:
|
|
403
|
+
nc = current_cert.extensions.get_extension_for_class(
|
|
404
|
+
cryptography.x509.NameConstraints
|
|
405
|
+
).value
|
|
406
|
+
except x509.ExtensionNotFound:
|
|
407
|
+
# No name constraints. This cert is therefore OK to sign
|
|
408
|
+
# any name whatsoever.
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
name = next_cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
|
|
412
|
+
if nc.permitted_subtrees:
|
|
413
|
+
for constraint in nc.permitted_subtrees:
|
|
414
|
+
if _name_constraint_matches(name, constraint):
|
|
415
|
+
break
|
|
416
|
+
else:
|
|
417
|
+
raise CertificateChainNameNotPermitted(
|
|
418
|
+
nc.permitted_subtrees, current=current_cert, next=next_cert
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
excluded_subtrees = nc.excluded_subtrees or []
|
|
422
|
+
|
|
423
|
+
for constraint in excluded_subtrees:
|
|
424
|
+
if _name_constraint_matches(name, constraint):
|
|
425
|
+
raise CertificateChainNameExcluded(
|
|
426
|
+
nc.excluded_subtrees, current=current_cert, next=next_cert
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def _check_can_sign_other_certs(self, cert):
|
|
430
|
+
basic = cert.extensions.get_extension_for_class(cryptography.x509.BasicConstraints).value
|
|
431
|
+
if not basic.ca:
|
|
432
|
+
raise CertificateCannotSign(cert, "ca is false")
|
|
433
|
+
|
|
434
|
+
usage = cert.extensions.get_extension_for_class(cryptography.x509.KeyUsage).value
|
|
435
|
+
usage_is_ok = usage.key_cert_sign and usage.crl_sign
|
|
436
|
+
if not usage_is_ok:
|
|
437
|
+
raise CertificateCannotSign(cert, "key usage is incomplete")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _name_constraint_matches(hostname, name_constraint):
|
|
441
|
+
"""Check if a name matches a constraint.
|
|
442
|
+
|
|
443
|
+
Taken from
|
|
444
|
+
https://github.com/alex/x509-validator/blob/master/validator.py.
|
|
445
|
+
|
|
446
|
+
"""
|
|
447
|
+
if not isinstance(name_constraint, x509.DNSName):
|
|
448
|
+
return False
|
|
449
|
+
constraint_hostname = name_constraint.value
|
|
450
|
+
|
|
451
|
+
if constraint_hostname.startswith("."):
|
|
452
|
+
return hostname.endswith(constraint_hostname)
|
|
453
|
+
else:
|
|
454
|
+
return hostname == constraint_hostname or hostname.endswith("." + constraint_hostname)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def split_pem(s):
|
|
458
|
+
"""Split a string containing many ASCII-armored PEM structures.
|
|
459
|
+
|
|
460
|
+
No validation is performed on the PEM structures (even to the
|
|
461
|
+
point of verifying that the BEGIN lines match the END lines).
|
|
462
|
+
|
|
463
|
+
:param bytes s: bytes containing a list of PEM-encoded things
|
|
464
|
+
:returns: List of bytes, each representing a single PEM-encoded thing
|
|
465
|
+
"""
|
|
466
|
+
out = []
|
|
467
|
+
acc = []
|
|
468
|
+
state = "PRE"
|
|
469
|
+
for line in s.split(b"\n"):
|
|
470
|
+
if state == "PRE" and line.startswith(b"-----BEGIN "):
|
|
471
|
+
acc.append(line)
|
|
472
|
+
state = "BODY_OR_META"
|
|
473
|
+
elif state == "PRE" and not line:
|
|
474
|
+
pass
|
|
475
|
+
elif state == "BODY_OR_META" and b":" in line:
|
|
476
|
+
state = "META"
|
|
477
|
+
elif state == "BODY" and line.startswith(b"-----END "):
|
|
478
|
+
acc.append(line)
|
|
479
|
+
out.append(b"\n".join(acc))
|
|
480
|
+
acc = []
|
|
481
|
+
state = "PRE"
|
|
482
|
+
elif state == "META" and not line:
|
|
483
|
+
state = "BODY"
|
|
484
|
+
elif state == "BODY" or state == "BODY_OR_META":
|
|
485
|
+
acc.append(line)
|
|
486
|
+
state = "BODY"
|
|
487
|
+
else:
|
|
488
|
+
raise CertificateParseError(f'Unexpected input "{line}" in state "{state}"')
|
|
489
|
+
|
|
490
|
+
if acc:
|
|
491
|
+
raise CertificateParseError(f"Unexpected end of input. Leftover: {acc}")
|
|
492
|
+
|
|
493
|
+
return out
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def decode_mozilla_hash(s):
|
|
497
|
+
"""Convert a hash from pseudo-base16 colon-separated format.
|
|
498
|
+
|
|
499
|
+
>>> decode_mozilla_hash('4C:35:B1:C3')
|
|
500
|
+
b'\x4c\x35\xb1\xc3')
|
|
501
|
+
"""
|
|
502
|
+
return bytes.fromhex(s.replace(":", " "))
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _now():
|
|
506
|
+
"""Mockable function to get "now".
|
|
507
|
+
|
|
508
|
+
:returns: naive datetime representing a UTC timestamp
|
|
509
|
+
"""
|
|
510
|
+
return datetime.utcnow()
|
autograph_utils/main.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""Console script for autograph_utils."""
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.command()
|
|
9
|
+
def main(args=None):
|
|
10
|
+
"""Console script for autograph_utils."""
|
|
11
|
+
click.echo("Replace this message by putting your code into autograph_utils.cli.main")
|
|
12
|
+
click.echo("See click documentation at https://click.palletsprojects.com/")
|
|
13
|
+
return 0
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Apache Software License 2.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019, Ethan Glasser-Camp
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: autograph-utils
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: autograph-utils is a tool to configure a kinto server from an YAML file.
|
|
5
|
+
Author-email: Mozilla Services <dev-webdev@lists.mozilla.org>
|
|
6
|
+
License: Apache Software License 2.0
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2019, Ethan Glasser-Camp
|
|
9
|
+
|
|
10
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
11
|
+
you may not use this file except in compliance with the License.
|
|
12
|
+
You may obtain a copy of the License at
|
|
13
|
+
|
|
14
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
15
|
+
|
|
16
|
+
Unless required by applicable law or agreed to in writing, software
|
|
17
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
18
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
19
|
+
See the License for the specific language governing permissions and
|
|
20
|
+
limitations under the License.
|
|
21
|
+
|
|
22
|
+
Project-URL: Repository, https://github.com/mozilla-services/autograph-utils
|
|
23
|
+
Keywords: autograph
|
|
24
|
+
Classifier: Programming Language :: Python
|
|
25
|
+
Classifier: Programming Language :: Python :: 3
|
|
26
|
+
Classifier: Intended Audience :: Developers
|
|
27
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
28
|
+
Classifier: Natural Language :: English
|
|
29
|
+
Description-Content-Type: text/x-rst
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: Click >=7.0
|
|
32
|
+
Requires-Dist: aiohttp >=3.6
|
|
33
|
+
Requires-Dist: cryptography >=2.8
|
|
34
|
+
Requires-Dist: ecdsa >=0.13
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: ruff ; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest ; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-cache ; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest-aiohttp ; extra == 'dev'
|
|
40
|
+
Requires-Dist: pytest-cov ; extra == 'dev'
|
|
41
|
+
Requires-Dist: aioresponses ; extra == 'dev'
|
|
42
|
+
|
|
43
|
+
==========================
|
|
44
|
+
Python Autograph Utilities
|
|
45
|
+
==========================
|
|
46
|
+
|
|
47
|
+
|pypi| |ci| |coverage|
|
|
48
|
+
|
|
49
|
+
.. |pypi| image:: https://img.shields.io/pypi/v/autograph-utils.svg
|
|
50
|
+
:target: https://pypi.python.org/pypi/autograph-utils
|
|
51
|
+
.. |ci| image:: https://img.shields.io/github/actions/workflow/status/mozilla-services/python-autograph-utils/test.yml?branch=main
|
|
52
|
+
:target: https://github.com/mozilla-services/python-autograph-utils/actions
|
|
53
|
+
.. |coverage| image:: https://coveralls.io/repos/github/mozilla-services/python-autograph-utils/badge.svg?branch=main
|
|
54
|
+
:target: https://coveralls.io/github/mozilla-services/python-autograph-utils?branch=main
|
|
55
|
+
|
|
56
|
+
A library to simplify use of Autograph
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
* Free software: Apache Software License 2.0
|
|
60
|
+
* Documentation: https://python-autograph-utils.readthedocs.io.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
Features
|
|
64
|
+
--------
|
|
65
|
+
|
|
66
|
+
SignatureVerifier
|
|
67
|
+
=================
|
|
68
|
+
|
|
69
|
+
The canonical implementation of certificate chain validation. Although
|
|
70
|
+
some other implementations seem to exist (such as
|
|
71
|
+
https://github.com/river2sea/X509Validation,
|
|
72
|
+
https://github.com/alex/x509-validator, and
|
|
73
|
+
https://github.com/openstack/cursive), all are marked as
|
|
74
|
+
pre-production and/or needing work, so just do it ourselves.
|
|
75
|
+
|
|
76
|
+
Credits
|
|
77
|
+
-------
|
|
78
|
+
|
|
79
|
+
This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template.
|
|
80
|
+
|
|
81
|
+
.. _Cookiecutter: https://github.com/audreyr/cookiecutter
|
|
82
|
+
.. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
autograph_utils/__init__.py,sha256=luFKvmlgXRjkacIg7GMml6VPmMxbbvlRzXbHIlexeyw,16361
|
|
2
|
+
autograph_utils/__main__.py,sha256=8iIKqBCCSGGQeCqXT1ICe27lfjzwu9SOIvQPH6CHqy8,69
|
|
3
|
+
autograph_utils/main.py,sha256=MZ8ZlbKsXlk4CNYXutDTHyMcDwFcPze_Pc4tt3ZzM4E,350
|
|
4
|
+
autograph_utils-0.2.0.dist-info/LICENSE,sha256=LNVmJ5dLg64uFqoRBlph_037cXGhWkW9SjQqNTHZROQ,589
|
|
5
|
+
autograph_utils-0.2.0.dist-info/METADATA,sha256=XVIkDLV0Rd4GLgf7_vJV2-pXg8apXzUMN6vA1gW8G5s,3172
|
|
6
|
+
autograph_utils-0.2.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
7
|
+
autograph_utils-0.2.0.dist-info/entry_points.txt,sha256=FW66_7S93DJRjhc8fDtXL7yfsulOVLDZtkEaPOxqPOs,61
|
|
8
|
+
autograph_utils-0.2.0.dist-info/top_level.txt,sha256=_ZMBWDkQc1fqG36jSbXjaSW2tOUwRITIWHbsmdNCSLY,16
|
|
9
|
+
autograph_utils-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
autograph_utils
|