test_root.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. # -*- coding: utf-8 -*-
  2. """ tests.test_root
  3. Some integration tests tests for conda-content-trust that focus on
  4. generation, signing, and verification of root metadata. This tests GPG
  5. integration via securesystemslib if securesystemslib can be successfully
  6. imported.
  7. IN CONTINUOUS INTEGRATION, this set of tests should be run with and without
  8. securesystemslib and GPG available on the system.
  9. Run the tests this way:
  10. pytest tests/test_root.py
  11. """
  12. # Python2 Compatibility
  13. from __future__ import absolute_import, division, print_function, unicode_literals
  14. import copy
  15. import json
  16. import pytest
  17. # securesystemslib is an optional dependency, and required only for signing
  18. # root metadata via GPG. Verification of those signatures, and signing other
  19. # metadata with raw ed25519 signatures, does not require securesystemslib.
  20. try:
  21. import securesystemslib.gpg.functions as gpg_funcs
  22. import securesystemslib.formats
  23. SSLIB_AVAILABLE = True
  24. except ImportError:
  25. SSLIB_AVAILABLE = False
  26. import conda_content_trust.common as common
  27. import conda_content_trust.metadata_construction as metadata_construction
  28. import conda_content_trust.signing as signing
  29. import conda_content_trust.root_signing as root_signing
  30. import conda_content_trust.authentication as authentication
  31. # Note that changing these sample values breaks the sample signature, so you'd
  32. # have to generate a new one.
  33. # A 40-hex-character GPG public key fingerprint
  34. SAMPLE_FINGERPRINT = '917adb684e2e9fb5ed4e59909ddd19a1268b62d0'
  35. SAMPLE_UNKNOWN_FINGERPRINT = '0123456789abcdef0123456789abcdef01234567'
  36. # The real key value of the public key (q, 32-byte ed25519 public key val),
  37. # as a length-64 hex string.
  38. SAMPLE_KEYVAL = 'c8bd83b3bfc991face417d97b9c0db011b5d256476b602b92fec92849fc2b36c'
  39. SAMPLE_GPG_KEY_OBJ = {
  40. 'creation_time': 1571411344,
  41. 'hashes': ['pgp+SHA2'],
  42. 'keyid': SAMPLE_FINGERPRINT,
  43. 'keyval': {
  44. 'private': '',
  45. 'public': {'q': SAMPLE_KEYVAL}
  46. },
  47. 'method': 'pgp+eddsa-ed25519',
  48. 'type': 'eddsa'
  49. }
  50. SAMPLE_ROOT_MD_CONTENT = {
  51. 'delegations': {
  52. 'key_mgr': {'pubkeys': [], 'threshold': 1},
  53. 'root': {
  54. 'pubkeys': ['c8bd83b3bfc991face417d97b9c0db011b5d256476b602b92fec92849fc2b36c'],
  55. 'threshold': 1}
  56. },
  57. 'expiration': '2030-12-09T17:20:19Z',
  58. 'metadata_spec_version': '0.6.0',
  59. 'type': 'root',
  60. 'version': 1
  61. }
  62. # To generate a new signature after changing SAMPLE_ROOT_MD_CONTENT:
  63. # > serialized = common.canonserialize(SAMPLE_ROOT_MD_CONTENT)
  64. # > new_sig = root_signing.sign_via_gpg(serialized, SAMPLE_FINGERPRINT)
  65. # If desired, you can add: new_sig['see_also'] = SAMPLE_FINGERPRINT to keep
  66. # the optional value listed (the OpenPGP fingerprint of the signing key).
  67. SAMPLE_GPG_SIG = {
  68. 'see_also': '917adb684e2e9fb5ed4e59909ddd19a1268b62d0', # optional entry
  69. 'other_headers': '04001608001d162104917adb684e2e9fb5ed4e59909ddd19a1268b62d005025f970318',
  70. 'signature': '41867f58064c89acb300b1b42f4d59ec52e11e6aab05cb7f651345d878ee06d3a4f32411646134e112a7d8adc1d1304f63fb918b57cac449baba36ef0a1fbe07'}
  71. SAMPLE_SIGNED_ROOT_MD = {
  72. 'signatures': {
  73. SAMPLE_KEYVAL: SAMPLE_GPG_SIG},
  74. 'signed': SAMPLE_ROOT_MD_CONTENT}
  75. def test_gpg_key_retrieval_with_unknown_fingerprint():
  76. if not SSLIB_AVAILABLE:
  77. pytest.skip(
  78. '--TEST SKIPPED⚠️ : Unable to use GPG key retrieval or '
  79. 'signing without securesystemslib and GPG.')
  80. return
  81. # TODO✅: Adjust this to use whatever assertRaises() functionality the
  82. # testing suite we're using provides.
  83. with pytest.raises(securesystemslib.gpg.exceptions.KeyNotFoundError):
  84. full_gpg_pubkey = gpg_funcs.export_pubkey(SAMPLE_UNKNOWN_FINGERPRINT)
  85. print(
  86. '--TEST SUCCESS✅: detection of error when we pass an unknown '
  87. 'key fingerprint to GPG for retrieval of the full public key.')
  88. def test_gpg_signing_with_unknown_fingerprint():
  89. if not SSLIB_AVAILABLE:
  90. pytest.skip(
  91. '--TEST SKIPPED⚠️ : Unable to use GPG key retrieval or '
  92. 'signing without securesystemslib and GPG.')
  93. return
  94. # TODO✅: Adjust this to use whatever assertRaises() functionality the
  95. # testing suite we're using provides.
  96. try:
  97. gpg_sig = root_signing.sign_via_gpg(
  98. b'1234', SAMPLE_UNKNOWN_FINGERPRINT)
  99. except securesystemslib.gpg.exceptions.CommandError as e:
  100. # TODO✅: This is a clumsy check. It's a shame we don't get better
  101. # than CommandError(), but this will do for now.
  102. assert 'signing failed: No secret key' in e.args[0]
  103. else:
  104. assert False, 'Expected CommandError was not raised!'
  105. print(
  106. '--TEST SUCCESS✅: detection of error when we pass an unknown '
  107. 'key fingerprint to GPG for signing.')
  108. # def test_gpg_verification_compared_to_ssls():
  109. def test_root_gen_sign_verify():
  110. # Integration test
  111. # Build a basic root metadata file with empty key_mgr delegation and one
  112. # root key, threshold 1, version 1.
  113. rmd = metadata_construction.build_root_metadata(
  114. root_version=1,
  115. root_pubkeys=[SAMPLE_KEYVAL], root_threshold=1,
  116. key_mgr_pubkeys=[], key_mgr_threshold=1)
  117. rmd = signing.wrap_as_signable(rmd)
  118. signed_portion = rmd['signed']
  119. canonical_signed_portion = common.canonserialize(signed_portion)
  120. if not SSLIB_AVAILABLE:
  121. pytest.skip(
  122. '--TEST SKIPPED⚠️ : Unable to perform GPG signing without '
  123. 'securesystemslib and GPG.')
  124. return
  125. # gpg_key_obj = securesystemslib.gpg.functions.export_pubkey(
  126. # SAMPLE_FINGERPRINT)
  127. gpg_sig = root_signing.sign_via_gpg(
  128. canonical_signed_portion, SAMPLE_FINGERPRINT)
  129. signed_rmd = copy.deepcopy(rmd)
  130. signed_rmd['signatures'][SAMPLE_KEYVAL] = gpg_sig
  131. # # Dump working files
  132. # with open('T_gpg_sig.json', 'wb') as fobj:
  133. # fobj.write(common.canonserialize(gpg_sig))
  134. # with open('T_gpg_key_obj.json', 'wb') as fobj:
  135. # fobj.write(common.canonserialize(gpg_key_obj))
  136. # with open('T_canonical_sigless_md.json', 'wb') as fobj:
  137. # fobj.write(canonical_signed_portion)
  138. # with open('T_full_rmd.json', 'wb') as fobj:
  139. # fobj.write(common.canonserialize(signed_rmd))
  140. # Verify using the SSL code and the expected pubkey object.
  141. # # (Purely as a test -- we wouldn't normally do this.)
  142. # verified = securesystemslib.gpg.functions.verify_signature(
  143. # gpg_sig, gpg_key_obj, canonical_signed_portion)
  144. # assert verified
  145. authentication.verify_gpg_signature(
  146. gpg_sig, SAMPLE_KEYVAL, canonical_signed_portion)
  147. print(
  148. '--TEST SUCCESS✅: GPG signing (using GPG and securesystemslib) and '
  149. 'GPG signature verification (using only cryptography)')
  150. def test_verify_existing_root_md():
  151. # It's important that we are able to verify root metadata without anything
  152. # except old root metadata, so in particular we don't want to need the
  153. # full GPG public key object. Ideally, we want only the Q value of the
  154. # key, but if we also need to retain the GPG key fingerprint (listed in the
  155. # signature itself), we can do that.....
  156. # with open('T_full_rmd.json', 'rb') as fobj:
  157. # signed_rmd = json.load(fobj)
  158. #
  159. # gpg_sig = signed_rmd['signatures'][
  160. # 'bfbeb6554fca9558da7aa05c5e9952b7a1aa3995dede93f3bb89f0abecc7dc07']
  161. #
  162. # canonical_signed_portion = common.canonserialize(signed_rmd['signed'])
  163. #
  164. # with open('T_gpg_key_obj.json', 'rb') as fobj:
  165. # gpg_key_obj = json.load(fobj)
  166. # q = gpg_key_obj['keyval']['public']['q']
  167. # fingerprint = gpg_key_obj['keyid']
  168. canonical_signed_portion = common.canonserialize(
  169. SAMPLE_ROOT_MD_CONTENT)
  170. # # First, try using securesystemslib's GPG signature verifier directly.
  171. # verified = securesystemslib.gpg.functions.verify_signature(
  172. # SAMPLE_GPG_SIG,
  173. # SAMPLE_GPG_KEY_OBJ, # <-- We don't want conda to have to provide this.
  174. # canonical_signed_portion)
  175. # assert verified
  176. # # Second, try it using my adapter, skipping a bit of ssl's process.
  177. # verified = root_signing.verify_gpg_sig_using_ssl(
  178. # SAMPLE_GPG_SIG,
  179. # SAMPLE_FINGERPRINT,
  180. # SAMPLE_KEYVAL,
  181. # canonical_signed_portion)
  182. # assert verified
  183. # Third, use internal code only. (This is what we're actually going to
  184. # use in conda.)
  185. # Verify using verify_gpg_signature.
  186. authentication.verify_gpg_signature(
  187. # authentication.verify_gpg_signature(
  188. SAMPLE_GPG_SIG,
  189. SAMPLE_KEYVAL,
  190. canonical_signed_portion)
  191. print(
  192. '--TEST SUCCESS✅: GPG signature verification without using GPG or '
  193. 'securesystemslib')
  194. # Verify using verify_signable.
  195. authentication.verify_signable(
  196. SAMPLE_SIGNED_ROOT_MD, [SAMPLE_KEYVAL], 1, gpg=True)
  197. # TODO ✅: Add a v2 of root to this test, and verify static v2 via v1 as
  198. # well. Also add failure modes (verifying valid v2 using v0
  199. # expectations.)