-
Notifications
You must be signed in to change notification settings - Fork 13
/
authentication.py
566 lines (455 loc) · 23.5 KB
/
authentication.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
# -*- coding: utf-8 -*-
""" conda_content_trust.authentication
This module contains functions that verify signatures and thereby authenticate
data.
Function Manifest for this Module
verify_signature
verify_gpg_signature
verify_signable
verify_root
verify_delegation
"""
# Python2 Compatibility
from __future__ import absolute_import, division, print_function, unicode_literals
# Standard libraries
import binascii # for Python2/3-compatible hex string <- -> bytes conversion
import struct # for struct.pack
# Dependency-provided libraries
from six import string_types # for Python2/3-compatible string type checks
import cryptography.exceptions
import cryptography.hazmat.primitives.asymmetric.ed25519 as ed25519
#import cryptography.hazmat.primitives.serialization as serialization
#import cryptography.hazmat.primitives.hashes
#import cryptography.hazmat.backends
# car modules
from .common import (
#SUPPORTED_SERIALIZABLE_TYPES,
canonserialize,
PublicKey,
is_a_signable,
checkformat_signable,
# is_hex_string,
is_hex_signature,
is_hex_key,
is_signature,
is_gpg_signature,
is_a_signature,
checkformat_gpg_signature,
checkformat_hex_key,
checkformat_byteslike,
# checkformat_natural_int, checkformat_expiration_distance,
# checkformat_list_of_hex_keys,
# checkformat_utc_isoformat,
checkformat_delegation,
checkformat_delegating_metadata,
SignatureError,
UnknownRoleError,
MetadataVerificationError # TODO: ✅ Use this more.
)
# TODO✅: Consider reversing this argument order? What's more intuitive?
def verify_root(trusted_current_root_metadata, untrusted_new_root_metadata):
"""
Given currently trusted root metadata, verify that new root metadata is
trustworthy per the currently trusted root metadata.
This requires a root chaining process as specified in The Update Framework
specification. (Version N must be used in order to verify version N+1.
Versions cannot be skipped.)
# TODO✅: Proper docstring.
"""
# TODO✅💣❌⚠️: Vet against root chaining algorithm we updated in TUF,
# and add the attack tests to tests/test_authentication.py.
# TODO✅: More argument validation
checkformat_delegating_metadata(trusted_current_root_metadata)
checkformat_delegating_metadata(untrusted_new_root_metadata)
if (trusted_current_root_metadata['signed']['type'] != 'root'
or untrusted_new_root_metadata['signed']['type'] != 'root'):
raise ValueError(
'Expected two instances of root metadata. Listed metadata '
'type in one or both pieces of metadata provided is not '
'"root".')
# Extract rules for root from old, trusted version of root.
root_expectations = (
trusted_current_root_metadata['signed']['delegations']['root'])
expected_threshold = root_expectations['threshold']
authorized_pub_keys = root_expectations['pubkeys']
# Also extract new rules for root per new untrusted version of root.
# NOTE THAT it is important that a new root version be verified BOTH
# based on the prior, trusted version of root, and also based on ITSELF
# (the latter in order to reduce the odds of accidentally breaking the root
# trust chain).
new_root_expectations = (
untrusted_new_root_metadata['signed']['delegations']['root'])
new_expected_threshold = new_root_expectations['threshold']
new_authorized_pub_keys = new_root_expectations['pubkeys']
trusted_root_version = trusted_current_root_metadata['signed']['version']
untrusted_root_version = untrusted_new_root_metadata['signed']['version']
if trusted_root_version + 1 != untrusted_root_version:
# TODO ✅: Create a suitable error class for this.
raise MetadataVerificationError(
'Root chaining failure: we currently trust a version of root '
'that marks itself as version ' + str(trusted_root_version) +
', and the provided new root metadata to verify marks itself '
'as version ' + str(untrusted_root_version) + '; the new '
'version must be 1 more than the old version: root updates '
'MUST be processed one at a time for security reasons: no '
'root version may be skipped.')
# Verify the new root metadata based on the prior, trusted root version.
verify_signable(
untrusted_new_root_metadata,
authorized_pub_keys,
expected_threshold,
gpg=True)
# Make sure that the signatures on the new root metadata would be
# sufficient to verify it using the new root metadata's own rules as well.
# Doing this helps avoid breaking the chain of trust.
verify_signable(
untrusted_new_root_metadata,
new_authorized_pub_keys,
new_expected_threshold,
gpg=True)
# TODO ✅: Consider verify_untrusted_based_on_trusted(), a function that just
# takes the two roles and does the digging to fetch authorized keys
# and threshold for you, along with an argument specifying what role
# we're trying to verify (redundant, perhaps, but important to make
# explicit, to avoid bad patterns of trusting attackers in the calling
# code).
# The function should:
# - return if verification succeeds
# - raise UnknownRoleError if there's no matching delegation
# - raise a verification failure error if a signature is bad or
# signatures don't match expectations (threshold, wrong keys,
# etc.)
# - of course, raise ValueError if the arguments are invalid
#
# TODO ✅: Autodetect signature type rather than expecting an argument,
# and allow both OpenPGP-facilitated ed25519 and raw ed25519
# signatures for any authority metadata (root, key_mgr, etc.)
# TODO ✅: Find way to specifically discourage anti-pattern of calling
# verify_delegation() directly to verify root metadata (instead of
# calling verify_root). We could add a _helper function and have
# both verify_delegation and verify_root call that, and each check
# to make sure the metadata type provided is/isn't root as
# appropriate, but I'd like to avoid adding another level of
# functions if possible.
# TODO ✅: Remove delegation_name and just take it from
# untrusted_delegating_metadata['signed']['type']. Consider utility
# of keeping argument, though... (allow enforcement, if you have
# reason to constrain the verifications? unlikely to be a useful arg)
def verify_delegation(
delegation_name,
untrusted_delegated_metadata, trusted_delegating_metadata,
gpg=False):
"""
Verify that the given untrusted, delegated-to metadata is trustworthy,
based on the given trusted metadata's expectations (expected keys and
threshold). This function returns if verification succeeds.
In other words, check trusted_delegating_metadata's delegation
to delegation_name to find the expected signing keys and threshold for
delegation_name, and then check untrusted_delegated_metadata to see if it
is signed by enough of the right keys to be trustworthy.
For example, using root metadata to verify key_mgr metadata looks like
this:
verify_delegation(
'key_mgr', <full root metadata>, <full key_mgr metadata>)
Arguments:
(string) delegation_name is the name of the role delegated.
(dict) trusted_delegating_metadata is a signable JSON-serializable
object representing the full metadata that delegates to role
delegation_name.
(dict) untrusted_delegated_metadata is a signable JSON-serializable
object
(bool) gpg should be true if the signatures to be verified in the
delegated metadata are expected to be OpenPGP signatures
rather than the usual raw ed25519 signatures.
Exceptions:
- raises UnknownRoleError if there's no matching delegation
- raises SignatureError if a signature is bad or signatures
don't match expectations (threshold, wrong keys, etc.)
# TODO: Consider exception handling to raise MetadataVerificationError instead?
- raises MetadataVerificationError if the metadata type is unexpected
- raises TypeError or ValueError if the arguments are invalid
"""
# Argument validation
if not isinstance(delegation_name, string_types):
raise TypeError(
'delegation_name must be a string, not a ' +
str(type(delegation_name)))
if gpg not in [True, False]:
raise TypeError('Argument "gpg" must be a boolean.') # should probably be ValueError
checkformat_delegating_metadata(trusted_delegating_metadata)
# Note that we don't really know the structure of the metadata we're
# verifying beyond that we expect it to be a signed envelope.
# We can't assume, for example, that it is itself delegating metadata also,
# (so no checkformat_delegating_metadata on it): while it could be
# that we're verifying key_mgr using root, it could also be that we're
# verifying some package metadata (which is not delegating metadata) using
# key_mgr.
# If, however, the untrusted_delegated_metadata *is* delegating metadata,
# we want to make sure that its type matches what the caller passed in as
# delegation_name.
checkformat_signable(untrusted_delegated_metadata)
try:
checkformat_delegating_metadata(untrusted_delegated_metadata)
except:
# If we can't verify that we're verifying more delegating metadata
# (e.g. we're using root to verify key_mgr), then we don't need to
# perform the type check, as it can just be any signed content we're
# verifying.
pass
else:
# If this is indeed more delegating metadata, make sure the type
# the caller expects matches what the metadata claims.
if delegation_name != untrusted_delegated_metadata['signed']['type']:
raise MetadataVerificationError(
'Instructed to verify provided metadata as if it is of '
'type "' + delegation_name + '", but it claims to be of '
'type "' + untrusted_delegated_metadata['signed']['type']
+ '"!')
# Process the delegation.
delegations = trusted_delegating_metadata['signed']['delegations']
if delegation_name not in delegations:
raise UnknownRoleError(
'Role ' + delegation_name + ' not found in the given '
'delegating metadata.')
expected_keys = delegations[delegation_name]['pubkeys']
threshold = delegations[delegation_name]['threshold']
verify_signable(
untrusted_delegated_metadata,
expected_keys, # drawn from trusted_delegating_metadata
threshold, # drawn from trusted_delegating_metadata
gpg=gpg) # from argument to this func
# TODO ✅: Consider taking a hex public key instead of a key object, so that:
# 1: the API is simpler (verify_signature is part of the API)
# 2: we can remove PublicKey from the higher-level code, making it
# simpler.
# The tradeoff is that we can't later accept key objects that might
# be used as interfaces to hardware keys, for example.
def verify_signature(signature, public_key, data):
"""
Raises ❌cryptography.exceptions.InvalidSignature if signature is not a
correct signature by the given key over the given data.
Raises ❌TypeError if public_key, signature, or data are not correctly
formatted.
Otherwise, returns (nothing), indicating the signature was verified.
Note that this does not use the generalized signature format (which would
be compatible with OpenPGP/GPG signatures as well as pyca/cryptography's
simple ed25519 sigs).
Args:
- public_key must be an ed25519.Ed25519PublicKeyObject
- signature must be a hex string, length 128, representing a 64-byte
raw ed25519 signature
- data must be bytes
"""
if not isinstance(public_key, ed25519.Ed25519PublicKey):
raise TypeError(
'verify_signature expects a '
'cryptography.hazmat.primitives.asymmetric.ed25519ed25519.Ed25519PublicKey'
'object as the "public_key" argument. Instead, received ' +
str(type(public_key)))
if not is_hex_signature(signature):
raise TypeError(
'verify_signature expects a hex string representing an '
'ed25519 signature as the "signature" argument. Instead, '
'received object of type ' + str(type(signature)))
if not isinstance(data, bytes):
raise TypeError(
'verify_signature expects a bytes object as the "signature" '
'argument. Instead, received ' + str(type(data)))
public_key.verify(binascii.unhexlify(signature), data)
# If no error is raised, return, indicating success (Explicit for editors)
return
def verify_signable(signable, authorized_pub_keys, threshold, gpg=False):
"""
Raises a ❌SignatureError if signable does not include at least threshold
good signatures from (unique) keys with public keys listed in
authorized_pub_keys, over the data contained in signable['signed'].
Raises ❌TypeError if the arguments are invalid.
Else returns (nothing).
Args:
- signable
common.is_a_signable(signable) must return true.
wrap_as_signable() produces output of this type. See those
functions.
- authorized_pub_keys
a list of ed25519 public keys (32 bytes) expressed as 64-character
hex strings. This is the form in which they appear in authority
metadata (root.json, etc.) Only good signatures from keys listed
in authorized_pub_keys count against the threshold of signatures
required to verify the signable.
- threshold
the number of good signatures from unique authorized keys required
in order to verify the signable.
- gpg (boolean, default False)
If True, expects OpenPGP ed25519 signatures (see RFC 4880 bis-08)
instead of raw ed25519 signatures.
If False, expects raw ed25519 signatures.
"""
# TODO: ✅ Be sure to check with the analogous code in the tuf reference
# implementation in case one of us had some clever gotcha there.
# Would be in tuf.sig or securesystemslib. See
# get_signature_status() there, in addition to any prettier
# verify_signable code I may have swapped in (dunno if that's in yet).
# TODO: ✅ Consider allowing this func (or another) to accept public keys
# in the form of ed25519.Ed25519PublicKey objects (instead of just
# the hex string representation of the public key bytes). I think
# we'll mostly have the hex strings on hand, but....
# Argument validation
if not is_a_signable(signable):
raise TypeError(
'verify_signable expects a signable dictionary. '
'Given argument failed the test.') # TODO: Tidier / expressive.
if not (isinstance(authorized_pub_keys, list) and all(
[is_hex_key(k) for k in authorized_pub_keys])):
raise TypeError('authorized_pub_keys must be a list of hex strings ')
# if not (isinstance(authorized_pub_keys, list) and all(
# [isinstance(k, ed25519.Ed25519PublicKey) for k in authorized_pub_keys])):
# raise TypeError(
# 'authorized_pub_keys must be a list of '
# 'ed25519.Ed25519PublicKeyobjects.')
if not isinstance(threshold, int) or threshold <= 0:
raise TypeError('threshold must be a positive integer.')
# TODO: ✅⚠️ Metadata specification version compatibility check.
# Check to see if signable['signed']['metadata_spec_version']
# is CLOSE ENOUGH to SECURITY_METADATA_SPEC_VERSION (same
# major version?). If it is not, raise an exception noting
# that the version cannot be verified because either it or the
# client are out of date. If versions are close enough,
# consider a warning instead. If the client is at major spec
# version x, and the metadata obtained is at major spec version
# x + 1, then proceed with a warning that the client must be
# updated. Note that root versions produced must never
# increase by more than one major spec version at a time, as a
# result.
# Put the 'signed' portion of the data into the format it should be in
# before it is signed, so that we can verify the signatures.
signed_data = canonserialize(signable['signed'])
# Even though we're not returning this, we produce this dictionary (instead
# of just counting) to facilitate future checks and logging.
# TODO: ✅ Keep track of unknown keys and bad signatures for diagnostic and
# other logging purposes.
good_sigs_from_trusted_keys = {}
for pubkey_hex, signature in signable['signatures'].items():
# Validate the signature data first (make sure it looks right).
if not is_hex_key(pubkey_hex):
# TODO: ✅ Make this a warning instead.
print(
'Ignoring signature from "key" with public key value that '
'does not look like a key value: ' + str(pubkey_hex))
continue
if not gpg and not is_signature(signature):
# TODO: ✅ Make this a warning instead.
print(
'Ignoring "signature" that does not look like a hex '
'signature value: ' + str(signature))
continue
if gpg and not is_gpg_signature(signature):
# TODO: ✅ Make this a warning instead.
print(
'Ignoring "signature" that does not look like a gpg '
'signature value: ' + str(signature))
continue
if pubkey_hex not in authorized_pub_keys:
# TODO: ✅ Make this an INFO-level log message.
print(
'Ignoring signature from a key ("' + str(pubkey_hex) +
'") that is not authorized to sign this metadata.')
continue
if not gpg: # normal ed25519 signatures using pyca/cryptography
public = PublicKey.from_hex(pubkey_hex)
if not is_signature(signature):
# TODO: ✅ Make this a warning or log statement instead.
print(
'Ignoring "signature" that does not look like a raw '
'ed25519 signature value.')
continue
try:
verify_signature(
signature['signature'],
public,
signed_data)
except cryptography.exceptions.InvalidSignature:
# TODO: ✅ Log at debug or info level.
continue
else:
good_sigs_from_trusted_keys[pubkey_hex] = signature
else: # expecting OpenPGP ed25519 signatures (RFC 4880-bis08)
assert gpg # code paranoia
try:
verify_gpg_signature(
signature,
pubkey_hex,
signed_data)
except cryptography.exceptions.InvalidSignature:
# TODO: ✅ Log at debug or info level.
continue
else:
good_sigs_from_trusted_keys[pubkey_hex] = signature
# TODO: ✅ Logging or more detailed info (which keys).
if len(good_sigs_from_trusted_keys) < threshold:
raise SignatureError(
'Expected good signatures from at least ' + str(threshold) +
' unique keys from a set of ' + str(len(authorized_pub_keys)) +
' keys. Saw ' + str(len(signable['signatures'])) +
' signatures, only ' + str(len(good_sigs_from_trusted_keys)) +
' of which were good signatures over the given data from the '
'expected keys.')
# Otherwise, return, indicating success. (Explicit for code editors)
return
def verify_gpg_signature(signature, key_value, data):
"""
Verifies a raw ed25519 signature that happens to have been produced by an
OpenPGP signing process (RFC4880).
NOTE that this code DISREGARDS most OpenPGP semantics: is interested solely
in the verification of a signature over the given data, with the raw
ed25519 public key given (in the form of a hex string). This code does not
care about the GPG public key infrastructure, including key
self-revocation, expiry, or the relationship of any key with any other key
through OpenPGP (subkeys, key-to-key signoff, etc.).
This codebase uses OpenPGP signatures solely as a means of facilitating a
TUF-style public key infrastructure, where the public key values are
trusted with specific privileges directly.
⚠️💣 ABSOLUTELY DO NOT use this for general purpose verification of GPG
signatures!! It is for our root signatures only, where OpenPGP
signing is just a proxy for a simple ed25519 signature through a
hardware signing mechanism.
# TODO: ✅ Proper docstring modeled on verify_signature.
"""
checkformat_gpg_signature(signature)
checkformat_hex_key(key_value)
checkformat_byteslike(data)
# if not isinstance(data, bytes): # TODO: ✅ use the byteslike checker in conda_content_trust.common.
# raise TypeError()
public_key = PublicKey.from_hex(key_value)
# -------
# This next part takes advantage of code pulled from:
# securesystemslib.gpg.eddsa.verify_signature(),
# securesystemslib.gpg.eddsa.create_pubkey(),
# and securesystemslib.gpg.util.hash_object().
#
# It has been unrolled, had formatting adjustments, variable
# renaming, unneeded code removal, etc.
# -------
# See RFC4880-bis8 14.8. EdDSA and 5.2.4 "Computing Signatures"
# digest = securesystemslib.gpg.util.hash_object(
# binascii.unhexlify(signature["other_headers"]),
# hasher(), data)
# Additional headers in the OpenPGP signature (bleh).
additional_header_data = binascii.unhexlify(signature['other_headers'])
# As per RFC4880 Section 5.2.4., we need to hash the content,
# signature headers and add a very opinionated trailing header
hasher = cryptography.hazmat.primitives.hashes.Hash(
cryptography.hazmat.primitives.hashes.SHA256(),
cryptography.hazmat.backends.default_backend())
hasher.update(data)
hasher.update(additional_header_data)
hasher.update(b'\x04\xff')
hasher.update(struct.pack('>I', len(additional_header_data)))
digest = hasher.finalize()
# # DEBUG 💣💥
# # DEBUG 💣💥
# print('Digest as produced by verify_gpg_signature: ' + str(digest))
# Raises cryptography.exceptions.InvalidSignature if not a valid signature.
public_key.verify(
binascii.unhexlify(signature['signature']), digest)
# Return if we succeeded.
return # explicit for clarity