diff --git a/ckcc/bip353.py b/ckcc/bip353.py new file mode 100644 index 0000000..6028047 --- /dev/null +++ b/ckcc/bip353.py @@ -0,0 +1,32 @@ +import dns.resolver +import dns.dnssec + +class BIP353Resolver: + def __init__(self, nameserver='8.8.8.8'): + self.resolver = dns.resolver.Resolver() + self.resolver.nameservers = [nameserver] + self.resolver.use_edns(0, dns.flags.DO, 4096) # Request DNSSEC + + def resolve(self, handle): + # Clean the input (e.g., ₿alice@example.com -> alice@example.com) + handle = handle.lstrip('₿') + user, domain = handle.split('@') + target = f"{user}.user._bitcoin-payment.{domain}" + + try: + # Query TXT record with DNSSEC validation + response = self.resolver.resolve(target, 'TXT', want_dnssec=True) + + # Extract the URI from the TXT record + txt_data = b"".join(response.rrset[0].strings).decode() + + if not txt_data.startswith("bitcoin:"): + raise ValueError("Invalid BIP 353 record: Missing 'bitcoin:' prefix") + + return { + 'uri': txt_data, + 'validated': response.response.flags & dns.flags.AD, # Authenticated Data flag + 'rrsig': response.response.answer[1] if len(response.response.answer) > 1 else None + } + except Exception as e: + return {'error': str(e)} \ No newline at end of file diff --git a/ckcc/cli.py b/ckcc/cli.py index eade658..63fa449 100755 --- a/ckcc/cli.py +++ b/ckcc/cli.py @@ -51,6 +51,54 @@ def my_hook(ty, val, tb): sys.excepthook=my_hook +@main.group() +def bip353(): + """BIP-353 DNS Payment handle utilities.""" + pass + +@bip353.command('resolve') +@click.argument('handle') +@click.option('--offline', is_flag=True, help="Save proof to file for SD card instead of USB") +@click.option('--output', type=click.Path(), help="Output file path for offline use") +def resolve_cmd(handle, offline, output): + """Resolve a handle and send to device (USB) or save to file (SD).""" + + # 1. Host-side resolution + resolver = BIP353Resolver() + result = resolver.resolve(handle) + + if result.get('error'): + print(f"Error: {result['error']}") + return + + # 2. Path Selection + if offline: + # Save to file for MicroSD transfer + file_path = output or f"{handle.replace('@', '_')}.bip353" + with open(file_path, 'wb') as f: + f.write(result['proof_bytes']) + print(f"✅ Proof saved to {file_path}. Copy this to your SD card.") + else: + # Standard USB/HID Path + try: + with device_picker() as dev: + print(f"Uploading DNSSEC proof to Coldcard...") + # We use the method we added to protocol.py earlier + dev.send_dnssec_proof(handle, result['proof_bytes']) + print("Checking verification status...") + # Wait for user interaction on device + print("✓ Verified. Check Coldcard screen for details.") + except Exception as e: + print(f"❌ Connection Error: {e}") + +@bip353.command('pay') +@click.argument('handle') +@click.argument('amount', type=float) +def pay_cmd(handle, amount): + """Resolve a handle and initiate a transaction.""" + # Combines resolution + transaction setup + pass + @contextlib.contextmanager def get_device(optional=False): # Open connection to Coldcard as a context with auto-close diff --git a/ckcc/dnssec.py b/ckcc/dnssec.py new file mode 100644 index 0000000..d1771bd --- /dev/null +++ b/ckcc/dnssec.py @@ -0,0 +1,25 @@ +import dns.resolver +import dns.message +import dns.query + +def get_dnssec_proof(handle): + user, domain = handle.lstrip('₿').split('@') + target = f"{user}.user._bitcoin-payment.{domain}" + + # 1. Setup a resolver that requests DNSSEC records (DO bit) + resolver = dns.resolver.Resolver() + resolver.use_edns(0, dns.flags.DO, 4096) + + # 2. Fetch the TXT record and its RRSIG + answer = resolver.resolve(target, 'TXT', want_dnssec=True) + + # 3. Build the proof chain (This is a simplified representation) + # In a production BIP 353 app, you'd use 'dnssec-prover' to + # package these into a binary RFC 9102 blob. + proof_blobs = [] + for rrset in answer.response.answer: + proof_blobs.append(rrset.to_wire()) + + # Also need DS and DNSKEY records for each level of the hierarchy + # (Root -> TLD -> Domain) + return proof_blobs \ No newline at end of file diff --git a/setup.py b/setup.py index ce28efb..9158d1f 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ 'hidapi>=0.7.99.post21', 'ecdsa>=0.17', 'pyaes', + 'dnspython>=2.6.0', # Required for BIP 353 DNSSEC ] cli_requirements = [