commit f815223fc2e7d2e460c18a90d66d0b4128eb9f23 Author: George Kadianakis desnacked@riseup.net Date: Tue Sep 3 14:03:04 2019 +0300
Add initial support for parsing v3 addresses. --- stem/descriptor/hidden_service.py | 10 ++++++- stem/descriptor/hsv3_crypto.py | 57 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-)
diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py index 52e1b0b1..e6809306 100644 --- a/stem/descriptor/hidden_service.py +++ b/stem/descriptor/hidden_service.py @@ -526,10 +526,18 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor): def create(cls, attr = None, exclude = (), validate = True, sign = False): return cls(cls.content(attr, exclude, sign), validate = validate, skip_crypto_validation = not sign)
- def __init__(self, raw_contents, validate = False, skip_crypto_validation = False): + def __init__(self, raw_contents, validate = False, onion_address = None, skip_crypto_validation = False): + """ + The onion_address is needed so that we can decrypt the descriptor, which is + impossible without the full onion address. + """ super(HiddenServiceDescriptorV3, self).__init__(raw_contents, lazy_load = not validate) entries = _descriptor_components(raw_contents, validate)
+ if onion_address == None: + raise ValueError("The onion address MUST be provided to parse a V3 descriptor") + self.onion_address = onion_address + if validate: for keyword in REQUIRED_V3_FIELDS: if keyword not in entries: diff --git a/stem/descriptor/hsv3_crypto.py b/stem/descriptor/hsv3_crypto.py new file mode 100644 index 00000000..0feb0fe0 --- /dev/null +++ b/stem/descriptor/hsv3_crypto.py @@ -0,0 +1,57 @@ +import base64 +import hashlib + +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend + + +""" +Onion addresses + + onion_address = base32(PUBKEY | CHECKSUM | VERSION) + ".onion" + CHECKSUM = H(".onion checksum" | PUBKEY | VERSION)[:2] + + - PUBKEY is the 32 bytes ed25519 master pubkey of the hidden service. + - VERSION is an one byte version field (default value '\x03') + - ".onion checksum" is a constant string + - CHECKSUM is truncated to two bytes before inserting it in onion_address + +""" + +CHECKSUM_CONSTANT = b".onion checksum" + +def decode_address(onion_address_str): + """ + Parse onion_address_str and return the pubkey. + + onion_address = base32(PUBKEY | CHECKSUM | VERSION) + ".onion" + CHECKSUM = H(".onion checksum" | PUBKEY | VERSION)[:2] + + :return: Ed25519PublicKey + + :raises: ValueError + """ + if (len(onion_address_str) != 56 + len(".onion")): + raise ValueError("Wrong address length") + + # drop the '.onion' + onion_address = onion_address_str[:56] + + # base32 decode the addr (convert to uppercase since that's what python expects) + onion_address = base64.b32decode(onion_address.upper()) + assert(len(onion_address) == 35) + + # extract pieces of information + pubkey = onion_address[:32] + checksum = onion_address[32:34] + version = onion_address[34] + + # Do checksum validation + my_checksum_body = b"%s%s%s" % (CHECKSUM_CONSTANT, pubkey, bytes([version])) + my_checksum = hashlib.sha3_256(my_checksum_body).digest() + + if (checksum != my_checksum[:2]): + raise ValueError("Bad checksum") + + return Ed25519PublicKey.from_public_bytes(pubkey)