-
Notifications
You must be signed in to change notification settings - Fork 13
/
common.py
1265 lines (981 loc) · 45.9 KB
/
common.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
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
""" conda_content_trust.common
This module contains functions that provide format validation, serialization,
and some key transformations for the pyca/cryptography library. These are used
across conda_content_trust modules.
Function Manifest for this Module, by Category
Encoding:
x canonserialize
Formats and Validation:
PrivateKey -- extends cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey
PublicKey -- extends cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey
checkformat_string
x is_hex_string
x is_hex_signature
r is_hex_key
is_hex_hash
r checkformat_hex_key
checkformat_hex_hash
r checkformat_list_of_hex_keys
x is_a_signable
x checkformat_byteslike
x checkformat_natural_int
x checkformat_expiration_distance
x checkformat_utc_isoformat
x checkformat_gpg_fingerprint
is_gpg_fingerprint
x checkformat_gpg_signature
is_gpg_signature
checkformat_any_signature
is_delegation
checkformat_delegation
is_delegations
checkformat_delegations
checkformat_delegating_metadata
x iso8601_time_plus_delta
Crypto Utility:
x sha512256
x keyfiles_to_keys
x keyfiles_to_bytes
Exceptions:
CCT_Error
SignatureError
MetadataVerificationError
UnknownRoleError
"""
# Python2 Compatibility
from __future__ import absolute_import, division, print_function, unicode_literals
import json
import datetime
import re # for UTC iso8601 date string checking
import binascii # solely for hex string <-> bytes conversions
from six import string_types
import cryptography.hazmat.primitives.asymmetric.ed25519 as ed25519
import cryptography.hazmat.primitives.serialization as serialization
import cryptography.hazmat.primitives.hashes
import cryptography.hazmat.backends.openssl.ed25519
# specification version for the metadata produced by conda-content-trust
# Details in the Conda Security Metadata Specification. Note that this
# version string is parsed via setuptools's packaging.version library, and so
# supports PEP 440; however, we should use a limited subset that is numerical
# only, and according to SemVer principles.
# PEP 440 compatibility:
# > None is not re.match(r'^([1-9]\d*!)?(0|[1-9]\d*)(\.(0|[1-9]\d*))*((a|b|rc)(0|[1-9]\d*))?(\.post(0|[1-9]\d*))?(\.dev(0|[1-9]\d*))?$', version_string)
# SemVer compatibility:
# > None is not re.match(r'^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$', version_string)
# Try, however, to keep to three simple numeric elements separated by periods,
# i.e., things that match this subset of SemVer:
# > None is not re.match(r'^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)$', version_string)
SECURITY_METADATA_SPEC_VERSION = '0.6.0'
# The only types we're allowed to wrap as "signables" and sign are
# the JSON-serializable types. (There are further constraints to what is
# JSON-serializable in addition to these type constraints.)
SUPPORTED_SERIALIZABLE_TYPES = [
dict, list, tuple, str, int, float, bool, type(None)]
# These are the permissible strings in the "type" field of delegating metadata.
SUPPORTED_DELEGATING_METADATA_TYPES = ['root', 'key_mgr'] # May be loosened later.
# (I think the regular expression checks for datetime strings run faster if we
# compile the pattern once and use the same object for all checks. For a
# pattern like this, it's probably a negligible difference, though, and
# it's conceivable that the compiler already optimizes this....)
UTC_ISO8601_REGEX_PATTERN = re.compile(
'^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$')
class CCT_Error(Exception):
"""
All errors we raise that are not ValueErrors, TypeErrors, or
certain errors from securesystemslib should be instances of this class or
of subclasses of this class.
"""
class SignatureError(CCT_Error):
"""
Indicates that a signable cannot be verified due to issues with the
signature(s) inside it.
"""
class MetadataVerificationError(CCT_Error):
"""
Indicates that a chain of authority metadata cannot be verified (e.g.
a metadata update is found on the repository, but could not be
authenticated).
"""
class UnknownRoleError(CCT_Error):
"""
Indicates that a piece of role metadata (like root.json, or key_mgr.json)
was expected but not found.
"""
def canonserialize(obj):
"""
Given a JSON-compatible object, does the following:
- serializes the dictionary as utf-8-encoded JSON, lazy-canonicalized
such that any dictionary keys in any dictionaries inside <dictionary>
are sorted and indentation is used and set to 2 spaces (using json lib)
TODO: ✅ Implement the serialization checks from serialization document.
Note that if the provided object includes a dictionary that is *indexed*
by both strings and integers, a TypeError will be raised complaining about
comparing strings and integers during the sort. (Each dictionary in an
object must be indexed only by strings or only by integers.)
"""
# Try converting to a JSON string.
try:
# TODO: In the future, assess whether or not to employ more typical
# practice of using no whitespace (instead of NLs and 2-indent).
json_string = json.dumps(obj, indent=2, sort_keys=True)
except TypeError:
# TODO: ✅ Log or craft/use an appropriate exception class.
raise
return json_string.encode('utf-8')
def load_metadata_from_file(fname):
# TODO ✅: Argument validation for fname. Consider adding "pathvalidate"
# as a dependency, and calling its sanitize_filename() here.
with open(fname, 'rb') as fobj:
metadata = json.load(fobj)
# TODO ✅: Consider validating what is read here, for everywhere.
return metadata
def write_metadata_to_file(metadata, filename):
"""
Canonicalizes and serializes JSON-friendly metadata, and writes that to the
given filename.
"""
# TODO ✅: Argument validation for filename. Consider adding
# "pathvalidate" as a dependency, and calling its
# sanitize_filename() here.
metadata = canonserialize(metadata)
with open(filename, 'wb') as fobj:
fobj.write(metadata)
class MixinKey(object):
"""
This is a mix-in (https://www.ianlewis.org/en/mixins-and-python) for the
PrivateKey and PublicKey classes, specifically. It provides some
convenience functions.
"""
def to_bytes(self):
"""
Pops out the nice, tidy bytes of a given ed25519 key object, public or
private.
"""
if isinstance(self, ed25519.Ed25519PrivateKey):
return self.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption())
elif isinstance(self, ed25519.Ed25519PublicKey):
return self.public_bytes(
serialization.Encoding.Raw,
serialization.PublicFormat.Raw)
else:
assert False, (
'Code error: this should not be possible. This mix-in '
'should only be used by classes inheriting from the '
'"cryptography" library ed25519 key classes.')
def to_hex(self):
"""
Represents the underlying ed25519 key value as a hex string, 64
characters long, representing 32 bytes of data.
"""
return binascii.hexlify(self.to_bytes()).decode('utf-8')
def is_equivalent_to(self, k2):
"""
Given Ed25519PrivateKey or Ed25519PublicKey objects, determines if the
underlying key data is identical.
"""
checkformat_key(k2)
return self.to_bytes() == k2.to_bytes()
@classmethod # a class method for inheritors of this mix-in
def from_bytes(cls, key_value_in_bytes):
"""
Constructs an object of the class based on the given key value.
The "cryptography" library provides from_public_bytes() and
from_private_bytes() class methods for Ed25519PublicKey and
Ed25519PrivateKey classes in place of constructors. We extend provide
a single API for those, and make the created objects objects of the
subclass using this mix-in.
"""
# from_private_bytes() and from_public_bytes() both check length (32),
# but do not produce helpful errors if the argument provided it is not
# the right type, so we'll do that here before calling them.
checkformat_byteslike(key_value_in_bytes)
if issubclass(cls, ed25519.Ed25519PrivateKey):
new_object = cls.from_private_bytes(key_value_in_bytes)
elif issubclass(cls, ed25519.Ed25519PublicKey):
new_object = cls.from_public_bytes(key_value_in_bytes)
else:
assert False, (
'Code error: this should not be possible. This mix-in '
'should only be used by classes inheriting from the '
'"cryptography" library ed25519 key classes.')
# Fixed:
# # TODO: ✅❌⚠️💣 Changing this here is uncouth. It MUST BE SET AT
# # CLASS DEFINITION time. Change this!
# # Note that this mro modification mess is required in some form or
# # another because ed25519.Ed25519PrivateKey and Ed25519PublicKey
# # use metaclassing (in a way that I don't think is useful, btw).
# # This line is poking cls.__bases__. It would appear to do nothing,
# # since we're extending a tuple with nothing, but it *actually* causes
# # the class's MRO (method resolution order) to be recalculated.
# # Before this line is run, it does not include PrivateKey (this class),
# # and after this line is run, it will include PrivateKey. This should
# # probably be done with some manner of metaclass decorator instead.
# #
# # Before the next two lines are run, this is the situation:
# # > cls.__bases__
# # (<class 'conda_content_trust.common.MixinKey'>,
# # <class 'cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey'>)
# # > new_object.__class__
# # <class 'cryptography.hazmat.backends.openssl.ed25519._Ed25519PrivateKey'>
# cls.__bases__ += tuple()
new_object.__class__ = cls
assert isinstance(new_object, cls)
assert (
isinstance(new_object, ed25519.Ed25519PrivateKey)
or isinstance(new_object, ed25519.Ed25519PublicKey))
checkformat_key(new_object)
return new_object
@classmethod # a class method for inheritors of this mix-in
def from_hex(cls, key_value_in_hex):
# from_private_bytes() and from_public_bytes() both check length (32),
# but do not produce helpful errors if the argument provided it is not
# the right type, so we'll do that here before calling them.
checkformat_hex_key(key_value_in_hex)
key_value_in_bytes = binascii.unhexlify(key_value_in_hex)
new_object = cls.from_bytes(key_value_in_bytes)
checkformat_key(new_object)
return new_object
# if issubclass(cls, ed25519.Ed25519PrivateKey):
# return cls.from_private_bytes(binascii.unhexlify(key_value_in_hex))
# elif issubclass(cls, ed25519.Ed25519PublicKey):
# return cls.from_public_bytes(binascii.unhexlify(key_value_in_hex))
# else:
# assert False, (
# 'Code error: this should not be possible. This mix-in '
# 'should only be used by classes inheriting from the '
# '"cryptography" library ed25519 key classes.')
# new_object.__class__ = cls
# assert isinstance(new_object, cls)
# assert (
# isinstance(new_object, Ed25519PrivateKey)
# or isinstance(new_object, Ed25519PublicKey))
class PrivateKey(
MixinKey,
# TODO: ✅❌⚠️💣 Find a way around leaving this next line here if
# possible. It's a private class.
cryptography.hazmat.backends.openssl.ed25519._Ed25519PrivateKey, # DANGER
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey
# Note that inheritance class order should use the "true" base class
# last in Python.
):
"""
This class expands the class
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey
very slightly, adding some functionality from MixinKey.
Note on the sign() method:
We preserve Ed25519PrivateKey's sign method unchanged. The sign()
method is deterministic and does not depend at any point on the ability
to generate random data (unlike the key generation). The returned
value for sign() is a length 64 bytes() object, a raw ed25519
signature.
"""
def public_key(self): # Overrides ed25519.Ed25519PrivateKey's method
"""
Return the public key corresponding to this private key.
"""
# TODO: ✅❌⚠️💣 Confirm that this override works. We MUST override
# the public_key() method. If we just let the
# parent class's public_key() method be called, we'll
# get an object of the wrong type.
public = super().public_key() # TODO: ✅ Python 2 compliance
public.__class__ = PublicKey # TODO: ✅ This should not be hardcoded?
checkformat_key(public)
return public
@classmethod # a class method for inheritors of this mix-in
def generate(cls): # Overrides ed25519.Ed25519PrivateKey's class method
"""
Wrap the superclass's key generation class function
(ed25519.Ed25519PrivateKey.generate()), in order to make sure the
generated key has the PrivateKey subclass.
"""
# TODO: ✅❌⚠️💣 Confirm that this override works. We MUST override
# the generate() class method. If we just let the
# parent class's generate() method be called, we'll
# get an object of the wrong type.
private = super().generate() # TODO: ✅ Python 2 compliance
private.__class__ = PrivateKey # TODO: ✅ Should this be hardcoded?
checkformat_key(private)
return private
class PublicKey(
MixinKey,
# TODO: ✅❌⚠️💣 Find a way around leaving this next line here if
# possible. It's a private class.
cryptography.hazmat.backends.openssl.ed25519._Ed25519PublicKey, # DANGER
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey
# Note that inheritance class order should use the "true" base class
# last in Python.
):
"""
This class expands the class
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey
very slightly, adding some functionality from MixinKey.
We preserve Ed25519PublicKey's verify() method unchanged.
"""
# No.... For now, I'll stick with the raw dictionary representations.
# If function profusion makes it inconvenient for folks to use this library,
# it MAY then be time to make signatures into class objects... but it's
# probably best to avoid that potential complexity and confusion.
# class Signature():
# def __init__(self, ):
# self.is_gpg_sig = False
# ✅ TODO: Consider a schema definitions module, e.g. PyPI project "schema"
def is_hex_string(s):
"""
Returns True if hex is a hex string with no uppercase characters, no spaces,
etc. Else, False.
"""
try:
checkformat_hex_string(s)
return True
except (ValueError, TypeError):
return False
def checkformat_hex_string(s):
"""
Throws TypeError if s is not a string (string_types).
Throws ValueError if the given string is not a string of hexadecimal
characters (upper-case not allowed to prevent redundancy).
"""
if not isinstance(s, string_types):
raise TypeError(
'Expected a hex string; given value is not string typed.')
for c in s:
if c not in [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
'a', 'b', 'c', 'd', 'e', 'f']:
raise ValueError(
'Expected a hex string; non-hexadecimal or upper-case '
'character found: "' + str(c) + '".')
def is_hex_signature(sig):
"""
Returns True if key is a hex string with no uppercase characters, no
spaces, no '0x' prefix(es), etc., and is 128 hexadecimal characters (the
correct length for an ed25519 signature, 64 bytes of raw data represented
as 128 hexadecimal characters).
Else, returns False.
"""
if is_hex_string(sig) and len(sig) == 128:
return True
return False
def is_hex_key(key):
"""
Returns True if key is a hex string with no uppercase characters, no
spaces, no '0x' prefix(es), etc., and is 64 hexadecimal characters (the
correct length for an ed25519 key, 32 bytes of raw data represented as 64
hexadecimal characters).
Else, returns False.
"""
try:
checkformat_hex_key(key)
return True
except (TypeError, ValueError):
return False
def is_hex_hash(h):
"""
Returns True if h is a hex string with no uppercase characters, no
spaces, no '0x' prefix(es), etc., and is 64 hexadecimal characters (the
correct length for a sha256 or sha512256 hash, 32 bytes of raw data
represented as 64 hexadecimal characters).
Else, returns False.
Indistinguishable from is_hex_key.
"""
return is_hex_key(h)
def is_a_signable(dictionary):
"""
Returns True if the given dictionary is a signable dictionary as produced
by wrap_as_signable. Note that there MUST be no additional elements beyond
'signed' and 'signable' in the dictionary. (The only data in the envelope
outside the signed portion of the data should be the signatures; what's
outside of 'signed' is under attacker control.)
"""
if (
isinstance(dictionary, dict)
and 'signatures' in dictionary
and 'signed' in dictionary
and isinstance(dictionary['signatures'], dict) #, list)
and type(dictionary['signed']) in SUPPORTED_SERIALIZABLE_TYPES
and len(dictionary) == 2
):
return True
else:
return False
# TODO: ✅ Consolidate: switch to use of this wherever is_a_signable is called
# and then an error is raised if the result is False.
def checkformat_signable(dictionary):
if not is_a_signable(dictionary):
raise TypeError(
'Expected a signable dictionary, but the given argument '
'does not match expectations for a signable dictionary '
'(must be a dictionary containing only keys "signatures" and '
'"signed", where the value for key "signatures" is a dict '
'and the value for key "signed" is a supported serializable '
'type (' + str(SUPPORTED_SERIALIZABLE_TYPES) + ')')
def checkformat_byteslike(obj):
if not hasattr(obj, 'decode'):
raise TypeError('Expected a bytes-like object with a decode method.')
def checkformat_natural_int(number):
# Technically a TypeError or ValueError, depending, but meh.
if int(number) != number or number < 1:
raise ValueError('Expected an integer >= 1.')
# This is not yet widely used.
# TODO: ✅ See to it that anywhere we're checking for a string, we use this.
def checkformat_string(s):
if not isinstance(s, string_types):
raise TypeError('Expecting a string')
def checkformat_expiration_distance(expiration_distance):
if not isinstance(expiration_distance, datetime.timedelta):
raise TypeError(
'Expiration distance must be a datetime.timedelta object. '
'Instead received a ' + + str(type(expiration_distance)))
def checkformat_hex_key(k):
checkformat_hex_string(k)
if 64 != len(k):
raise ValueError(
'Expected a 64-character hex string representing a key value.')
# Prevent multiple possible representations of keys. There are security
# implications. For example, we cannot permit two signatures from the
# same key -- with the key represented differently -- to count as two
# signatures from distinct keys.
if k.lower() != k:
raise ValueError(
'Hex representations of keys must use only lowercase.')
def checkformat_hex_hash(h):
checkformat_hex_string(h)
if 64 != len(h):
raise ValueError(
'Expected a 64-character hex string representing a hash.')
# Prevent multiple possible representations. There are security
# implications.
if h.lower() != h:
raise ValueError(
'Hex representations of hashes must use only lowercase.')
def checkformat_list_of_hex_keys(l):
"""
Note that this rejects any list of keys that includes any exact duplicates.
"""
if not isinstance(l, list):
raise TypeError(
'Expected a list of 64-character hex strings representing keys.')
for key in l:
checkformat_hex_key(key)
if len(set(l)) != len(l):
raise ValueError(
'The given list of keys in hex string form contains duplicates. '
'Duplicates are not permitted.')
def checkformat_utc_isoformat(s):
# e.g. '1999-12-31T23:59:59Z'
# TODO: ✅ Python2/3-compatible string check
# Note that ^ and $ use is redundant with use of fullmatch here (defensive
# coding). See also notes for UTC_ISO8601_REGEX_PATTERN above.
if UTC_ISO8601_REGEX_PATTERN.fullmatch(s) is None:
raise TypeError(
'The provided string appears not to be a datetime string '
'formatted as an ISO8601 UTC-specific datetime (e.g. '
'"1999-12-31T23:59:59Z".')
def is_gpg_fingerprint(fingerprint):
"""
True if the given value is a hex string of length 40 (representing a
20-byte SHA-1 value, which is what OpenPGP/GPG uses as a key fingerprint).
"""
try:
checkformat_gpg_fingerprint(fingerprint)
return True
except (TypeError, ValueError):
return False
def checkformat_gpg_fingerprint(fingerprint):
"""
See is_gpg_fingerprint. Raises a TypeError if is_gpg_fingerprint is not
True.
"""
checkformat_hex_string(fingerprint)
if len(fingerprint) != 40:
raise ValueError(
'The given value, "' + str(fingerprint) + '", is not a full '
'GPG fingerprint (40 hex characters).')
# ⚠️ Yes, the following is a redundant test. Please leave it here in case
# code changes elsewhere.
# Prevent multiple possible representations of keys. There are security
# implications. For example, we cannot permit two signatures from the
# same key -- with the key represented differently -- to count as two
# signatures from distinct keys.
if fingerprint.lower() != fingerprint:
raise ValueError(
'Hex representations of GPG key fingerprints should use only '
'lowercase.')
def checkformat_sslgpg_signature(signature_obj):
"""
Raises a TypeError if the given object is not a dictionary representing a
signature in a format like that produced by
securesystemslib.gpg.functions.create_signature(), conforming to
securesystemslib.formats.GPG_SIGNATURE_SCHEMA.
We will generally use a slightly different format in order to include the
raw ed25519 public key value.
This is the format we
expect for Root signatures.
If the given object matches the format, returns silently.
"""
if not (
isinstance(signature_obj, dict)
and 'keyid' in signature_obj
and 'other_headers' in signature_obj
and 'signature' in signature_obj
and len(signature_obj) == 3
and is_hex_signature(signature_obj['signature'])
# TODO ✅: Determine if we can constrain "other_headers" beyond
# limiting it to a hex string. (No length constraint is
# provided here, for example.)
and is_hex_string(signature_obj['other_headers'])):
raise TypeError(
'Expected a dictionary representing a GPG signature in the '
'securesystemslib.formats.GPG_SIGNATURE_SCHEMA format.')
checkformat_gpg_fingerprint(signature_obj['keyid'])
def is_gpg_signature(signature_obj):
# TODO: ✅ docstring based on docstring from checkformat_gpg_signature
try:
checkformat_gpg_signature(signature_obj)
return True
except (ValueError, TypeError):
return False
def checkformat_gpg_signature(signature_obj):
"""
Raises a TypeError if the given object is not a dictionary representing a
signature in a format that we expect.
This is similar to BUT NOT THE SAME AS that produced by
securesystemslib.gpg.functions.create_signature(), conforming to
securesystemslib.formats.GPG_SIGNATURE_SCHEMA.
We use a slightly different format in order to include the raw ed25519
public key value. This is the format we expect for Root signatures.
If the given object matches the format, returns silently.
"""
if not isinstance(signature_obj, dict):
raise TypeError(
'OpenPGP signatures objects must be dictionaries. Received '
'type ' + str(type(signature_obj)) + ' instead.')
if sorted(list(signature_obj.keys())) not in [
['other_headers', 'signature'],
['other_headers', 'see_also', 'signature']]:
raise ValueError(
'OpenPGP signature objects must include a "signature" and an '
'"other_headers" entry, and may include a "see_also" entry. No '
'other entries are permitted.')
if not is_hex_string(signature_obj['other_headers']):
raise ValueError(
'"other_headers" entry in OpenPGP signature object must be a '
'hex string.')
# TODO ✅: Determine if we can constrain "other_headers" beyond
# limiting it to a hex string. (No length constraint is
# provided here, for example.)
if not is_hex_signature(signature_obj['signature']):
raise ValueError(
'"signature" entry in OpenPGP signature obj must be a hex '
'string representing an ed25519 signature, 128 hex characters '
'representing 64 bytes of data.')
if 'see_also' in signature_obj:
checkformat_gpg_fingerprint(signature_obj['see_also'])
def is_a_signature(signature_obj):
"""
Returns True if signature_obj is a dictionary representing an ed25519
signature, either in the conda-content-trust normal format, or
the format for a GPG signature.
See conda_content_trust.common.checkformat_signature() docstring for more details.
"""
try:
checkformat_signature(signature_obj)
return True
except (TypeError, ValueError):
return False
def checkformat_signature(signature_obj):
"""
Raises a TypeError if the given object is not a dictionary.
Raises a ValueError if the given object is a dictionary, but is not in
our generalized signature format (supports both raw ed25519 signatures
OpenPGP/GPG signatures).
If the given object matches the format, returns silently.
The generalized signature format is:
{
(REQUIRED) 'signature': <64-byte value ed25519 signature, as 128 hex chars>,
(GPG SIGS ONLY) 'other_headers': <hex string of irrelevant OpenPGP data hashed in the signature digest>,
(OPTIONAL) 'see_also': <40-hex-character SHA1 OpenPGP/GPG key identifier, for diagnostic purposes>
}
Examples:
{ 'signature': 'deadbeef'*32} # normal ed25519 signature (no OpenPGP)
{ 'signature': 'deadbeef'*32, # OpenPGP ed25519 signature
'other_headers': 'deadbeef'*??} # extra info OpenPGP insists on signing over
{ 'signature': 'deadbeef'*32, # OpenPGP ed25519 signature
'other_headers': 'deadbeef'*??,
'see_also': 'deadbeef'*10}} # listing an OpenPGP key fingerprint
"""
if not isinstance(signature_obj, dict):
raise TypeError('Expected a signature object, of type dict.')
elif not (
'signature' in signature_obj
and is_hex_signature(signature_obj['signature'])):
# Even the minimal required element is not correct, so...
raise ValueError(
'Expected a dictionary representing an ed25519 signature as a '
'128-character hex string. This requires at least key '
'"signature", with value a 128-character hexadecimal string '
'representing a (64-byte) ed25519 signature.')
# simple ed25519 signature, not an OpenPGP signature
elif len(signature_obj) == 1:
# If this is a simple ed25519 signature, and not an OpenPGP/GPG
# signature, then we're all set, since 'signature' is included and
# has a reasonable value.
return
# Permit an OpenPGP (GPG / RFC 4880) signature noted as defined in
# function is_gpg_signature.
elif is_gpg_signature(signature_obj):
return
else:
raise ValueError(
'Provided signature does not have the correct format for a '
'signature object (neither simple ed25519 sig nor OpenPGP '
'ed25519 sig).')
def is_signature(s):
"""
True if the given value is a dictionary containing a 'signature' entry
with value set to a hex string of length 128 (representing an ed25519
signature).
"""
try:
checkformat_signature(s)
return True
except (TypeError, ValueError):
return False
def checkformat_delegation(delegation):
"""
A dictionary specifying public key values and threshold of keys
e.g.
{ 'pubkeys': ['ff'*32, '1e'*32],
'threshold': 1}
threshold must be an integer >= 1. pubkeys must be a list of hexadecimal
representations of ed25519 public keys.
Note that because drafts are allowed, we do not demand here that the list
of pubkeys include enough keys to meet threshold. Not listing pubkeys yet
is okay during writing, but when verifying metadata, one should not accept
delegations with impossible-to-meet requirements (len(pubkeys) < threshold)
"""
if not isinstance(delegation, dict):
raise TypeError(
'Delegation information must be a dictionary specifying '
'"pubkeys" and "threshold" elements.')
elif not (
len(delegation) == 2
and 'threshold' in delegation
and delegation['threshold'] >= 1
and 'pubkeys' in delegation
and isinstance(delegation['pubkeys'], list)
and all([is_hex_key(k) for k in delegation['pubkeys']])):
raise ValueError(
'Delegation information must be a dictionary specifying '
'exactly two elements: "pubkeys" (assigned a list of '
'64-character hex strings representing individual ed25519 '
'public keys) and "threshold", assigned an integer >= 1.')
# We have the right type, and the right keys. Check the values.
checkformat_list_of_hex_keys(delegation['pubkeys'])
checkformat_natural_int(delegation['threshold'])
def is_a_delegation(delegation):
try:
checkformat_delegation(delegation)
return True
except (ValueError, TypeError):
return False
def checkformat_delegations(delegations):
"""
A dictionary specifying a delegation for any number of role names.
Index: rolename. Value: delegation (see checkformat_delegation).
e.g.
{ 'root.json':
{'pubkeys': ['01'*32, '02'*32, '03'*32], # each is a lower-case hex string w/ length 64
'threshold': 2}, # integer >= 1
'channeler.json':
{'pubkeys': ['04'*32], 'threshold': 1}}
"""
if not isinstance(delegations, dict):
raise TypeError(
'"Delegations" information must be a dictionary indexed by '
'role names, with values equal to dictionaries that each '
'specify elements "pubkeys" and "threshold".')
for index in delegations:
checkformat_string(index)
checkformat_delegation(delegations[index])
def is_delegations(delegations):
try:
checkformat_delegations(delegations)
return True
except (ValueError, TypeError):
return False
def checkformat_delegating_metadata(metadata):
"""
Validates argument "metadata" as delegating metadata. Passes if it is,
raises a TypeError or ValueError if it is not.
The required format is a dictionary containing all contents of a delegating
metadata file, like root.json or key_mgr.json. (This includes both the
signatures portion and the signed contents portion, in the usual envelope
-- see also checkformat_signable.)
The required structure:
{
'signatures': {}, # for each entry in the 'signatures' dict:
# - the key must pass checkformat_hex_key
# - the value must pass checkformat_signature()
# or checkformat_gpg_signature()
'signed': {
'type': <type>: # must match a string in SUPPORTED_DELEGATING_METADATA_TYPES (e.g. 'root')
'metadata_spec_version': <SemVer string>,
'delegations': {}, # value must pass checkformat_delegations()
'expiration': <date>, # date must pass checkformat_utc_isoformat()
# The 'signed' dict must always include either a 'timestamp' entry,
# a 'version' entry, or both. Further, in root metadata the
# 'signed' dict must always include a 'version' entry (to support
# root chaining).
'timestamp': <date>, # if included, must pass checkformat_utc_isoformat()
'version': <integer> # if included, must pass checkformat_natural_int(), i.e. be an integer >= 1
}
}
e.g.
{
"signatures": { # 0 or more signatures over the signed contents
<public key>: {
"other_headers": "04001608001d162104f075dd2f6f4cb3bd76134bbb81b6ca16ef9cd58905025f0bf546",
"signature": "ab3e03385f757da74e08b46f1bf82709fbc2ce21823c28e2f0e3452415e2a9f1e2c82e418cc44e2908618cf0c7375f32fe0a5a75494909a59a82875ebc703c02",
},
...
},
"signed": { # signed contents
"delegations": {
"key_mgr.json": {
"pubkeys": [
"013ddd714962866d12ba5bae273f14d48c89cf0773dee2dbf6d4561e521c83f7"
],
"threshold": 1
},
"root.json": {
"pubkeys": [
"bfbeb6554fca9558da7aa05c5e9952b7a1aa3995dede93f3bb89f0abecc7dc07"
],
"threshold": 1
}
},
"expiration": "2021-07-13T05:46:45Z",
"metadata_spec_version": "0.1.0",
"timestamp": "2020-07-13T05:46:45Z",
"type": "root",
"version": 1
}
}
"""
# Signing envelope required
checkformat_signable(metadata)
for k in metadata['signatures']:
checkformat_any_signature(metadata['signatures'][k])
contents = metadata['signed']
for entry in [ # required fields
'type', 'metadata_spec_version', 'delegations', 'expiration']:
if entry not in contents:
raise ValueError(
'Expected a "' + str(entry) + '" entry in the given '
'delegating metadata.')
checkformat_string(contents['type'])
if contents['type'] not in SUPPORTED_DELEGATING_METADATA_TYPES: