| 1 | """ |
|---|
| 2 | Testtools-style matchers useful to the Tahoe-LAFS test suite. |
|---|
| 3 | |
|---|
| 4 | Ported to Python 3. |
|---|
| 5 | """ |
|---|
| 6 | |
|---|
| 7 | import attr |
|---|
| 8 | from hyperlink import DecodedURL |
|---|
| 9 | |
|---|
| 10 | from testtools.matchers import ( |
|---|
| 11 | Mismatch, |
|---|
| 12 | AfterPreprocessing, |
|---|
| 13 | MatchesStructure, |
|---|
| 14 | MatchesDict, |
|---|
| 15 | MatchesListwise, |
|---|
| 16 | Always, |
|---|
| 17 | Equals, |
|---|
| 18 | ) |
|---|
| 19 | |
|---|
| 20 | from foolscap.furl import ( |
|---|
| 21 | decode_furl, |
|---|
| 22 | ) |
|---|
| 23 | |
|---|
| 24 | from allmydata.util import ( |
|---|
| 25 | base32, |
|---|
| 26 | ) |
|---|
| 27 | from allmydata.node import ( |
|---|
| 28 | read_config, |
|---|
| 29 | ) |
|---|
| 30 | from allmydata.crypto import ( |
|---|
| 31 | ed25519, |
|---|
| 32 | error, |
|---|
| 33 | ) |
|---|
| 34 | |
|---|
| 35 | @attr.s |
|---|
| 36 | class MatchesNodePublicKey: |
|---|
| 37 | """ |
|---|
| 38 | Match an object representing the node's private key. |
|---|
| 39 | |
|---|
| 40 | To verify, the private key is loaded from the node's private config |
|---|
| 41 | directory at the time the match is checked. |
|---|
| 42 | """ |
|---|
| 43 | basedir = attr.ib() |
|---|
| 44 | |
|---|
| 45 | def match(self, other): |
|---|
| 46 | """ |
|---|
| 47 | Match a private key which is the same as the private key in the node at |
|---|
| 48 | ``self.basedir``. |
|---|
| 49 | |
|---|
| 50 | :param other: A signing key (aka "private key") from |
|---|
| 51 | ``allmydata.crypto.ed25519``. This is the key to check against |
|---|
| 52 | the node's key. |
|---|
| 53 | |
|---|
| 54 | :return Mismatch: If the keys don't match. |
|---|
| 55 | """ |
|---|
| 56 | config = read_config(self.basedir, u"tub.port") |
|---|
| 57 | privkey_bytes = config.get_private_config("node.privkey").encode("utf-8") |
|---|
| 58 | private_key = ed25519.signing_keypair_from_string(privkey_bytes)[0] |
|---|
| 59 | signature = ed25519.sign_data(private_key, b"") |
|---|
| 60 | other_public_key = ed25519.verifying_key_from_signing_key(other) |
|---|
| 61 | try: |
|---|
| 62 | ed25519.verify_signature(other_public_key, signature, b"") |
|---|
| 63 | except error.BadSignature: |
|---|
| 64 | return Mismatch("The signature did not verify.") |
|---|
| 65 | |
|---|
| 66 | |
|---|
| 67 | def matches_storage_announcement(basedir, anonymous=True, options=None): |
|---|
| 68 | """ |
|---|
| 69 | Match a storage announcement. |
|---|
| 70 | |
|---|
| 71 | :param bytes basedir: The path to the node base directory which is |
|---|
| 72 | expected to emit the announcement. This is used to determine the key |
|---|
| 73 | which is meant to sign the announcement. |
|---|
| 74 | |
|---|
| 75 | :param bool anonymous: If True, matches a storage announcement containing |
|---|
| 76 | an anonymous access fURL. Otherwise, fails to match such an |
|---|
| 77 | announcement. |
|---|
| 78 | |
|---|
| 79 | :param list[matcher]|NoneType options: If a list, matches a storage |
|---|
| 80 | announcement containing a list of storage plugin options matching the |
|---|
| 81 | elements of the list. If None, fails to match an announcement with |
|---|
| 82 | storage plugin options. |
|---|
| 83 | |
|---|
| 84 | :return: A matcher with the requested behavior. |
|---|
| 85 | """ |
|---|
| 86 | announcement = { |
|---|
| 87 | u"permutation-seed-base32": matches_base32(), |
|---|
| 88 | } |
|---|
| 89 | if anonymous: |
|---|
| 90 | announcement[u"anonymous-storage-FURL"] = matches_furl() |
|---|
| 91 | announcement[u"anonymous-storage-NURLs"] = matches_nurls() |
|---|
| 92 | if options: |
|---|
| 93 | announcement[u"storage-options"] = MatchesListwise(options) |
|---|
| 94 | return MatchesStructure( |
|---|
| 95 | # Has each of these keys with associated values that match |
|---|
| 96 | service_name=Equals(u"storage"), |
|---|
| 97 | ann=MatchesDict(announcement), |
|---|
| 98 | signing_key=MatchesNodePublicKey(basedir), |
|---|
| 99 | ) |
|---|
| 100 | |
|---|
| 101 | |
|---|
| 102 | def matches_furl(): |
|---|
| 103 | """ |
|---|
| 104 | Match any Foolscap fURL byte string. |
|---|
| 105 | """ |
|---|
| 106 | return AfterPreprocessing(decode_furl, Always()) |
|---|
| 107 | |
|---|
| 108 | |
|---|
| 109 | def matches_nurls(): |
|---|
| 110 | """ |
|---|
| 111 | Matches a sequence of NURLs. |
|---|
| 112 | """ |
|---|
| 113 | return AfterPreprocessing( |
|---|
| 114 | lambda nurls: [DecodedURL.from_text(u) for u in nurls], |
|---|
| 115 | Always() |
|---|
| 116 | ) |
|---|
| 117 | |
|---|
| 118 | |
|---|
| 119 | def matches_base32(): |
|---|
| 120 | """ |
|---|
| 121 | Match any base32 encoded byte string. |
|---|
| 122 | """ |
|---|
| 123 | return AfterPreprocessing(base32.a2b, Always()) |
|---|
| 124 | |
|---|
| 125 | |
|---|
| 126 | |
|---|
| 127 | class MatchesSameElements: |
|---|
| 128 | """ |
|---|
| 129 | Match if the two-tuple value given contains two elements that are equal to |
|---|
| 130 | each other. |
|---|
| 131 | """ |
|---|
| 132 | def match(self, value): |
|---|
| 133 | left, right = value |
|---|
| 134 | return Equals(left).match(right) |
|---|