make .onion domains declarative, site updates, add helpers

This commit is contained in:
root 2025-11-07 03:09:13 +00:00
parent b3d2a34bc0
commit 450a5ce1d6
25 changed files with 341 additions and 109 deletions

79
helpers/services.nix Normal file
View file

@ -0,0 +1,79 @@
{
config,
lib,
...
}: let
cfg = config.distrust.services;
in {
options = {
distrust = {
services = lib.mkOption {
description = "Services configuration map";
type = lib.types.attrsOf (
lib.types.submodule {
options = {
url = lib.mkOption {
description = "Clearnet URL";
type = lib.types.str;
};
onion = lib.mkOption {
description = "Onion service settings";
type = lib.types.submodule {
options = {
url = lib.mkOption {
description = ".onion URL";
type = lib.types.str;
};
secretKey = lib.mkOption {
description = "Path to onion secret key file";
type = lib.types.path;
};
};
};
default = {};
};
virtualHostConfig = lib.mkOption {
description = "Caddy virtual host config";
type = lib.types.str;
};
};
}
);
default = {};
};
};
};
config = {
services.tor.relay.onionServices =
builtins.foldl'
(acc: key:
acc
// {
"${key}" = {
map = [80];
inherit (cfg.${key}.onion) secretKey;
};
})
{}
(builtins.attrNames cfg);
services.caddy = {
enable = true;
virtualHosts = builtins.foldl' (acc: key: let
site = cfg.${key};
vhostKey = "${site.url} ${site.onion.url}";
extraCfg = ''
${site.virtualHostConfig or ""}
header Onion-Location ${site.onion.url}
'';
in
acc
// {
"${vhostKey}" = {
extraConfig = extraCfg;
};
}) {} (builtins.attrNames cfg);
};
};
}

84
helpers/tor-hostname.nix Normal file
View file

@ -0,0 +1,84 @@
{pkgs ? import <nixpkgs> {}}:
pkgs.python3Packages.buildPythonApplication rec {
pname = "tor-hostname";
version = "1.0.0";
format = "other";
propagatedBuildInputs = with pkgs.python3Packages; [
pynacl
cryptography
];
dontUnpack = true;
installPhase = ''
mkdir -p $out/bin
cat > $out/bin/tor-hostname <<'EOF'
#!/usr/bin/env python3
"""Convert Tor v3 secret key to .onion hostname"""
import hashlib
import base64
import sys
def onion_address_from_public_key(public_key):
"""Generate .onion address from ed25519 public key"""
version = b'\x03'
checksum = hashlib.sha3_256(b'.onion checksum' + public_key + version).digest()[:2]
onion_address = base64.b32encode(public_key + checksum + version).decode().lower()
return f"{onion_address}.onion"
def read_tor_secret_key(filepath):
"""Read Tor hs_ed25519_secret_key file"""
with open(filepath, 'rb') as f:
content = f.read()
header = b'== ed25519v1-secret: type0 ==\x00\x00\x00'
if not content.startswith(header):
raise ValueError("Invalid Tor secret key file format")
expanded_key = content[32:96]
return expanded_key
def derive_public_from_expanded_secret(expanded_key):
"""Derive ed25519 public key from 64-byte expanded secret key"""
from nacl.bindings import crypto_scalarmult_ed25519_base_noclamp
secret_scalar = expanded_key[:32]
public_key = crypto_scalarmult_ed25519_base_noclamp(secret_scalar)
return public_key
def main():
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <path_to_hs_ed25519_secret_key>")
sys.exit(1)
secret_key_path = sys.argv[1]
try:
expanded_key = read_tor_secret_key(secret_key_path)
public_key = derive_public_from_expanded_secret(expanded_key)
hostname = onion_address_from_public_key(public_key)
print(hostname)
except FileNotFoundError:
print(f"Error: File not found: {secret_key_path}")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == '__main__':
main()
EOF
chmod +x $out/bin/tor-hostname
'';
meta = with pkgs.lib; {
description = "Convert Tor v3 secret key to .onion hostname";
license = licenses.mit;
platforms = platforms.unix;
};
}