Tor v2 addresses were generated by a base32 string of the first 80 bits of the SHA1 hash of the private key (sic - public key)

How can I generate Tor v3 addresses? (basic command line tools preferred, openssl, ssh-keygen, base32 and so on)

Please add sample key.pub, (full, not first 2 bytes of) checksum, and resulting .onion address.


I have found a sample (encoded in hex).


3d3d 2065 6432 3535 3139 7631 2d70 7562
6c69 633a 2074 7970 6530 203d 3d00 0000
0044 6619 5537 af38 e2c6 7ac2 d8f6 c13f
958f 7e63 41d0 60e2 727f ca7b a285 722b


3d3d 2065 6432 3535 3139 7631 2d73 6563
7265 743a 2074 7970 6530 203d 3d00 0000
40ff f076 1f62 2657 85e9 0ec8 a2f9 7c52
1a6f 97ab bc20 2bd8 13e6 7442 7d67 506c
27aa 4c9b 935a 15c9 891d f2b0 e2a2 8acd
68fc 8bc8 a483 2829 7f93 9de4 f714 cb59

Resulting address
(do not use this address - the private key is now public)


3 Answers 3


https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt#n2013 (links to the correct line as of today. If you're from the future, you might need to ctrl-f for "ONIONADDRESS")

The onion address of a hidden service includes its identity public key, a version field and a basic checksum. All this information is then base32 encoded as shown below:

 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

Here are a few example addresses:


For more information about this encoding, please see our discussion thread at [ONIONADDRESS-REFS].

  • Truncate from left or right? Say hash is 123abc. Does this truncate to 12 or bc?
    – pnujhpdo
    Commented Apr 26, 2018 at 23:39
  • cat my_key.pub will not work, no. Look at the contents of the file. It isn't just the raw public key. The hash function is sha3_256. The truncation only keeps the first two bytes. You are not going to have a fun time trying to do this with a bash script. You should use a language that makes manipulating bytes easy.
    – pastly
    Commented Apr 27, 2018 at 12:44
  • if one wonders what the pipe (|) in the specs means, see this
    – Mr.
    Commented Jul 6, 2019 at 13:40
  • 1
    What is the H() function used to create the checksum? Commented Sep 21, 2019 at 20:41

So it seems the public key is only 32 bytes long, but in your example it is 64 bytes long. I coded the following script, given the full public key of 64 bytes and got the sample address you put. So take it as a reference!

from base64 import b32encode
from hashlib import sha3_256

def main():
    # Expected result: abcgmgkvg6xtrywgplbnr5wbh6ky67tdihigbytsp7fhxiufoiv6w3yd.onion
    hex_string = "3d3d 2065 6432 3535 3139 7631 2d70 7562 6c69 633a 2074 7970 6530 203d 3d00 0000" + " 0044 6619 5537 af38 e2c6 7ac2 d8f6 c13f 958f 7e63 41d0 60e2 727f ca7b a285 722b"
    pub = bytearray.fromhex(hex_string)[32:]
    version = b"\x03"
    checksum = sha3_256(bytes(".onion checksum", "utf-8") + pub + version).digest()[:2]
    address = b32encode(pub + checksum + version).decode().lower()
    print(address + ".onion")

if __name__ == "__main__":

Here a shell script to generate an onion domain with openssl-tools:

# generate an Ed25519 private key if it's not exists yet
[ ! -f test25519.pem ] && openssl genpkey -algorithm ed25519 -outform PEM -out test25519.pem

# OpenSSL does not support outputting only the raw key from the command line.
# You can get it in SubjectPublicKeyInfo format.
# For Ed25519 key it always consist of 12 bytes of ASN.1 header followed by 32 bytes of raw key.
# Therefore to get a raw public key we'll skip the the first 12 bytes

pub=$(openssl pkey -outform DER -pubout -in test25519.pem | tail -c +13)
# SHA3-256(".onion checksum" | PUBKEY | VERSION)[:2]
checksum=$(printf ".onion checksum%s\x03" "$pub" | openssl dgst -sha3-256 -binary | head -c 2)
# onion_address = base32(PUBKEY | CHECKSUM | VERSION) + ".onion"
onion_domain=$(printf "%s%s\x03" "$pub" "$checksum" | base32 | tr A-Z a-z)
echo "$onion_domain.onion"

mkdir ./hs
echo "$onion_domain.onion" > ./hs/hostname

key_raw=$(openssl pkey -outform DER -in test25519.pem | tail -c +13)
printf "== ed25519v1-secret: type0 ==\x00\x00\x00%s" "$key_raw" > ./hs/hs_ed25519_secret_key

The generated hs folder can be used as an onion service. The public key will be created by a Tor itself on start. The hostname is also not necessary.

Please note that here we don't have a key expansion but still this key is working.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .