v0.0.48 - Added OTP one-time pad encryption (otp_encrypt/otp_decrypt verbs), HTTP listener mode (--listen http:HOST:PORT), interactive OTP pad auto-scan on USB drives, raised SERVER_MAX_MSG_SIZE to 16MB, updated README with curl examples and current API documentation
This commit is contained in:
@@ -8,7 +8,7 @@ import time
|
||||
from coincurve import PrivateKey
|
||||
|
||||
HOST = "npub15uqyclnr3er7r8uhka7f0ae2yt4gkjat8gxdan04q0e6xrnwmtjswcyla3.fips"
|
||||
PORT = 8080
|
||||
PORT = 11111
|
||||
|
||||
# Demo caller key (32 bytes). Replace with your stable caller key in real use.
|
||||
PRIVKEY = bytes(range(1, 33))
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
* bech32 npub for each.
|
||||
*
|
||||
* This is a cross-qube test client for Qubes OS: the signer runs in the
|
||||
* nostr_signer qube listening on tcp:[::]:8080, and this client runs in
|
||||
* nostr_signer qube listening on tcp:[::]:11111, and this client runs in
|
||||
* a different qube connecting to the signer's FIPS address.
|
||||
*
|
||||
* Usage:
|
||||
* ./get_pubkey_tcp <host> <port>
|
||||
* ./get_pubkey_tcp npub1xxx...fips 8080
|
||||
* ./get_pubkey_tcp npub1xxx...fips 11111
|
||||
*
|
||||
* If no arguments are given, defaults to localhost:8080.
|
||||
* If no arguments are given, defaults to localhost:11111.
|
||||
*
|
||||
* Output: for each index, prints:
|
||||
* index 0: hex=<64 hex chars> npub=npub1...
|
||||
@@ -139,7 +139,7 @@ cleanup:
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *host = "127.0.0.1";
|
||||
int port = 8080;
|
||||
int port = 11111;
|
||||
char hex0[65], npub0[128];
|
||||
char hex1[65], npub1[128];
|
||||
int failures = 0;
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
* and bech32 npub for each.
|
||||
*
|
||||
* This is a cross-qube test client for Qubes OS: the signer runs in the
|
||||
* nostr_signer qube listening on tcp:[::]:8080, and this client runs in
|
||||
* nostr_signer qube listening on tcp:[::]:11111, and this client runs in
|
||||
* a different qube connecting to the signer's FIPS address.
|
||||
*
|
||||
* Usage:
|
||||
* node n_signer_qube_example.js [host] [port]
|
||||
* node n_signer_qube_example.js fd56:d7c3:f605:719d:15b:18a0:fb06:982f 8080
|
||||
* node n_signer_qube_example.js fd56:d7c3:f605:719d:15b:18a0:fb06:982f 11111
|
||||
*
|
||||
* If no arguments are given, defaults to localhost:8080.
|
||||
* If no arguments are given, defaults to localhost:11111.
|
||||
*
|
||||
* Protocol:
|
||||
* - 4-byte big-endian length prefix + JSON payload (TCP framing)
|
||||
@@ -214,7 +214,7 @@ function hexToNpub(pubkeyHex) {
|
||||
|
||||
async function main() {
|
||||
const host = process.argv[2] || "127.0.0.1";
|
||||
const port = parseInt(process.argv[3] || "8080", 10);
|
||||
const port = parseInt(process.argv[3] || "11111", 10);
|
||||
|
||||
console.log(`Connecting to n_signer at ${host}:${port}`);
|
||||
console.log("Querying get_public_key for nostr_index 0 and 1...\n");
|
||||
|
||||
219
examples/otp_nostr_30078.py
Normal file
219
examples/otp_nostr_30078.py
Normal file
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
otp_nostr_30078.py — example: encrypt data with OTP, wrap in a Nostr kind 30078
|
||||
event, sign it with n_signer, and print the signed event for publishing.
|
||||
|
||||
Workflow:
|
||||
1. Call n_signer's `otp_encrypt` verb to encrypt plaintext with the bound OTP pad.
|
||||
2. Build a Nostr kind 30078 (replaceable parameterized) event with the ASCII-armored
|
||||
ciphertext as the `content` field.
|
||||
3. Call n_signer's `sign_event` verb to sign the event with the secp256k1 key.
|
||||
4. Print the signed event JSON, ready to publish to Nostr relays.
|
||||
|
||||
This is a demo — it does not actually publish to a relay. To publish, send the
|
||||
signed event to your preferred Nostr relay using a library like nostr-tools,
|
||||
nostril, or nak.
|
||||
|
||||
Usage:
|
||||
python3 examples/otp_nostr_30078.py "Your secret message here"
|
||||
|
||||
Requirements:
|
||||
- n_signer running with --otp-pad-dir / --otp-pad bound, and a secp256k1
|
||||
role (e.g. "main") available for sign_event.
|
||||
- This script connects to n_signer via stdio (one process per request).
|
||||
|
||||
See plans/otp_nostr_integration.md for the full design.
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
NSIGNER = "./build/nsigner"
|
||||
PAD_DIR = "/media/user/Music/pads"
|
||||
PAD_SPEC = "333e9902db839d9d"
|
||||
MNEMONIC_FILE = ".test_mnemonic"
|
||||
MNEMONIC_TMP = ".test_mnemonic_otp_30078.tmp"
|
||||
|
||||
|
||||
def send_framed(proc, obj):
|
||||
payload = json.dumps(obj).encode()
|
||||
proc.stdin.write(struct.pack(">I", len(payload)))
|
||||
proc.stdin.write(payload)
|
||||
proc.stdin.flush()
|
||||
|
||||
|
||||
def recv_framed(proc):
|
||||
"""Read a framed response, skipping any banner text on stdout."""
|
||||
buf = b""
|
||||
while True:
|
||||
b = proc.stdout.read(1)
|
||||
if not b:
|
||||
return None
|
||||
buf = (buf + b)[-4:]
|
||||
if len(buf) < 4:
|
||||
continue
|
||||
(length,) = struct.unpack(">I", buf)
|
||||
if 1 <= length <= 1024 * 1024:
|
||||
peek = proc.stdout.read(1)
|
||||
if peek == b"{":
|
||||
body = peek + proc.stdout.read(length - 1)
|
||||
return json.loads(body.decode())
|
||||
else:
|
||||
buf = (buf + peek)[-4:]
|
||||
|
||||
|
||||
def run_one_request(req_obj):
|
||||
"""Run nsigner in stdio mode for a single framed request/response."""
|
||||
shell_cmd = (
|
||||
f"exec 3<{MNEMONIC_TMP}; "
|
||||
f"exec {NSIGNER} --listen stdio --mnemonic-fd 3 "
|
||||
f"--otp-pad-dir {PAD_DIR} --otp-pad {PAD_SPEC} "
|
||||
f"--otp-allow-blkback --allow-all"
|
||||
)
|
||||
proc = subprocess.Popen(
|
||||
["bash", "-c", shell_cmd],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
)
|
||||
import time as _time
|
||||
_time.sleep(1.0)
|
||||
if proc.poll() is not None:
|
||||
err = proc.stderr.read().decode()
|
||||
print(f"ERROR: nsigner exited early (code {proc.returncode})")
|
||||
print(f"stderr: {err}")
|
||||
return None
|
||||
send_framed(proc, req_obj)
|
||||
resp = recv_framed(proc)
|
||||
proc.stdin.close()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
return resp
|
||||
|
||||
|
||||
def compute_event_id(event):
|
||||
"""Compute the Nostr event ID (SHA-256 of the canonical serialized event)."""
|
||||
# Nostr event serialization: [0, pubkey, created_at, kind, tags, content]
|
||||
serialized = json.dumps([
|
||||
0,
|
||||
event["pubkey"],
|
||||
event["created_at"],
|
||||
event["kind"],
|
||||
event["tags"],
|
||||
event["content"],
|
||||
], separators=(",", ":"), ensure_ascii=False)
|
||||
return hashlib.sha256(serialized.encode()).hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
plaintext = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "Secret OTP message"
|
||||
print(f"Plaintext: {plaintext}")
|
||||
|
||||
# Prepare the mnemonic temp file.
|
||||
with open(MNEMONIC_FILE) as f:
|
||||
mnemonic = f.read().strip()
|
||||
with open(MNEMONIC_TMP, "w") as f:
|
||||
f.write(mnemonic + "\n")
|
||||
|
||||
try:
|
||||
# Step 1: Get the public key for the "main" role
|
||||
print("\n=== Step 1: get_public_key ===")
|
||||
resp = run_one_request({
|
||||
"id": "1",
|
||||
"method": "get_public_key",
|
||||
"params": [{"role": "main"}],
|
||||
})
|
||||
if resp is None or "result" not in resp:
|
||||
print("ERROR: get_public_key failed")
|
||||
print(f"Response: {resp}")
|
||||
return 1
|
||||
# The result is a plain hex string for secp256k1 backward compat.
|
||||
pubkey_hex = resp["result"].strip('"')
|
||||
print(f"Public key: {pubkey_hex}")
|
||||
|
||||
# Step 2: Encrypt the plaintext with OTP
|
||||
print("\n=== Step 2: otp_encrypt ===")
|
||||
pt_b64 = base64.b64encode(plaintext.encode()).decode()
|
||||
resp = run_one_request({
|
||||
"id": "2",
|
||||
"method": "otp_encrypt",
|
||||
"params": [pt_b64, {"encoding": "ascii"}],
|
||||
})
|
||||
if resp is None or "result" not in resp:
|
||||
print("ERROR: otp_encrypt failed")
|
||||
print(f"Response: {resp}")
|
||||
return 1
|
||||
enc_result = json.loads(resp["result"])
|
||||
ciphertext = enc_result["ciphertext"]
|
||||
pad_chksum = enc_result["pad_chksum"]
|
||||
pad_offset = enc_result["pad_offset_after"]
|
||||
print(f"Pad checksum: {pad_chksum}")
|
||||
print(f"Pad offset after encrypt: {pad_offset}")
|
||||
print(f"Ciphertext (first 60 chars): {ciphertext[:60]}...")
|
||||
|
||||
# Step 3: Build the Nostr kind 30078 event
|
||||
print("\n=== Step 3: Build kind 30078 event ===")
|
||||
# Use a unique d-tag based on the pad checksum and offset.
|
||||
d_tag = f"otp-{pad_chksum[:16]}-{pad_offset}"
|
||||
event = {
|
||||
"pubkey": pubkey_hex,
|
||||
"created_at": int(time.time()),
|
||||
"kind": 30078,
|
||||
"tags": [
|
||||
["d", d_tag],
|
||||
["otp-pad", pad_chksum[:16]],
|
||||
["otp-version", "v0.0.2-otp"],
|
||||
["otp-encoding", "ascii"],
|
||||
],
|
||||
"content": ciphertext,
|
||||
}
|
||||
# Compute the event ID.
|
||||
event_id = compute_event_id(event)
|
||||
event["id"] = event_id
|
||||
print(f"Event ID: {event_id}")
|
||||
print(f"d-tag: {d_tag}")
|
||||
|
||||
# Step 4: Sign the event with n_signer
|
||||
print("\n=== Step 4: sign_event ===")
|
||||
# sign_event expects the event JSON as the first param (without id/sig).
|
||||
# The signer computes the id and signature internally.
|
||||
event_for_signing = {
|
||||
"pubkey": event["pubkey"],
|
||||
"created_at": event["created_at"],
|
||||
"kind": event["kind"],
|
||||
"tags": event["tags"],
|
||||
"content": event["content"],
|
||||
}
|
||||
resp = run_one_request({
|
||||
"id": "3",
|
||||
"method": "sign_event",
|
||||
"params": [json.dumps(event_for_signing), {"role": "main"}],
|
||||
})
|
||||
if resp is None or "result" not in resp:
|
||||
print("ERROR: sign_event failed")
|
||||
print(f"Response: {resp}")
|
||||
return 1
|
||||
sig = resp["result"].strip('"')
|
||||
event["sig"] = sig
|
||||
print(f"Signature: {sig[:60]}...")
|
||||
|
||||
# Step 5: Print the signed event
|
||||
print("\n=== Signed Nostr event (ready to publish) ===")
|
||||
print(json.dumps(event, indent=2))
|
||||
print(f"\nTo publish: send this event to a Nostr relay.")
|
||||
print(f"To decrypt: call otp_decrypt with the content field.")
|
||||
return 0
|
||||
finally:
|
||||
try:
|
||||
os.unlink(MNEMONIC_TMP)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user