diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py index 449e106c5..7bb6a31c5 100644 --- a/stem/descriptor/certificate.py +++ b/stem/descriptor/certificate.py @@ -26,13 +26,16 @@ Purpose of Ed25519 certificate. As new certificate versions are added this enumeration will expand. - ============== =========== - CertType Description - ============== =========== - **SIGNING** signing a signing key with an identity key - **LINK_CERT** TLS link certificate signed with ed25519 signing key - **AUTH** authentication key signed with ed25519 signing key - ============== =========== + ============== =========== + CertType Description + ============== =========== + **SIGNING** signing a signing key with an identity key + **LINK_CERT** TLS link certificate signed with ed25519 signing key + **AUTH** authentication key signed with ed25519 signing key + **HS_V3_DESC_SIGNING_KEY** onion service v3 descriptor signing key cert (see rend-spec-v3.txt) + **HS_V3_INTRO_POINT_AUTH_KEY** onion service v3 intro point authentication key cert (see rend-spec-v3.txt) + **HS_V3_INTRO_POINT_ENC_KEY** onion service v3 intro point encryption key cert (see rend-spec-v3.txt) + ============== =========== .. data:: ExtensionType (enum) @@ -65,12 +68,19 @@ import stem.prereq import stem.util.enum import stem.util.str_tools +import stem.util + +from cryptography.hazmat.primitives import serialization ED25519_HEADER_LENGTH = 40 ED25519_SIGNATURE_LENGTH = 64 ED25519_ROUTER_SIGNATURE_PREFIX = b'Tor router descriptor signature v1' -CertType = stem.util.enum.UppercaseEnum('SIGNING', 'LINK_CERT', 'AUTH') +CertType = stem.util.enum.UppercaseEnum( + 'RESERVED_0', 'RESERVED_1', 'RESERVED_2', 'RESERVED_3', 'SIGNING', 'LINK_CERT', + 'AUTH', 'RESERVED_RSA', 'HS_V3_DESC_SIGNING_KEY', 'HS_V3_INTRO_POINT_AUTH_KEY', + 'HS_V3_INTRO_POINT_ENC_KEY', 'HS_V3_NTOR_ENC') + ExtensionType = stem.util.enum.Enum(('HAS_SIGNING_KEY', 4),) ExtensionFlag = stem.util.enum.UppercaseEnum('AFFECTS_VALIDATION', 'UNKNOWN') @@ -147,19 +157,16 @@ def __init__(self, version, encoded, decoded): raise ValueError('Ed25519 certificate was %i bytes, but should be at least %i' % (len(decoded), ED25519_HEADER_LENGTH + ED25519_SIGNATURE_LENGTH)) cert_type = stem.util.str_tools._to_int(decoded[1:2]) + try: + self.type = CertType.keys()[cert_type] + except IndexError: + raise ValueError('Certificate has wrong cert type') - if cert_type in (0, 1, 2, 3): + # Catch some invalid cert types + if self.type in ('RESERVED_0', 'RESERVED_1', 'RESERVED_2', 'RESERVED_3'): raise ValueError('Ed25519 certificate cannot have a type of %i. This is reserved to avoid conflicts with tor CERTS cells.' % cert_type) - elif cert_type == 4: - self.type = CertType.SIGNING - elif cert_type == 5: - self.type = CertType.LINK_CERT - elif cert_type == 6: - self.type = CertType.AUTH - elif cert_type == 7: + elif self.type == ('RESERVED_RSA'): raise ValueError('Ed25519 certificate cannot have a type of 7. This is reserved for RSA identity cross-certification.') - else: - raise ValueError("BUG: Ed25519 certificate type is decoded from one byte. It shouldn't be possible to have a value of %i." % cert_type) # expiration time is in hours since epoch try: @@ -214,6 +221,9 @@ def is_expired(self): return datetime.datetime.now() > self.expiration + # ATAGAR XXX certificates are generic and not just for descriptor, however + # this function assumes they are. this needs to be moved to the descriptor + # module. the new verify() function is more generic and should be used. def validate(self, server_descriptor): """ Validates our signing key and that the given descriptor content matches its @@ -269,3 +279,127 @@ def validate(self, server_descriptor): verify_key.verify(signature_bytes, descriptor_sha256_digest) except InvalidSignature: raise ValueError('Descriptor Ed25519 certificate signature invalid (Signature was forged or corrupt)') + + def get_signing_key(self): + """ + Get the signing key for this certificate. This is included in the extensions. + WARNING: This is the key that signed the certificate, not the key that got + certified. + + :returns: Raw bytes of an ed25519 key. + + :raises: **ValueError** if the signing key cannot be found. + """ + signing_key_extension = None + + for extension in self.extensions: + if extension.type == ExtensionType.HAS_SIGNING_KEY: + signing_key_extension = extension + break + + if not signing_key_extension: + raise ValueError('Signing key extension could not be found') + + if (len(signing_key_extension.data) != 32): + raise ValueError('Signing key extension has malformed key') + + return signing_key_extension.data + +class MyED25519Certificate(object): + def __init__(self, version, cert_type, expiration_date, + cert_key_type, certified_pub_key, + signing_priv_key, include_signing_key): + """ + :var int version + :var int cert_type + :var CertType cert_type + :var datetime expiration_date + :var int cert_key_type + :var ED25519PublicKey certified_pub_key + :var ED25519PrivateKey signing_priv_key + :var bool include_signing_key + """ + self.version = version + self.cert_type = cert_type + self.expiration_date = expiration_date + self.cert_key_type = cert_key_type + self.certified_pub_key = certified_pub_key + + self.signing_priv_key = signing_priv_key + self.signing_pub_key = signing_priv_key.public_key() + + self.include_signing_key = include_signing_key + # XXX validate params + + def _get_certificate_signature(self, msg_body): + return self.signing_priv_key.sign(msg_body) + + def _get_cert_extensions_bytes(self): + n_extensions = 0 + + # If we need to include the signing key, let's create the extension body + # ExtLength [2 bytes] + # ExtType [1 byte] + # ExtFlags [1 byte] + # ExtData [ExtLength bytes] + if self.include_signing_key: + n_extensions += 1 + + signing_pubkey_bytes = self.signing_pub_key.public_bytes(encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw) + + ext_length = len(signing_pubkey_bytes) + ext_type = 4 + ext_flags = 0 + ext_data = signing_pubkey_bytes + + # Now build the actual byte representation of any extensions + ext_obj = bytes() + ext_obj += n_extensions.to_bytes(1, 'big') + + if self.include_signing_key: + ext_obj += ext_length.to_bytes(2, 'big') + ext_obj += ext_type.to_bytes(1, 'big') + ext_obj += ext_flags.to_bytes(1, 'big') + ext_obj += ext_data + + return ext_obj + + def encode(self): + """Return a bytes representation of this certificate.""" + obj = bytes() + + # Encode VERSION + obj += self.version.to_bytes(1, 'big') + + # Encode CERT_TYPE + try: + cert_type_int = CertType.index_of(self.cert_type) + except ValueError: + raise ValueError("Bad cert type %s" % self.cert_type) + + obj += cert_type_int.to_bytes(1, 'big') + + # Encode EXPIRATION_DATE + expiration_seconds_since_epoch = stem.util.datetime_to_unix(self.expiration_date) + expiration_hours_since_epoch = int(expiration_seconds_since_epoch) // 3600 + obj += expiration_hours_since_epoch.to_bytes(4, 'big') + + # Encode CERT_KEY_TYPE + obj += self.cert_key_type.to_bytes(1, 'big') + + # Encode CERTIFIED_KEY + certified_pub_key_bytes = self.certified_pub_key.public_bytes(encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw) + assert(len(certified_pub_key_bytes) == 32) + obj += certified_pub_key_bytes + + # Encode N_EXTENSIONS and EXTENSIONS + obj += self._get_cert_extensions_bytes() + + # Do the signature on the body we have so far + obj += self._get_certificate_signature(obj) + + return obj + + diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py index 52e1b0b19..468821703 100644 --- a/stem/descriptor/hidden_service.py +++ b/stem/descriptor/hidden_service.py @@ -470,6 +470,9 @@ def _parse_introduction_points(content): return introduction_points +import stem.descriptor.certificate +import stem.descriptor.hsv3_crypto as hsv3_crypto +from cryptography.hazmat.primitives import serialization class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor): """ @@ -509,27 +512,92 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor): } @classmethod - def content(cls, attr = None, exclude = (), sign = False): + def content(cls, attr = None, exclude = (), sign = False, ed25519_private_identity_key = None): + """ + Creates descriptor content with the given attributes. Mandatory fields are + filled with dummy information unless data is supplied. This doesn't yet + create a valid signature. + + .. versionadded:: 1.6.0 + + :param dict attr: keyword/value mappings to be included in the descriptor + :param list exclude: mandatory keywords to exclude from the descriptor, this + results in an invalid descriptor + :param bool sign: includes cryptographic signatures and digests if True + :param ED25519PrivateKey ed25519_private_identity_key: the private identity key of + this onion service + + :returns: **str** with the content of a descriptor + + :raises: + * **ImportError** if cryptography is unavailable and sign is True + * **NotImplementedError** if not implemented for this descriptor type + """ if sign: raise NotImplementedError('Signing of %s not implemented' % cls.__name__) + # We need an private identity key for the onion service to create its + # descriptor. We could make a new one on the spot, but we also need to + # return it to the caller, otherwise the caller will have no way to decode + # the descriptor without knowing the private key or the onion address, so + # for now we consider it a mandatory argument. + if not ed25519_private_identity_key: + raise ValueError('Need to provide a private ed25519 identity key to create a descriptor') + + return _descriptor_content(attr, exclude, ( ('hs-descriptor', '3'), ('descriptor-lifetime', '180'), + # here we need to write a crypto blob ('descriptor-signing-key-cert', _random_crypto_blob('ED25519 CERT')), + # here we need the OEP scheme ('revision-counter', '15'), + # here we need the encrypted blob ('superencrypted', _random_crypto_blob('MESSAGE')), + # here we need an actual signature ('signature', 'wdc7ffr+dPZJ/mIQ1l4WYqNABcmsm6SHW/NL3M3wG7bjjqOJWoPR5TimUXxH52n5Zk0Gc7hl/hz3YYmAx5MvAg'), ), ()) @classmethod def create(cls, attr = None, exclude = (), validate = True, sign = False): + """ + Creates a descriptor with the given attributes. Mandatory fields are filled + with dummy information unless data is supplied. This doesn't yet create a + valid signature. + + .. versionadded:: 1.6.0 + + :param dict attr: keyword/value mappings to be included in the descriptor + :param list exclude: mandatory keywords to exclude from the descriptor, this + results in an invalid descriptor + :param bool validate: checks the validity of the descriptor's content if + **True**, skips these checks otherwise + :param bool sign: includes cryptographic signatures and digests if True + + :returns: :class:`~stem.descriptor.Descriptor` subclass + + :raises: + * **ValueError** if the contents is malformed and validate is True + * **ImportError** if cryptography is unavailable and sign is True + * **NotImplementedError** if not implemented for this descriptor type + """ + # Create a string-representation of the descriptor and then parse it + # immediately to create an object. 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 + + # XXX Do this parsing in its own function if validate: for keyword in REQUIRED_V3_FIELDS: if keyword not in entries: @@ -546,6 +614,42 @@ def __init__(self, raw_contents, validate = False, skip_crypto_validation = Fals else: self._entries = entries + # ATAGAR XXX need to do this cert extraction in the parsing handler + assert(self.signing_cert) + cert_lines = self.signing_cert.split('\n') + assert(cert_lines[0] == '-----BEGIN ED25519 CERT-----' and cert_lines[-1] == '-----END ED25519 CERT-----') + desc_signing_cert = stem.descriptor.certificate.Ed25519Certificate.parse(''.join(cert_lines[1:-1])) + + # crypto validation (check skip_crypto_validation) + # ASN XXX need to verify descriptor signing certificate (for now we trust Tor to do it) + # ASN XXX need to verify descriptor signature (for now we trust Tor to do it) + + plaintext = self.decrypt_descriptor(desc_signing_cert) + + def decrypt_descriptor(self, desc_signing_cert): + # Get crypto material. + # ASN XXX Extract to its own function and assign them to class variables + blinded_key_bytes = desc_signing_cert.get_signing_key() + identity_public_key = hsv3_crypto.decode_address(self.onion_address) + identity_public_key_bytes = identity_public_key.public_bytes(encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw) + assert(len(identity_public_key_bytes) == 32) + assert(len(blinded_key_bytes) == 32) + + subcredential_bytes = hsv3_crypto.get_subcredential(identity_public_key_bytes, blinded_key_bytes) + + ####################################### Do the decryption ################################### + + outter_layer_plaintext = hsv3_crypto.decrypt_outter_layer(self.superencrypted, self.revision_counter, + identity_public_key_bytes, blinded_key_bytes, subcredential_bytes) + + # ATAGAR XXX this parsing function is a hack. need to replace it with some stem parsing. + inner_layer_ciphertext = hsv3_crypto.parse_superencrypted_plaintext(outter_layer_plaintext) + + inner_layer_plaintext = hsv3_crypto.decrypt_inner_layer(inner_layer_ciphertext, self.revision_counter, + identity_public_key_bytes, blinded_key_bytes, subcredential_bytes) + + print(inner_layer_plaintext) # TODO: drop this alias in stem 2.x diff --git a/stem/descriptor/hsv3_crypto.py b/stem/descriptor/hsv3_crypto.py new file mode 100644 index 000000000..de88b7ac4 --- /dev/null +++ b/stem/descriptor/hsv3_crypto.py @@ -0,0 +1,242 @@ +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) + +""" +Blinded key stuff + + Now wrt SRVs, if a client is in the time segment between a new time period + and a new SRV (i.e. the segments drawn with "-") it uses the current SRV, + else if the client is in a time segment between a new SRV and a new time + period (i.e. the segments drawn with "="), it uses the previous SRV. +""" + +pass + +""" +Subcredential: + + subcredential = H("subcredential" | credential | blinded-public-key + credential = H("credential" | public-identity-key) + +Both keys are in bytes +""" +def get_subcredential(public_identity_key, blinded_key): + cred_bytes_constant = "credential".encode() + subcred_bytes_constant = "subcredential".encode() + + credential = hashlib.sha3_256(b"%s%s" % (cred_bytes_constant, public_identity_key)).digest() + subcredential = hashlib.sha3_256(b"%s%s%s" % (subcred_bytes_constant, credential, blinded_key)).digest() + + print("public_identity_key: %s" % (public_identity_key.hex())) + print("credential: %s" % (credential.hex())) + print("blinded_key: %s" % (blinded_key.hex())) + print("subcredential: %s" % (subcredential.hex())) + + print("===") + + return subcredential + +""" +Basic descriptor logic: + + SALT = 16 bytes from H(random), changes each time we rebuld the + descriptor even if the content of the descriptor hasn't changed. + (So that we don't leak whether the intro point list etc. changed) + + secret_input = SECRET_DATA | subcredential | INT_8(revision_counter) + + keys = KDF(secret_input | salt | STRING_CONSTANT, S_KEY_LEN + S_IV_LEN + MAC_KEY_LEN) + + SECRET_KEY = first S_KEY_LEN bytes of keys + SECRET_IV = next S_IV_LEN bytes of keys + MAC_KEY = last MAC_KEY_LEN bytes of keys + + +Layer data: + + 2.5.1.1. First layer encryption logic + SECRET_DATA = blinded-public-key + STRING_CONSTANT = "hsdir-superencrypted-data" + + 2.5.2.1. Second layer encryption keys + SECRET_DATA = blinded-public-key | descriptor_cookie + STRING_CONSTANT = "hsdir-encrypted-data" +""" + +SALT_LEN = 16 +MAC_LEN = 32 + +S_KEY_LEN = 32 +S_IV_LEN = 16 +MAC_KEY_LEN = 32 + +def _ciphertext_mac_is_valid(key, salt, ciphertext, mac): + """ + Instantiate MAC(key=k, message=m) with H(k_len | k | m), where k_len is + htonll(len(k)). + + XXX spec: H(mac_key_len | mac_key | salt_len | salt | encrypted) + """ + # Construct our own MAC first + key_len = len(key).to_bytes(8, 'big') + salt_len = len(salt).to_bytes(8, 'big') + + my_mac_body = b"%s%s%s%s%s" % (key_len, key, salt_len, salt, ciphertext) + my_mac = hashlib.sha3_256(my_mac_body).digest() + + print("===") + print("my mac: %s" % my_mac.hex()) + print("their mac: %s" % mac.hex()) + + # Compare the two MACs + return my_mac == mac + +def _decrypt_descriptor_layer(ciphertext_blob_b64, revision_counter, + public_identity_key, subcredential, + secret_data, string_constant): + # decode the thing + ciphertext_blob = base64.b64decode(ciphertext_blob_b64) + + if (len(ciphertext_blob) < SALT_LEN + MAC_LEN): + raise ValueError("bad encrypted blob") + + salt = ciphertext_blob[:16] + ciphertext = ciphertext_blob[16:-32] + mac = ciphertext_blob[-32:] + + print("encrypted blob lenth :%s" % len(ciphertext_blob)) + print("salt: %s" % salt.hex()) + print("ciphertext length: %s" % len(ciphertext)) + print("mac: %s" % mac.hex()) + print("===") + + # INT_8(revision_counter) + rev_counter_int_8 = revision_counter.to_bytes(8, 'big') + secret_input = b"%s%s%s" % (secret_data, subcredential, rev_counter_int_8) + secret_input = secret_input + + print("secret_data (%d): %s" % (len(secret_data), secret_data.hex())) + print("subcredential (%d): %s" % (len(subcredential), subcredential.hex())) + print("rev counter int 8 (%d): %s" % (len(rev_counter_int_8), rev_counter_int_8.hex())) + print("secret_input (%s): %s" % (len(secret_input), secret_input.hex())) + print("===") + + kdf = hashlib.shake_256(b"%s%s%s" % (secret_input, salt, string_constant)) + keys = kdf.digest(S_KEY_LEN+S_IV_LEN+MAC_KEY_LEN) + + secret_key = keys[:S_KEY_LEN] + secret_iv = keys[S_KEY_LEN:S_KEY_LEN+S_IV_LEN] + mac_key = keys[S_KEY_LEN+S_IV_LEN:] + + print("secret_key: %s" % secret_key.hex()) + print("secret_iv: %s" % secret_iv.hex()) + print("mac_key: %s" % mac_key.hex()) + + # Now time to decrypt descriptor + cipher = Cipher(algorithms.AES(secret_key), modes.CTR(secret_iv), default_backend()) + decryptor = cipher.decryptor() + decrypted = decryptor.update(ciphertext) + decryptor.finalize() + + # validate mac (the mac validates the two fields before the mac) + if not _ciphertext_mac_is_valid(mac_key, salt, ciphertext, mac): + raise ValueError("Bad MAC!!!") + + return decrypted + +def decrypt_outter_layer(superencrypted_blob_b64, revision_counter, + public_identity_key, blinded_key, subcredential): + secret_data = blinded_key + string_constant = b"hsdir-superencrypted-data" + + # XXX Remove the BEGIN MESSSAGE around the thing + superencrypted_blob_b64_lines = superencrypted_blob_b64.split('\n') + assert(superencrypted_blob_b64_lines[0] == '-----BEGIN MESSAGE-----') + assert(superencrypted_blob_b64_lines[-1] == '-----END MESSAGE-----') + superencrypted_blob_b64 = ''.join(superencrypted_blob_b64_lines[1:-1]) + + print("====== Decrypting outter layer =======") + + return _decrypt_descriptor_layer(superencrypted_blob_b64, revision_counter, + public_identity_key, subcredential, + secret_data, string_constant) + +def decrypt_inner_layer(encrypted_blob_b64, revision_counter, + public_identity_key, blinded_key, subcredential): + secret_data = blinded_key + string_constant = b"hsdir-encrypted-data" + + print("====== Decrypting inner layer =======") + + return _decrypt_descriptor_layer(encrypted_blob_b64, revision_counter, + public_identity_key, subcredential, + secret_data, string_constant) + +def parse_superencrypted_plaintext(outter_layer_plaintext): + """Super hacky function to parse the superencrypted plaintext. This will need to be replaced by proper stem code.""" + import re + + START_CONSTANT = b'-----BEGIN MESSAGE-----\n' + END_CONSTANT = b'\n-----END MESSAGE-----' + + start = outter_layer_plaintext.find(START_CONSTANT) + end = outter_layer_plaintext.find(END_CONSTANT) + + start = start + len(START_CONSTANT) + + return outter_layer_plaintext[start:end] + diff --git a/test/unit/descriptor/certificate.py b/test/unit/descriptor/certificate.py index ca0a626ed..5f359f0a7 100644 --- a/test/unit/descriptor/certificate.py +++ b/test/unit/descriptor/certificate.py @@ -15,6 +15,8 @@ from stem.descriptor.certificate import ED25519_SIGNATURE_LENGTH, CertType, ExtensionType, ExtensionFlag, Ed25519Certificate, Ed25519CertificateV1, Ed25519Extension from test.unit.descriptor import get_resource +from cryptography.hazmat.primitives import serialization + ED25519_CERT = """ AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABnprVR ptIr43bWPo2fIzo3uOywfoMrryprpbm4HhCkZMaO064LP+1KNuLvlc8sGG8lTjx1 @@ -193,3 +195,35 @@ def test_validation_with_invalid_descriptor(self): cert = Ed25519Certificate.parse(certificate()) self.assertRaisesWith(ValueError, 'Ed25519KeyCertificate signing key is invalid (Signature was forged or corrupt)', cert.validate, desc) + + @test.require.ed25519_support + def test_encode_decode_certificate(self): + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + + certified_priv_key = Ed25519PrivateKey.generate() + certified_pub_key = certified_priv_key.public_key() + + signing_priv_key = Ed25519PrivateKey.generate() + + expiration_date = datetime.datetime(2037, 8, 28, 17, 0) + + my_ed_cert = stem.descriptor.certificate.MyED25519Certificate(1, CertType.HS_V3_DESC_SIGNING_KEY, expiration_date, + 1, certified_pub_key, + signing_priv_key, + True) + + ed_cert_bytes = my_ed_cert.encode() + self.assertTrue(my_ed_cert) + + # base64 the cert since that's what the parsing func expects + ed_cert_bytes_b64 = base64.b64encode(ed_cert_bytes) + + ed_cert_parsed = stem.descriptor.certificate.Ed25519Certificate.parse(ed_cert_bytes_b64) + + self.assertEqual(ed_cert_parsed.type, my_ed_cert.cert_type) + self.assertEqual(ed_cert_parsed.expiration, my_ed_cert.expiration_date) + self.assertEqual(ed_cert_parsed.key_type, my_ed_cert.cert_key_type) + self.assertEqual(ed_cert_parsed.key, my_ed_cert.certified_pub_key.public_bytes(encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw)) + self.assertEqual(ed_cert_parsed.get_signing_key(), my_ed_cert.signing_pub_key.public_bytes(encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw)) diff --git a/test/unit/descriptor/data/hidden_service_v3_test b/test/unit/descriptor/data/hidden_service_v3_test new file mode 100644 index 000000000..12ddcc699 --- /dev/null +++ b/test/unit/descriptor/data/hidden_service_v3_test @@ -0,0 +1,223 @@ +hs-descriptor 3 +descriptor-lifetime 180 +descriptor-signing-key-cert +-----BEGIN ED25519 CERT----- +AQgABl5/AZLmgPpXVS59SEydKj7bRvvAduVOqQt3u4Tj5tVlfVKhAQAgBABUhpfe +/Wd3p/M74DphsGcIMee/npQ9BTzkzCyTyVmDbykek2EciWaOTCVZJVyiKPErngfW +BDwQZ8rhp05oCqhhY3oFHqG9KS7HGzv9g2v1/PrVJMbkfpwu1YK4b3zIZAk= +-----END ED25519 CERT----- +revision-counter 42 +superencrypted +-----BEGIN MESSAGE----- +Jmu66WXn0+CDLXVM02n85rj84Fv4ynLcjFFWPoLNm6Op+S14CAm0H2qfMj8OO/jw +NJiNxY/L/8SeY5ZlvqPHzI8jBqKW7nT5CN7xLUEvzdFhG3AnWC48r8fp2E+TQ8gb +Fw00gDEIPT8q1nfKHNEnErS03KbW25kPGv8iX5v8XxpkwBFexR1BPEJGi2U7sI18 +SyuJ21hbc03khpj/PilVgNeY359/Aoa6sER5kz46YHR+xpFt7fufybpCcFQQSYkX +fXZ+dHTMpshCSUtbyKJZtO1P9PPaDZ2NpNTGrCXf8T45q/OlnTg2YO4OuBX87HA3 +CT4jyc1dmduXirAucJB2f9DQkvWTIz1dMtE8itjiH2fnuawJMFFXS+prbMU0VAwT +yGIqCPLc6xkChuG/NWS3Pd5UsM/xTmBulDt7SCZ7OMOIHqn1uFbMip/HuGzAo60o +oXgdkPqx1RZnmpfD4xeaH8VdaTKS97xRP5R860PgTaGJ/jyzkKGcpuThsTqbYSel +P7rFVhdZNIyDb6lSGsqvoxuL+E3fz+Nb/vSxXd2B+iQ6gjB81s5wiAnycmhvruz5 +8hLpYGISyYw6XEG3k74+lbbFAmXTViUPV2cL/On6ROgNAix/7L7iREnWfWGuEaIc +4IbH2QPaOTtOMeghZQKsb3StB1fVA640RV0eCw62i7IZ2KQazcb6UnACw0PypBZq +EzE2BsydtMWid3Hk1mFmXt0tY5i7XzXfdxaTQXIH4GVSPt3QJg8JMMP2+6TCghDe +wZjScrfRcwRTDNEVSrHkd09ZFSMJUijtyFWDkY/VW0YxII0lgFBA/gaqfBcTa9D0 +ag81vIQxozq6b1mVVmUcJC8SSw2bHKmjnBlY7EO1CnBGzMxJilR02jfQqqOkUxnv +6PFyzMCTG5iNm/Ob+BS61WseUW5rmgg+2EzwuugAKD5fAZGW9V/fV0nKNJPwW45R +XJsDiaFu1bB2vFclL99DpOkgUMYVCq7jNN7XWBTl3FR6AlPR4x6Y0hOT0vqyEeak +vR1cVTqBdHoV1VeIQvk9FzRL/JRI5cXFUy0zfzBX23bt01FmZqKv2ObnTjkuiV6k +CA1gfWiFQXd59c/roeH5DWbGyfmUn8b8X0hTBsuRBHPrdEDUAco+WFhhA2sAG7yM +7UOxUTrDqc15CZvK55rDRRBE2crSJ/odTCvBXr7eODD+3XcfbhGlq+frYTHN2/z9 +eZm9wiU/ik98R7fbT8RQio0sba6wyFSevbJ3NlI40MdFgqD+S0Dr46FbgLd/6L6r +tGG1mmeXv5U3TiO/99n9s+RXCMnoU/FXdldP1X7qVlelNxlFzMIuy2N6ZPGGm5dX +/TX459ujKpIF9JHhA2+rzSfqy/aV+0mbNJ+/7GK94zMOm1xzg9Koa94Dim5tvvq7 +6vvB/HEfoi3tT24o6eoWBvHE4FB+toMEqVmq6wVTbWInWZtXeJ8mxY04ChkllJ/Q +/SvtJcApd9UCDVNY98w97KYQx1qAl9f67+pkNvjABtwKhTc2x8FSuO51QFzZ6LD+ +ksth/ccyKeS0CrQz0SVGB0lerI14db1nasrQAE3gXfC1a/2hR+LweftowES4dUMB +4zW1CQMP5mK7lsdGrnjUqMjiCku0gPeSQMLIFcfLDMqECQ4X5aM3JS2unA2Lyq1n +JiL/bpa6AvmSvmVKnCAoWYS1HXhqRuj2OQmtNEKtsUvmfb9RJYhvisD408jPYqbj +k1O6ykQiqm6oqmg0bWlJXYzJ4aXa7E9rtI9kAFV27yODXGitT53PCi173kxadONS +/XFsfZoxLVkQdRRUDWpPyVe5xn38rDY8fvoG683xxwbuaOHk+H2Dxcwbwn1RLT9Z +nsgHQ3MW3xcAmfKn1d0cQhHkHaLWP4HBQmiQq9nJtdQYbOmT64EMajSt+FxzPM5M +0joHs/wCCFYsJzAW9UZ1/Y8l7Y18wM76VPLoK43cwqHjwKjKoYL8QzKaBfbdyWz1 +irGrRZLDmQaFj1IiPjni6nis0+yb09bdz7p48p3r+SEej1HO2kuggixolEa4tT0B +az9GMBFN9ydsEN8n+A3xIXHzlg8RNDTa5MYUAW0d8punj5NaDg08yMN5OAoisndy +J4yeNJAdkncRFtcGyX8QyYPnFNybqfmT7epl89Ax9gdHQ1FQhbWbmBthgGgGs80P +0kHZLRGDUEGQkhSS/imKOInINJVgcbcRZTqf9g5gVEy5P3TkVQDEv/KDcE9Sjke7 +CMrtpsiU/O/9so+0ltVtGOrbzQIebCbvnQNbO9i1kniTtidrAlmyaF4bjIDoAvwo +/nih+f/J7w3KdVs4DdFqCRpYIibJC6FQqHpbizliN6PfPtOeEnLDxyrcp7BxSs6i +6otmxeHRbLF2vio6ek2HHGvQegbVLRyQ2gAbuKfaJyOpxFekde70S+HngjBvZ10J +QX8MHBmwlQABzFLe6Exd/Oi5Wlf7XlyJXOjJ3q+YnOx/bQKNhA3JqpfTDJMhciqr +zgMfWVJN3FrJwBMyNh9IjIpUyHCDDof3YOxL9xW0zxn8RlAQgYEUHGmPTIN0qc19 +aWbYdwf4U5Hsw78CVZvTe/UdMRTSa5YbrVViDbYBH1AGO60rtmZ4sqLV7hUe9sIH +fBXlUCaAyYS3BCd0qIdzWMlZD3s8PBWfwTOsGZPGw2iVTjV7IioKGAD8onCnSToT +rmTSJWcMx0P+XIkvsMqf4EWqPeaMWv3eY2JtqaslPIasy4m9h6CCin08fHPl+s80 +f0h6820c5HCuEPRcH9VuRMm3MOdjOG3K+SVdBnovoJxllSp/N3aii2D5vrQgV/rq +fJXx+OL6xNATcJYydFOlN4UkkyHvhEnK2nqmYQFnnzqgS1Glg/ZpheJ/9Ib9ugFI +0qJf/1VZNHaASnAVFBxG67AkMyPotlU++pmFk8tlH0C9lDdsXLZ+TAkhzvmO+4PR +WjRASGli1QiEIQY9AtrNbyV001acR0qJZD7FC3oftYT8GcnzxBbDxhjqDiOTP6TC +cqvSzCy+e59lLh8oNf0aaYs0WoLawva7vxG7TEWPF6xoU/O++SOj9eJXjlIwV5E4 +IklE94jhKZ0Cyi3B7x9H2TYn1qDlvzAr3nYCXFQMP2dO7tWr9ItuZsvMm3KEa6ap +GXvWML6wwRRWZeLPr1nuiU4n60EETY/ZBJFH7Gzwa1/b0qhcIk2coUCcrPdH8CFK +6HuEhxqqBzr3BqijoWbhwsLrZL8WR6ouRRZ8iGgzt5utWDq3nz30PhmSKWZt22ym +nY0MrWS/oMWYkftDQbDSv+76UC8pbvF43vKCGNIHnFBWNi48CPhSgwqSEeXiOFpV ++MjkhaVP7LwHFqmWg+yrZMeWU4GDmGQV6poixIJRzPThBhsFgJNd+UX5e35WjXDg +JBsdUGneT4aEzVi3KrSc7PdJN1f3X3F9lb7K+2IEg3vezxmR2DuGJMWf7QtfEJH5 +1ezAn7B+4cRkKzVxhgeKKjo6Lk6CVVUfHKFz74Dar04BhwkdKvyLRm3vwDDpEYKX +sxqAt1irNyNwbbUC9Ldw+Iyx+I2VgQ6aLHkpvBKYgXsJeXBeuRriHw7Umep8Hwu9 +piWt6b2lHB0qH5P9l0LPK2n64vzpT4S+1vPcsQ91DB+QynssBLSgilPUtyn/0/LO +MtIECHSkgJznFY1l3jJ1OV6snC0mFfuYio4gf7WYMHwWjoZ99rudtsrYpHmJoyq8 +V9Mbl1GRGPlGWGe8lMmU7Wb/ZFsX6GPUsBUOUN0NA6u16Z565dPoxnHWbjoVlMbK +LzI/RZFWOOOc4AIZTCPBNjZ1j5tlRnptG1+O0BCyD6cSOfq/4zHAHsY+fcbgkFQF +hOok2PV44vETV3JH9Oihpr1YeQ3swTWMzh53+IYOxdFx/5i8SjvjULmYTmiKXt1H +Q3dA5eFNVv9y0Et6iimLadMEBf0x9WUsL5aofG10RhDdciO41patUmdwxo+sq+38 +87qP2RPGxybqC3JUvR7zDINzGgCNkwDzcSV2lOdvOT7u08fumYo2PkVhub4o9QpJ +yQlIMxyn/glkayMmBwgOLEwCCEkq39Zi8zhvUnAwsMDNJTd2fUE1DlQQ/V0S4XHf +XrMA9LcJZj4gH1vauNsVdd5zBJ92g4RhEu5w9tAya1LdFCBChWv5JUrSKWB4Uw2P +7q2Dg92MbDJLXj3hsbuKpbreLGFnCoh0hWjp0HTKcKys6Ec5+aslA+6TVo1j01mT +5rXJKQsPbqUmXeh9zfqtFzhPXTHnAZTdIYpJWVaxwMkacNJAgJ6NGg4NR3uc7Xjv +CV64CKk7KZViCiaAogpqveIyCgjkqtWfMkbqRFhCBHGB2Tm1G9m8+yFsllaUjBgo +fm/HFBx7w7ho/2iwv4tmEtrV5QqzYFOcpxSnUzqiMlCKSwqHfFg4Q0ajNIhs7Yqg +XKfeWIg/hs8MKc5h20o3d8e/esgXMoTIMyNZoiA+ACtS8A4N1ve31NEspoANX4n/ +W0ZuRHuT3aa5BERjQFD9xfy7UAU1ZrPSEfIxC0RBXP2MmXJOdIWjW2xiEXDlLnal +pCKQJ3Sy/gUw7iaQ/PnROqKyK9eMsQ7xA5O98FTDC9wonymVU0WymupRyvJTYk4l +LvaHL8YPTnHvjFSy/FltfxIrJBtsNLkq8MrDIJaxrTW+8dwEUH68b6j4ZQTo2Xvm +mkBXojwOG8I1BsMY89/uphia+NW4uSh/ZrsV+lF5Jc9fPDdo7AfM38gY/+45HL5U +wo9kdP9Exff1vQHTFcP48nG097AeZAWbEMn57yWrK28OWFP2xJCG+DfslHaZUL8y +SbDc9l8faLYCRPa07Qd+CTxPqrdRixKygunNPJyZErEBqxw/ftxWictgEoaHHEmU +h5Ge3pJ1+wdZrrs0EOhc0iBEIhxuifUhBxtwH0ohkYYv/14UnULVca48ZeE9OwjI +R10G48Lz5XQbxBMU4xwSlOchyCBagS9PrRtcoMzpv0Dyb1Q11ndtRaj8LKa3hX6u +kkoa3LEORDkXMichUcfOQVSZIXXvyDaXJDbaeZx0tqeRx0ZPBUFNe4Fbu45gXxUj +m/uo1bBzAtLq91uR/txhwNuLrxbv+JwvY3ng/Cs3Y53GK8M6i6/iswELb9eRz7Bt +cm/J+RmfcAMQZeQRDkIRBc8DM2tNnfMDkdaqpvdDUY6jZNQgbKuQpbskJGguI+yJ +sx53hpZfBF+E0YHUV9s9R1Qm8l/4imzgRbAenSCDcYc56YYk1v5CC3MK2VCCwQ9h +ZJp7n3LQIrRCVZb1teCTtwQ2aOc7qLeceaeNTpjayfGBVACeokK8H39xf7Qc5HL3 +fIFADtH3Rj86IjtjfSSo6OuXZlOKyBZMP+7lRC8gVknRxLwTpU4CIPm0R7Qk7LSx +mWKwvqb0sYXpqmBngkHI2N5pInd3yzDq//ZcXLwpB8pxhBTmCkPQGFXhXgrby6+B +b9BiYGoUaS8+5TXVB1EJcvzTv18CLtqv7b8zblrsdeYc81T/w0fl/dnfTaSEOfXa +ggoG5hJpPuM20BdZNVvJDhQNU0ffAW2TKzLtjqa64XyEhKmvnBd9Y3m21gaJBdUp +reDjLHTEyGFX6cyXU8T4dnYcvIq4pkU43xNSSUhpPFA0CDa5hjbpFhWd+4nARPqx +T3q362ERag+MFUdCyo9Ufzn+YbmagEAOdmWmmga8wYIY+xBfOugV4t4DFEFqRVZI +bCSmcGHi19jAkmOWm6Ik8/H5mW8XqsRLiXUvDRAj/f1LH7/tuWQ/+JQmIAEBllIh +IEdVErWlZCm30iOC4d3g05SYIQbLi73zWxtuUHArKoyRm5YWxjh/fphYHtq2gscM +97OX3JvG5C+4KDbojgaPHPcGfC64sL6+T9eImHb7KsV+KLXBn8TUTlS1nvHHC9fp +xsiHo32xCvcA5KUHzYAqzjfs9xd1uxHEcHADko0wfzh6XVvCqTNTcghwJ7IfvWUL +hXrJeKqw6eeqwUISwAsUrYfDfoi89ju73YrlnAMxNAt7MolrjK3hvgqEgVbCWqi1 +8RmPNReNmB0N+7OBxk+Rxm6wv64WfWu1hgSnq3AFhhqe3sX05JUcABBVKAXQ6ZNv +Ax34oYr+NdCP8DnTEDoaqDf7xLjfjdJB8ZQuaSpYFKAMxUNsiwvG424vA0vlYLy+ +UO3RQRYx85HnAIx+lrp2BlN+AMR/b38df6G20bovzuWPK+PH/kMqVDdpfRHL70iK +bT28zdg9KsRWCA8rTs0UkO1yWuByOGX6EzbDaHXmx9qzOzokRZoRNfj/Sxjs4g1l +r60B74TvJqN/ps0OcsZGOF7di55fius8GnHjvFi5qBRbdL/KjCdkDiis4M3t6r6E +e2cki/zm3UwU+fH1M1AemeNm7X8Jo63bryN9TLHvSmXLN7kO3APY5rJZd66ko3Av +s/wjOcqZVedARglSyjPXC0zl5xezEinA0hof7JItcpTEw6GS8NS6cIy906F+0xc9 +bEzrYx5kIQhJ/iM0vZzTz7554w8Fd6FAL0f6gw8+HK26mV+6QnD0Jwb7QwWDBFxF +MBRoWA7/0YrOI5w/To0STU8pIUcIbKBbD4NGS/6LnevwQHgom/N8xDYB9Q9pjLyj +i3O0Gy4zw8Wem+OCzHr9ElOdos4KEVocGwBOQYzNZMQINIeoxoARZ5JklIe6xT1D +jW72Lg9w4Waekwr7Nh/yBN2iatiXiqK5781jjXU36dw9SWqYJTLuDic0u66GQj4u +JDLMfc4oKQ+LPGI/1aUvPMdtvxTG+uE5kQLv6SS3jwuoRws9aiBtM05vgAO1gbtc +S8kTjy1C1QMVvTulMXN2H/Z2m1NX+xfsrbXfl384/wDM2U4sIsFqLbEcddmlj84J +SjbtWuZHBnbEd03Cp6Scaw66AXKIhWkRVwiHFkQKVn8Jj3d9/mEn9jl/ktMgsspD +fP6utlNDv5abZ5Dj/jN/ODHBlZaCxMCVl4xLEq89SYGLgIbxD7+QyOItGr8Y1m5E +TIdKINUExwVw6/Fw169DZWX6JAIiL01PjZtejT3jrqmGhiT/X/XF1K+QwJYRnPFK +9OkwGX6MtgnRzHMxjLqLbsrvfZKcybd1MOBXjNK1tYoEDzrAZJLfViOX1VA1mDqe +TL7GEam59WwuMPmELBICM5fqeiu6/YW/1+B7wesImxP04KXztWRTXZr5hodQlBs3 +w5A89HEmoRLCWyDJNcpaCBP9m6mp4LHDjeFcdkHGDC16P09r0fn+1oRVzjhFue+l +WjShPRFJhUgkh8y+JimAhjpCm+a2O9/pJcMmIyCN39FTUKemS6QVuzZwwa4PYrT5 +r9GJ4mp6n1bBI3snDue2CxgTDk4CtWOXgS0rnDDgNwENTN56H3guCAAqr9mHjLGu +SOr/UxScVBxto4YSCn0z4SbUw2O4nF0IR0bLKJEfpl0WszgRCve7+T0RmLv5klyl +g67tIC6lsEz44DePOpTOZo6blpNnFbmk1Awm8wkb+ycv6uSaiKAsq7KnlBnaRmst +d2dsBOLcMNZEvCrUR6ce6o/aDmB23Tz4N2o+bqlbVlcPChSwOM8TKxKzreUzrrPo +4zp1C8rVEBg3SAOv5LmC2V0GwT0ms85xBQ0zjFoM9SUUcA0WKWOln8C6CyQK0mMv +ZCxzvnfUrtfpGvfAtXhIIDTU6wm22aX6fH0eQ7wicEbuTSEe8WPOZXHLqqqWerwz +ZKZAvUvDLtM+lHql+EhI4jIs3YvCO8pQGtGeSLn9o50M24d6a8zvsv0PNd1pNyxH +HyCJqilc02hbkjUsRiN8JaTaC6uwO3VWHb6SuNcf8MXxTGhri8j3bsn7QdYddSvK +GBUTpsUgecLVIqxfpq3y1T08IMGEYOPypN+3tUfKLn1TXU1iJCyzffFsNxt4CpJS +KUG/ytJFT8XoxQwf5uRQ6pm6OFZKD7AycAaHa/GjDggqNU7YgYAOyrdbCx5XRi6l +yD8hr759L1mOn1b7RGS/d7Z5Vvhnh7m7CuxUvC6/KJNmKJL4PiMGjfFPfcrUTD25 +3ZvHQFsKAe2zi+Iqs5g81m7X24XVm1UsRcdMqPZw8QrGsuneUBzfs5P5snsRkmOu +b5/AC8zVH66GdlIZ0PygrO1e3RE1OqGbRH+QXHEd4v209QDmBlXinnqi1IouxXZK +Cm0UjeR9y1uGlLknJsMIFe7FgFRYUxghZhc+99yBr/u5mNuc4v+8hTYsLtBgoPQ4 +CNz+67dOfqKMDJz8pd2qiF0x6oJuO9EzAkfYfCUJjK4JS4dHI0hw50/UfJShRNsv +rprtj5dGSaVn8HAI4+ugMsD0If1dTMWFOvySmnayOfug5JhlKZk8whHJWz616o8b +qJMbIqXQOFCeznb/WFtq66eXIlac9OPZ1oe5vApKNlkpqBfXHDLXwG9vKD76J/M3 +9JwHUSZRz8KIvYaUkiqWTy8u7o/DwTDaYSzIfvQfG7mCY+lGgrCbpzaHbxLndU1B +RVNbd4GhxlQSzlqzP8awLJH3MWOaw09x9C2gg3cB9gLtwT7OIdjWXc8/O8Otgldq +6/1wuavW+rGG3eMKpS7N5OE6tyUQCndTjDvpthvuaD6aPP3y9cGwe9Ujo1XMAZcx +oW8+gMVxeURzftJm490j1UbfWSW+5XRr0JNI80sqRDaedmJ04MwxMP++TJY9Fswy +F8xS7UFUCookBdcFFf8jHWqYj0hLp3Mq78MJTtGZgOI0Tt7XfVqX1MH5lRPpRWFx +/4CgNf3L5GpUgdiIvCdJSk9J7ivWYeIMzxQ7eLv2EBVdTkosxrTI7bsZEqRm076m +W9cALrsVc/0yuSo/ZPtB6rZ/L3BwRNmIcVMQlVZa3J8cILJDwpZ5xoTGd+qLkbA7 +UzM/uXsuqOsn6AmrgKrG07Dlm9RC4N9jjKuBuA/mvvYp90KaXcHU+4S1ZacfA0Ad +4FtvBDwDMBs+tCcs7olAFXpKci6itL6QWfujaNtuEBmjtdizO7b13fkQI4UWp9Ud +k4wT7Z53mdmgwnI8ufDi+Kn3eKcsjFMPneKa7qx3mM7LJd/sRr3aToZj7uwextDY +BWVuFY8iA1HU33Bp2/DKYfR/UYcxAwNxPxOF9vOk1cqH0r60N0WDaEVuLJa9SVNv +0ikCwjL0LLSv1otSsornwUCgp8QjzRT0XcunaJNVaWmQu50x8ZfNOINBXVg/4XK6 +Kyy3sqfhf0ZdtRwITZ7UiGvNbwOzsrCu5gfJjC/8waVUK61qIAyl3MHNxc/e60L1 +7XHf6iKtlxoPM2oNu7wMQiDYKcczRZe0kLkmBfNz//84U7Bu+uXrMfuG0NKsZJ81 +bxSvj7Kn8PSbY+n2OxuQyGYoRwUDUMQq8zCgdYInP4KX4KAbn83DeydEnPSz5XPW +j21zD5MWXtDiVBgBxPr1C1msGaenem98nu3S50iBxN9M5ZEl473fxjs3WaFofWMZ +5NVd1L5AkJIiMAIHAdHQDIEegoKkbeYUrvLd0vYvpXrsF2+7+hgKqefU0NgCWTzM +glhn4wMP2QrLteA2EcC9IqiCazukHmO7oG476AByxeZj63WrLSNvw5I4NURBo2wR +m3ZFJiTOSN3LYSIU49HT99jT/Y0Wn8qBvRNSr9ywXI+EvNMplerDDkuKjGCYs47l +lUUMcQjsUL9hDSDb0JjGagHtd6O9jw34v27QG9ws+DrvGOvoNKn8auPTfzSUDkZt +ztCLo09b1yT7wq7tT+kZW6DqOtfTbCN8NJRU5tVTJCtCtc0JEE5kK8yGovM96rnR +qVY3zasA7+S87zWXyqhj8MxpHW/ZSJlQzuiolAjZoOaxtq4e8i4p+uQvZcE06g5b +3bXBdw10xlbzInUG5b1YaJFtr1AVOZjQeduzCMBw0CFE7VAL0qA7MAoleMXcMMcN +SEyh5eIhbD6hhmmJ3LMB1j7WXtd7tU5lfrIdPefwSd8rJNMx5Nhhc/Td7+u3ulFZ +DgqMGr8YieaysasTHjTU28e+JRFzVyUAYWsVZXElbvhzkZS3iHATeFRYDMrteDYu +XbosIx8Boyrcc6ow8eh372t2IRyn2Aj0Yfpvw6a+I9iGmD4RjbjHNDWSRGBt1kgU +BNkq5DUapn1NNt4XIKnu4hQcyq0HldEAMVw3/VwFMeSQEpCVnwx6D77kGm65AYZo +d4ESqFw+91UX1dYfyCJxflI3eHhIg/p72BZ7VHWlHk/cMJ8ie6BXBqXpUVAQ4LD+ +78vJeKYBAI+OoC86sPPzykoU+Pjmju0FCVbTxESbwGUrpHHxjQpGEQfOYA4jUSwm +/163aUubZIjTCPKZa+euO2bBJ1diOPOS1+ZvzSzUlRJeFq5P60sROT2JtNdleNRI +ZDulqCreiUZmhGCFk1MQQFESV6E9mNoVdML7mAbj/rPmghTnCryuev8IhP2P5ZA1 +BMOtQO1ZFsUvry//Xk7TBzApdsHtlQvI4CwFxnlYdwoqwXERbKKI6J5J23aIU+k2 +rMwuOg50ALDO1zmkucFU7hwhIYSccVQ3kru+Di/+YsSVU/62WgJwDYAH1ChOJu1/ +lvLpZnce7/+F2jxt0hJHYxPf9lIDvEbSyn6c8/ElAveIrusbMgLk8GqpAnnsXokv +o+zxR48AXM0FsGjQ8WG59WVDUYbe/e86LH7PpNdw27qc1PI5mLw4Fof/S+1IDJm+ +oIAXJ3D0G2+mypD6n4RHX8Nem07lNSAOFRA9rkVxQ9dENs6sBiyetDtHLWeIy52M +XqG9D2vb1jaingrDocLeLW8vEFDTjYB7TP/sjqNeYE+7v5sKA2YZ+V6ws1HkX3Rf +HdMkLrz1B+DCulfjqg9u4G9IfE2t2GVYS97Vx/O/MjIoMTYU2e/5j67+PZvCY8Zf +jtbIfJANX7v5/iLoWbbncyE0j6yNjazkrGLaiA45UmJDqLrev2P9UPRgjQUb+vgM +KrQmCC6/C5z2KPiWVD0FGSOZr4G7HhGByawc6R1xFaXI8CbJ8sSPK5q+iNtR7hDb +FAn/kbMrep8H1syLv0uAl2+uRPWkutdazbBAK1P85/yjfHTOxWOaqYB/TsHA7FdH +Ps3b0QPwUtFsUJ75z4koEnVe0q6CNRMrrJitH6+i1jEmNnUXEq/ZkvA1NXblJGyu +KMK8kOgxlZWPVMkKR+4TrHoKtOQ5hSrpH+e1S1sutWDEAzqW4wc8r3Bjpx4m6Zw8 +uLwkkqUZG2ZYXReAf1JPrzlA4KIiAjlL/hnn/x2owGx77LKGnVrgaGZBtBHTL4Kc +UbOdRa0EPJ5qDQWQKAa71fy7Oj7wcF4roXarPcAvDsxUB4vvmCcecdE3QGf1zaF6 +siXMn26ldevLV9H4v7mehL//smO7FtFHtm/P5Kx+E1IWB4CXyuuC1qk7g9mn2oeq +ixqSOrr2ud0LyETsYrQPiSqys3rww+SQLPSb3tmv/PFmpjFl3C0SaloSpOMggfxy +a56maNVWwuNvKKMX5Hh4jujdq+wWO8Xfac0+ExedUMamJWcjlDoGDnbcsd0vwcYC +SWbZZJIyqXSHfwxkWkAozq8hG+QNI5w/IyUZWg4Srdkhs6yoErqhy/ueF9nvNemO +NUM7y6UsoEu0gvACds6ilkptfauk3Q1buXUEazSTL7oMjGa3HwfVnazx4J2HLTEb +rfWed5Zb5HoVeFIhIE2kXGy9CDJWp+BdC/PUpTCM9UX38dUjFVxJj1Id+YRJ1o0c +PW5FJhcOShCq/UkRHG9/OCsqDISOVMxbfkNOKV1WM6CHA2Y2Dm+Nu9wYLX2pwiBm +uFmv6hHrLKNrEI2FfR+vIOov0OUbQRgCyQ1+e0qNZbq9byMyOm8sxf0Vb+zhg2yv +6ZiQ3nJKZdvZgdHcyA1aR2aQxP3RZ3iIOZa/PjaMdV5cM+7J/pbBDb67jKtdbBDf +WLGRHh8omhqXwAOcQQ7oNiZed5pNg2ZNNZGwguGm2B+l8V4zmgvxuD4i+VeqEO5T +mPfP1AJOeVQ7f6+3dnqfDKM6ySIvzwiIc04hHkfzSgq3sGwmOdyJYxfRJphR/AH1 +j/J0Vzm+pgBswTdZtc7BemGt8s2/4CJp/eIwxT60Yucp9zEKPsedg0TkRFAOMQ22 +0ckKfRW8FQQDWxkRgrrAg+o/QNSF16dyHg7hnKNXjo0gR2f9HvzzLoyFLi/KxTxz +EZxtz4VmwZcA808NxrkM2gomdNuBsp+iK5wsPiJe3CpQSirb1eyhX/dKbHD16ns2 +iDHX9WA3jIidNVvJFl3MzX9La6R6Efi+PCmcK6zlKd54CGxzT7wcFhUYRxcdNCBx +FNqCJIlc9LNgxJ5KLXLCXThSJ8S1K0k0SP6VYQ6N6TObc/d0t4+XpHTZYG9ItMrT +uQxwVbtsLqPxdLOz9WGTtbyhTSiVWe9+174cHUSeMfmT8CVeqEIn5tb8t52Cx3en +oACa2ybPqR2McpVCSNwZkd6P+04JNu7fxZ5mf6M7YDwQiRq/JL6+StcKonhn9mg+ +0j6pIkz8piUYruHj67E647zItvwJf8mcdkCZtlZaAQQYZvUL1FFf/nLZSic6NhUo +A09cnhVibf00QEhPjJ2PZyZ+32V+Oo9tYJJmCC6rpG85i0aYQc9cIv1Yku+bp2jD +y14LioDF7+3C3XG00uganXzM/ZRvDMb0JenHhaB/EQAfW7jYbJkSsT74+IFpL2pQ +oIkiC+f8MnticqXmnyXREBBq3hZaM3s6Nt/TNleX7TUYsHTA7QDluRqHnuAo/kMB +pNF1aKQy+IHEz/duPiGp3nMj1CzXQvP6Ng5s5zF2aCWwMdqAYfhIx2YDUGK/U0I8 +nLCcSVNspS44jRV2fuY8AFMdwRLugrNgGDi/GwlkP545ff03KjKnO/vdPk/lUn9O +ish3lNhvFDFnfjx/TArL0Yl0zdYqYydAxygJMynXyMvv0+MKv25L4uVIPqcUBUFp +HvYuVZ1pFut4505wzKxs2Y1XBwZvQWoXbud7ndBknt8TS3SXbbjWcguJJIfUJMrG +70CFoJPWDggJkMRifwACqgbmTXpfnWNypYu++McymhyYBRXvf2gtnSOmghnq++MD +K8yNYD7bnFsLEedGIopPJYDCVvyZrfR1hcHyNVj5Gjxfmqrowe9SpGfLWAGuKNFf +9NRgx22lW2PHPk/FUEcTlzWAsZuqNGWha2+MPCVDB39i/cb1Uxl2nJKD7J/fEAWM +ck4GXColNtWPwI9JUGV6d1YOEi678gwhDQau4ErcAezncH2kjU9d0OGd4qn9IQNb +o22bew9awlh6PT/jvO6ngYiZY6v5X2m3OMR4KLF2GEZRPIxf+vrBU8r8bueQaDXk +3Fn+19aENaeSYdjGp54m4macm0OyECgWHGq//f8LOrHkRq36rapiaMzbY2G83oHK +cAdbZsQ41m1cj887LP2tJcldWGFWhQ+QstpQaBOURGM9JqmPkuYOmGkvNRUraIum +bqoJQIB8KxcD5uZqa4wtow== +-----END MESSAGE----- +signature aglChCQF+lbzKgyxJJTpYGVShV/GMDRJ4+cRGCp+a2y/yX/tLSh7hzqI7rVZrUoGj74Xr1CLMYO3fXYCS+DPDQ diff --git a/test/unit/descriptor/hidden_service_v3.py b/test/unit/descriptor/hidden_service_v3.py index f64076237..8172dae85 100644 --- a/test/unit/descriptor/hidden_service_v3.py +++ b/test/unit/descriptor/hidden_service_v3.py @@ -31,6 +31,30 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase): + def test_for_decrypt(self): + """ + Parse a test descriptor (used while making the v3 decode function)... + + sltib6sxkuxh2scmtuvd5w2g7pahnzkovefxpo4e4ptnkzl5kkq5h2ad.onion + """ + + with open(get_resource('hidden_service_v3_test'), 'rb') as descriptor_file: + desc = next(stem.descriptor.parse_file(descriptor_file, 'hidden-service-descriptor-3 1.0', validate = True, + onion_address="sltib6sxkuxh2scmtuvd5w2g7pahnzkovefxpo4e4ptnkzl5kkq5h2ad.onion")) + + self.assertEqual(3, desc.version) + self.assertEqual(180, desc.lifetime) + self.assertEqual(42, desc.revision_counter) + + def test_for_encoding(self): + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + private_identity_key = Ed25519PrivateKey.generate() + + desc_string = HiddenServiceDescriptorV3.content(ed25519_private_identity_key=private_identity_key) + + print("") + print(desc_string.decode()) + def test_for_riseup(self): """ Parse riseup's descriptor...