Last active
August 11, 2025 17:34
-
-
Save lsd-cat/d3bd05412d83628be7145462081462f9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # ctor -> arti onion service discovery key conversion script | |
| # Usage: ./hss_ctor_to_arti.py <base32 secret> > /path/to/arti/keystore/client/<hsid>/ks_hsc_desc_enc.x25519_private | |
| # <base32 secret> is the last part in the ctor client auth file | |
| # <hsid> is the Onion Service hostname without .onion | |
| # Any extra trailing space or newline at the end of the file will break the format | |
| # <hsid> folder must be chmod 700 | |
| # ks_hsc_desc_enc.x25519_private must be chmod 600 | |
| import base64, os, struct, sys | |
| from cryptography.hazmat.primitives.asymmetric import x25519 | |
| from cryptography.hazmat.primitives import serialization | |
| SEED_VALUE = sys.argv[1] | |
| COMMENT = "" # optional comment stored inside the key | |
| v = SEED_VALUE.strip().upper() | |
| v += "=" * ((8 - (len(v) % 8)) % 8) # missing paddong | |
| seed = base64.b32decode(v, casefold=True) | |
| # Clamp per RFC7748 §5 (X25519) | |
| k = bytearray(seed) | |
| k[0] &= 248 | |
| k[31] &= 127 | |
| k[31] |= 64 | |
| k = bytes(k) | |
| # Derive public key | |
| try: | |
| pub = x25519.X25519PrivateKey.from_private_bytes(k).public_key().public_bytes( | |
| encoding=serialization.Encoding.Raw, | |
| format=serialization.PublicFormat.Raw | |
| ) | |
| except Exception as e: | |
| raise SystemExit(f"Error importing key: {e}") | |
| ALG = b"x25519@spec.torproject.org" | |
| def ssh_string(b: bytes) -> bytes: | |
| return struct.pack(">I", len(b)) + b | |
| # Build the outer OpenSSH header | |
| magic = b"openssh-key-v1\0" | |
| ciphername = b"none" | |
| kdfname = b"none" | |
| kdfopts = b"" | |
| nkeys = 1 | |
| outer = [ | |
| magic, | |
| ssh_string(ciphername), | |
| ssh_string(kdfname), | |
| ssh_string(kdfopts), | |
| struct.pack(">I", nkeys), | |
| ] | |
| # Public key section (one key): string( keyblob ), where keyblob = string(name) + string(pubdata) | |
| pubkey_blob = ssh_string(ALG) + ssh_string(pub) # pubdata is a string-wrapped 32 bytes | |
| outer.append(ssh_string(pubkey_blob)) | |
| # Private section (unencrypted blob) | |
| check = os.urandom(4) | |
| priv_payload = bytearray() | |
| priv_payload += check + check # checkint1, checkint2 | |
| # One key: | |
| priv_payload += ssh_string(ALG) | |
| priv_payload += ssh_string(pub) # public key data (string-wrapped 32 bytes) | |
| priv_payload += ssh_string(k) # private key data (string-wrapped 32-byte scalar) | |
| priv_payload += ssh_string(COMMENT.encode()) | |
| # Padding: 1..n so total is multiple of 8 bytes | |
| block = 8 | |
| pad_len = (-len(priv_payload)) % block | |
| if pad_len: | |
| priv_payload += bytes((i % 256 for i in range(1, pad_len + 1))) | |
| outer.append(ssh_string(bytes(priv_payload))) | |
| key_blob = b"".join(outer) | |
| # Write PEM-like OpenSSH key | |
| b64 = base64.b64encode(key_blob).decode() | |
| lines = [b64[i:i+70] for i in range(0, len(b64), 70)] | |
| print("-----BEGIN OPENSSH PRIVATE KEY-----") | |
| print("\n".join(lines)) | |
| print("-----END OPENSSH PRIVATE KEY-----")% (.venv) g@Giulios-MacBook-Air-2 Work % |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This seems to be working well. TYSM!