I'm making an onion (v3) address generator in Python. Here is the part that generates the keypair:

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import x25519

def v3KeyGen():
    private_key = x25519.X25519PrivateKey.generate()
    private_bytes = private_key.private_bytes(encoding=serialization.Encoding.Raw, format=serialization.PrivateFormat.Raw, encryption_algorithm=serialization.NoEncryption())

    public_key = private_key.public_key()
    public_bytes = public_key.public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw)

    return public_bytes, private_bytes

What file format do I put the keys in (e.g. the format of the hs_ed25519_secret_key file), to get them to actually work with something like a Tor hidden service? Right now I have these keys, but have no idea how I actually get Tor to use them.

Edit: This is what an example private key file generated by Tor itself looks like, with some sort of binary data, I have no idea how to interpret it:

enter image description here

1 Answer 1


The data you've displayed on a screenshot is a binary displayed as a char stream, you need to put it in the right format. I have a good example on Go - here is the code, I'm sure you'll spot the format specs out of it :

// Example of generating Tor onion using the new ED25519-v3 format

package main

import (

// Hidden service version
const version = byte(0x03)

// Salt used to create checkdigits
const salt = ".onion checksum"

func main() {
    pub, pri, err := ed25519.GenerateKey(rand.Reader)
    if err != nil {
    fmt.Println("Address:", getServiceID(pub)+".onion")
    fmt.Println("Private Key:", expandKey(pri))

// Expand ed25519.PrivateKey to (a || RH) form, return base64
func expandKey(pri ed25519.PrivateKey) string {
    h := sha512.Sum512(pri[:32])
    // Set bits so that h[:32] is private scalar "a"
    h[0] &= 248
    h[31] &= 127
    h[31] |= 64
    // Since h[32:] is RH, h is now (a || RH)
    return base64.StdEncoding.EncodeToString(h[:])

func getCheckdigits(pub ed25519.PublicKey) []byte {
    // Calculate checksum sha3(".onion checksum" || publicKey || version)
    checkstr := []byte(salt)
    checkstr = append(checkstr, pub...)
    checkstr = append(checkstr, version)
    checksum := sha3.Sum256(checkstr)
    return checksum[:2]

func getServiceID(pub ed25519.PublicKey) string {
    // Construct onion address base32(publicKey || checkdigits || version)
    checkdigits := getCheckdigits(pub)
    combined := pub[:]
    combined = append(combined, checkdigits...)
    combined = append(combined, version)
    serviceID := base32.StdEncoding.EncodeToString(combined)
    return strings.ToLower(serviceID)

The source credit is https://gist.github.com/wybiral/8f737644fc140c97b6b26c13b1409837 - I used it myself when I needed to do exactly the same thing

  • Thanks for that, I know how to generate the address, just not sure how to put the key into a correct file that is valid for a Tor hidden service. Is the expandKey function what I'm looking for? I'm not familiar with Go, so I have no idea what the &= and |= operators mean
    – scob_
    Sep 18, 2021 at 9:16
  • Basically, where does the == ed25519v1-secret: type0 == come from, because I don't see it in that code either
    – scob_
    Sep 18, 2021 at 9:39
  • where have you found it? I don't see it in the code
    – Alexey Vesnin
    Sep 18, 2021 at 10:35
  • In the example private key file I included at the bottom of the question that was generated by Tor. I'm asking how to put the keys I generate into a file like that, so that Tor can actually use them
    – scob_
    Sep 18, 2021 at 21:44
  • you have to generate all the stuff you use by hand. That's - actually - how the Scallion tool works
    – Alexey Vesnin
    Sep 20, 2021 at 21:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.