├── python ├── requirements.txt ├── example.tlv ├── Makefile ├── addressproof.csv ├── preamble.py ├── test_bolt12address.py ├── generated.py └── bolt12address.py ├── shell └── make-addressproof.sh └── README.md /python/requirements.txt: -------------------------------------------------------------------------------- 1 | bolt12>=0.1.3 2 | -------------------------------------------------------------------------------- /python/example.tlv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyrussell/bolt12address/HEAD/python/example.tlv -------------------------------------------------------------------------------- /python/Makefile: -------------------------------------------------------------------------------- 1 | #! /usr/bin/make 2 | 3 | BOLTDIR=../../lightning-rfc 4 | BOLT12DIR= ../../bolt12 5 | 6 | generated.py: addressproof.csv preamble.py 7 | DIR=`pwd`; cd $(BOLT12DIR)/tools && ./generate-code.py --language=py --preamble=$$DIR/preamble.py --spec=$$DIR/$< addressproof > $$DIR/$@ 8 | 9 | addressproof.csv: ../README.md 10 | $(BOLTDIR)/tools/extract-formats.py $< > $@ 11 | 12 | -------------------------------------------------------------------------------- /python/addressproof.csv: -------------------------------------------------------------------------------- 1 | tlvtype,addressproof,chains,2 2 | tlvdata,addressproof,chains,chains,chain_hash,... 3 | tlvtype,addressproof,description,10 4 | tlvdata,addressproof,description,description,utf8,... 5 | tlvtype,addressproof,features,12 6 | tlvdata,addressproof,features,features,byte,... 7 | tlvtype,addressproof,absolute_expiry,14 8 | tlvdata,addressproof,absolute_expiry,seconds_from_epoch,tu64, 9 | tlvtype,addressproof,paths,16 10 | tlvdata,addressproof,paths,paths,blinded_path,... 11 | tlvtype,addressproof,vendor,20 12 | tlvdata,addressproof,vendor,vendor,utf8,... 13 | tlvtype,addressproof,node_ids,60 14 | tlvdata,addressproof,node_ids,node_ids,point32,... 15 | tlvtype,addressproof,certsignature,500 16 | tlvdata,addressproof,certsignature,sig,byte,... 17 | tlvtype,addressproof,cert,501 18 | tlvdata,addressproof,cert,cert,byte,... 19 | tlvtype,addressproof,certchain,503 20 | tlvdata,addressproof,certchain,chain,byte,... 21 | -------------------------------------------------------------------------------- /python/preamble.py: -------------------------------------------------------------------------------- 1 | from bolt12 import (towire_u64, towire_u32, towire_u16, 2 | towire_byte, towire_tu64, towire_tu32, 3 | fromwire_u64, fromwire_u32, fromwire_u16, 4 | fromwire_byte, fromwire_tu64, fromwire_tu32, 5 | towire_chain_hash, fromwire_chain_hash, 6 | towire_sha256, fromwire_sha256, 7 | towire_point32, fromwire_point32, 8 | towire_point, fromwire_point, 9 | towire_bip340sig, fromwire_bip340sig, 10 | towire_array_utf8, fromwire_array_utf8, 11 | towire_short_channel_id, 12 | fromwire_short_channel_id, towire_bigsize, 13 | fromwire_bigsize, 14 | towire_tu16, fromwire_tu16, 15 | towire_channel_id, fromwire_channel_id, 16 | towire_signature, fromwire_signature) 17 | 18 | -------------------------------------------------------------------------------- /python/test_bolt12address.py: -------------------------------------------------------------------------------- 1 | from bolt12address import AddressProof, AddressProofDecoder 2 | 3 | 4 | def test_encode(): 5 | with open("../certs/privkey.pem", "rb") as f: 6 | privkey_pem = f.read() 7 | with open("../certs/cert.pem", "rb") as f: 8 | cert_pem = f.read() 9 | with open("../certs/chain.pem", "rb") as f: 10 | chain_pem = f.read() 11 | ap = AddressProof.create(vendor="bootstrap.bolt12.org", 12 | node_ids=[bytes.fromhex("4b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605")], 13 | privkey_pem=privkey_pem, 14 | cert_pem = cert_pem, 15 | chain_pem = chain_pem) 16 | 17 | ok, whybad = ap.check() 18 | assert ok 19 | 20 | dec = AddressProofDecoder() 21 | dec.add(ap.encode()) 22 | 23 | ap2, _ = dec.result() 24 | ok, whybad = ap2.check() 25 | assert ok 26 | 27 | 28 | def test_decode(): 29 | with open("example.tlv", "rb") as f: 30 | tlvbytes = f.read() 31 | 32 | ap = AddressProof(tlvbytes) 33 | print(ap.values) 34 | print(ap.merkle().hex()) 35 | 36 | assert ap.check() == (True, '') 37 | -------------------------------------------------------------------------------- /shell/make-addressproof.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # Making an address proof, with shell. 3 | set -e 4 | 5 | hex_to_bytes() { 6 | echo "$@" | tr -d '[:space:]' | xxd -r -p 7 | } 8 | 9 | tlv_hexint() { 10 | if [ $1 -gt 4294967295 ]; then 11 | printf %02x 255 12 | printf %016x $1 13 | elif [ $1 -gt 65535 ]; then 14 | printf %02x 254 15 | printf %08x $1 16 | elif [ $1 -gt 253 ]; then 17 | printf %02x 253 18 | printf %04x $1 19 | else 20 | printf %02x $1 21 | fi 22 | } 23 | 24 | # Create a TLV value, hexencoded, given type (int) and contents (hex). 25 | tlv_hex() { 26 | TYPE="$1" 27 | shift 28 | CONTENTS="$(echo -n $* | tr -d '[:space:]')" 29 | tlv_hexint "$TYPE" 30 | LEN=$(( $(echo "$CONTENTS" | wc -c) / 2)) 31 | tlv_hexint "$LEN" 32 | echo "$CONTENTS" 33 | } 34 | 35 | # BOLT #12: 36 | # The Merkle tree's leaves are, in TLV-ascending order for each tlv: 37 | # 1. The H(`LnLeaf`,tlv). 38 | lnleaf_hash() { 39 | TAGH=`echo -n "LnLeaf" | sha256sum | cut -c1-64` 40 | hex_to_bytes $TAGH $TAGH $1 | sha256sum | cut -c1-64 41 | } 42 | 43 | # BOLT #12: 44 | # 2. The H(`LnAll`|all-tlvs,tlv) 45 | lnall_hash() { 46 | TAGH=`(echo -n "LnAll"; hex_to_bytes $1) | sha256sum | cut -c1-64` 47 | hex_to_bytes $TAGH $TAGH $2 | sha256sum | cut -c1-64 48 | } 49 | 50 | merkle_pair() 51 | { 52 | TAGH=`echo -n "LnBranch" | sha256sum | cut -c1-64` 53 | if [ "$( (echo $1; echo $2) | sort | head -n1)" = $1 ]; then 54 | hex_to_bytes $TAGH $TAGH $1 $2 | sha256sum | cut -c1-64 55 | else 56 | hex_to_bytes $TAGH $TAGH $2 $1 | sha256sum | cut -c1-64 57 | fi 58 | } 59 | 60 | # See Bolt12 for details 61 | merkle() { 62 | ALL="$1" 63 | shift 64 | if [ $# = 1 ]; then 65 | LNLEAF=`lnleaf_hash $1` 66 | LNALL=`lnall_hash "$ALL" $1` 67 | merkle_pair $LNLEAF $LNALL 68 | return 69 | fi 70 | ORDER=1 71 | while [ $(($ORDER * 2)) -lt $# ]; do 72 | ORDER=$(($ORDER * 2)) 73 | done 74 | LEFTARGS="" 75 | i=1 76 | while [ $i -le $ORDER ]; do 77 | LEFTARGS="$LEFTARGS $1" 78 | shift 79 | i=$(($i + 1)) 80 | done 81 | 82 | LEFT=$(merkle "$ALL" $LEFTARGS) 83 | RIGHT=$(merkle "$ALL" $@) 84 | 85 | merkle_pair $LEFT $RIGHT 86 | } 87 | 88 | for arg; do 89 | case "$arg" in 90 | --expiry=*) 91 | EXPVAL=$(date +%s -d "${arg#--expiry=}") 92 | # Tu64 93 | EXPIRY=$(printf %16x $EXPVAL | sed 's/^\(00\)*//') 94 | ;; 95 | --vendor=*) 96 | VENDOR=$(echo -n "${arg#--vendor=}" | od -tx1 -Anone) 97 | ;; 98 | --nodeid=*) 99 | NODEIDS="$NODEIDS ${arg#--nodeid=}" 100 | ;; 101 | --description=*) 102 | DESC=$(echo -n "${arg#--description=}" | od -tx1 -Anone) 103 | ;; 104 | --privkeyfile=*) 105 | PRIVKEYFILE="${arg#--privkeyfile=}" 106 | ;; 107 | --certfile=*) 108 | CERTFILE="${arg#--certfile=}" 109 | ;; 110 | --chainfile=*) 111 | CHAINFILE="${arg#--chainfile=}" 112 | ;; 113 | --chain=*) 114 | case "${arg#--chain=}" in 115 | bitcoin) 116 | CHAIN=6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000 117 | ;; 118 | testnet) 119 | CHAIN=43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000 120 | ;; 121 | regtest) 122 | CHAIN=06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f 123 | ;; 124 | signet) 125 | CHAIN=f61eee3b63a380a477a063af32b2bbc97c9ff9f01f2c4225e973988108000000 126 | ;; 127 | *) 128 | echo Unknown chain "${arg#--chain=}" >&2 129 | exit 1 130 | esac 131 | ;; 132 | --help|-h) 133 | echo "Usage: $0 --privkeyfile=privkey.pem --vendor=bootstrap.bolt12.org --nodeid=32-byte-nodeid [--nodeid=...] [--expiry=date] [--certfile=cert.pem] [--chainfile=chain.pem] [--chain=bitcoin|testnet|regtest|signet] [--description=\"Please send money\"]" >&2 134 | exit 1 135 | ;; 136 | *) 137 | echo Unknown argument "$arg" >&2 138 | exit 1 139 | esac 140 | done 141 | 142 | [ -n "$PRIVKEYFILE" ] || (echo Missing --privkeyfile >&2; exit 1) 143 | [ -n "$VENDOR" ] || (echo Missing --vendor >&2; exit 1) 144 | [ -n "$NODEIDS" ] || (echo Need at least one --nodeid >&2; exit 1) 145 | 146 | TLVHEX="" 147 | if [ -n "$CHAIN" ]; then 148 | TLVHEX="$TLVHEX $(tlv_hex 2 $CHAIN)" 149 | fi 150 | if [ -n "$DESC" ]; then 151 | TLVHEX="$TLVHEX $(tlv_hex 10 $DESC)" 152 | fi 153 | if [ -n "$EXPIRY" ]; then 154 | TLVHEX="$TLVHEX $(tlv_hex 14 $EXPIRY)" 155 | fi 156 | TLVHEX="$TLVHEX $(tlv_hex 20 $VENDOR)" 157 | TLVHEX="$TLVHEX $(tlv_hex 60 $NODEIDS)" 158 | 159 | echo TLVHEX=$TLVHEX >&2 160 | MERKLE=$(merkle "$TLVHEX" $TLVHEX) 161 | echo MERKLE=$MERKLE >&2 162 | 163 | # Fields 250 - 1000 inclusive don't get included in merkle. 164 | HEXSIG=$(hex_to_bytes $MERKLE | openssl pkeyutl -sign -inkey "$PRIVKEYFILE" -pkeyopt digest:sha256 -pkeyopt rsa_padding_mode:pss -pkeyopt rsa_pss_saltlen:max | od -tx1 -Anone) 165 | TLVHEX="$TLVHEX $(tlv_hex 500 $HEXSIG)" 166 | 167 | if [ -n "$CERTFILE" ]; then 168 | TLVHEX="$TLVHEX $(tlv_hex 501 $(od -tx1 -Anone < $CERTFILE) )" 169 | fi 170 | if [ -n "$CHAINFILE" ]; then 171 | TLVHEX="$TLVHEX $(tlv_hex 503 $(od -tx1 -Anone < $CHAINFILE) )" 172 | fi 173 | 174 | hex_to_bytes $TLVHEX 175 | -------------------------------------------------------------------------------- /python/generated.py: -------------------------------------------------------------------------------- 1 | from bolt12 import (towire_u64, towire_u32, towire_u16, 2 | towire_byte, towire_tu64, towire_tu32, 3 | fromwire_u64, fromwire_u32, fromwire_u16, 4 | fromwire_byte, fromwire_tu64, fromwire_tu32, 5 | towire_chain_hash, fromwire_chain_hash, 6 | towire_sha256, fromwire_sha256, 7 | towire_point32, fromwire_point32, 8 | towire_point, fromwire_point, 9 | towire_bip340sig, fromwire_bip340sig, 10 | towire_array_utf8, fromwire_array_utf8, 11 | towire_short_channel_id, 12 | fromwire_short_channel_id, towire_bigsize, 13 | fromwire_bigsize, 14 | towire_tu16, fromwire_tu16, 15 | towire_channel_id, fromwire_channel_id, 16 | towire_signature, fromwire_signature) 17 | 18 | 19 | 20 | def towire_addressproof_chains(value): 21 | _n = 0 22 | buf = bytes() 23 | value = {"chains": value} 24 | for v in value["chains"]: 25 | buf += towire_chain_hash(v) 26 | _n += 1 27 | # Ensures there are no extra keys! 28 | assert len(value) == _n 29 | return buf 30 | 31 | 32 | def fromwire_addressproof_chains(buffer): 33 | value = {} 34 | v = [] 35 | i = 0 36 | while len(buffer) != 0: 37 | val, buffer = fromwire_chain_hash(buffer) 38 | v.append(val) 39 | i += 1 40 | value["chains"] = v 41 | 42 | return value["chains"], buffer 43 | 44 | 45 | def towire_addressproof_description(value): 46 | _n = 0 47 | buf = bytes() 48 | value = {"description": value} 49 | buf += towire_array_utf8(value["description"]) 50 | _n += 1 51 | # Ensures there are no extra keys! 52 | assert len(value) == _n 53 | return buf 54 | 55 | 56 | def fromwire_addressproof_description(buffer): 57 | value = {} 58 | value["description"], buffer = fromwire_array_utf8(buffer, len(buffer)) 59 | 60 | return value["description"], buffer 61 | 62 | 63 | def towire_addressproof_features(value): 64 | _n = 0 65 | buf = bytes() 66 | value = {"features": value} 67 | for v in value["features"]: 68 | buf += towire_byte(v) 69 | _n += 1 70 | # Ensures there are no extra keys! 71 | assert len(value) == _n 72 | return buf 73 | 74 | 75 | def fromwire_addressproof_features(buffer): 76 | value = {} 77 | v = [] 78 | i = 0 79 | while len(buffer) != 0: 80 | val, buffer = fromwire_byte(buffer) 81 | v.append(val) 82 | i += 1 83 | value["features"] = v 84 | 85 | return value["features"], buffer 86 | 87 | 88 | def towire_addressproof_absolute_expiry(value): 89 | _n = 0 90 | buf = bytes() 91 | value = {"seconds_from_epoch": value} 92 | buf += towire_tu64(value["seconds_from_epoch"]) 93 | _n += 1 94 | # Ensures there are no extra keys! 95 | assert len(value) == _n 96 | return buf 97 | 98 | 99 | def fromwire_addressproof_absolute_expiry(buffer): 100 | value = {} 101 | val, buffer = fromwire_tu64(buffer) 102 | value["seconds_from_epoch"] = val 103 | 104 | return value["seconds_from_epoch"], buffer 105 | 106 | 107 | def towire_addressproof_paths(value): 108 | _n = 0 109 | buf = bytes() 110 | value = {"paths": value} 111 | for v in value["paths"]: 112 | buf += towire_blinded_path(v) 113 | _n += 1 114 | # Ensures there are no extra keys! 115 | assert len(value) == _n 116 | return buf 117 | 118 | 119 | def fromwire_addressproof_paths(buffer): 120 | value = {} 121 | v = [] 122 | i = 0 123 | while len(buffer) != 0: 124 | val, buffer = fromwire_blinded_path(buffer) 125 | v.append(val) 126 | i += 1 127 | value["paths"] = v 128 | 129 | return value["paths"], buffer 130 | 131 | 132 | def towire_addressproof_vendor(value): 133 | _n = 0 134 | buf = bytes() 135 | value = {"vendor": value} 136 | buf += towire_array_utf8(value["vendor"]) 137 | _n += 1 138 | # Ensures there are no extra keys! 139 | assert len(value) == _n 140 | return buf 141 | 142 | 143 | def fromwire_addressproof_vendor(buffer): 144 | value = {} 145 | value["vendor"], buffer = fromwire_array_utf8(buffer, len(buffer)) 146 | 147 | return value["vendor"], buffer 148 | 149 | 150 | def towire_addressproof_node_ids(value): 151 | _n = 0 152 | buf = bytes() 153 | value = {"node_ids": value} 154 | for v in value["node_ids"]: 155 | buf += towire_point32(v) 156 | _n += 1 157 | # Ensures there are no extra keys! 158 | assert len(value) == _n 159 | return buf 160 | 161 | 162 | def fromwire_addressproof_node_ids(buffer): 163 | value = {} 164 | v = [] 165 | i = 0 166 | while len(buffer) != 0: 167 | val, buffer = fromwire_point32(buffer) 168 | v.append(val) 169 | i += 1 170 | value["node_ids"] = v 171 | 172 | return value["node_ids"], buffer 173 | 174 | 175 | def towire_addressproof_certsignature(value): 176 | _n = 0 177 | buf = bytes() 178 | value = {"sig": value} 179 | for v in value["sig"]: 180 | buf += towire_byte(v) 181 | _n += 1 182 | # Ensures there are no extra keys! 183 | assert len(value) == _n 184 | return buf 185 | 186 | 187 | def fromwire_addressproof_certsignature(buffer): 188 | value = {} 189 | v = [] 190 | i = 0 191 | while len(buffer) != 0: 192 | val, buffer = fromwire_byte(buffer) 193 | v.append(val) 194 | i += 1 195 | value["sig"] = v 196 | 197 | return value["sig"], buffer 198 | 199 | 200 | def towire_addressproof_cert(value): 201 | _n = 0 202 | buf = bytes() 203 | value = {"cert": value} 204 | for v in value["cert"]: 205 | buf += towire_byte(v) 206 | _n += 1 207 | # Ensures there are no extra keys! 208 | assert len(value) == _n 209 | return buf 210 | 211 | 212 | def fromwire_addressproof_cert(buffer): 213 | value = {} 214 | v = [] 215 | i = 0 216 | while len(buffer) != 0: 217 | val, buffer = fromwire_byte(buffer) 218 | v.append(val) 219 | i += 1 220 | value["cert"] = v 221 | 222 | return value["cert"], buffer 223 | 224 | 225 | def towire_addressproof_certchain(value): 226 | _n = 0 227 | buf = bytes() 228 | value = {"chain": value} 229 | for v in value["chain"]: 230 | buf += towire_byte(v) 231 | _n += 1 232 | # Ensures there are no extra keys! 233 | assert len(value) == _n 234 | return buf 235 | 236 | 237 | def fromwire_addressproof_certchain(buffer): 238 | value = {} 239 | v = [] 240 | i = 0 241 | while len(buffer) != 0: 242 | val, buffer = fromwire_byte(buffer) 243 | v.append(val) 244 | i += 1 245 | value["chain"] = v 246 | 247 | return value["chain"], buffer 248 | 249 | 250 | tlv_addressproof = { 251 | 2: ("chains", towire_addressproof_chains, fromwire_addressproof_chains), 252 | 10: ("description", towire_addressproof_description, fromwire_addressproof_description), 253 | 12: ("features", towire_addressproof_features, fromwire_addressproof_features), 254 | 14: ("absolute_expiry", towire_addressproof_absolute_expiry, fromwire_addressproof_absolute_expiry), 255 | 16: ("paths", towire_addressproof_paths, fromwire_addressproof_paths), 256 | 20: ("vendor", towire_addressproof_vendor, fromwire_addressproof_vendor), 257 | 60: ("node_ids", towire_addressproof_node_ids, fromwire_addressproof_node_ids), 258 | 500: ("certsignature", towire_addressproof_certsignature, fromwire_addressproof_certsignature), 259 | 501: ("cert", towire_addressproof_cert, fromwire_addressproof_cert), 260 | 503: ("certchain", towire_addressproof_certchain, fromwire_addressproof_certchain), 261 | } 262 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BOLT12 Address Support (DRAFT!) 2 | 3 | Inspired by the awesome [lightningaddress.com](https://lightningaddress.com), 4 | except for BOLT12: 5 | 6 | 1. Supports BOLT12 7 | 2. Allows BOLT12 vendor string authentication 8 | 3. Doesn't require your wallet to query the server directly 9 | 4. Required only to establish the initial node linkage 10 | 11 | ## How Does it Work? 12 | 13 | Like lightningaddress.com, you turn username@domain.com into a web request: 14 | 15 | https://domain.com/.well-known/bolt12/bitcoin/username@domain.com 16 | 17 | But you can also authenticate the entire domain: 18 | 19 | https://domain.com/.well-known/bolt12/bitcoin/domain.com 20 | 21 | (Instead of `bitcoin` you could use `testnet`, `signet` or `regtest`) 22 | 23 | ## The Format 24 | 25 | The format is a bolt12 TLV binary (Content-type: 26 | application/x-lightning-bolt12), containing the following fields: 27 | 28 | 1. `tlv_stream`: `addressproof` 29 | 2. types: 30 | * type: 2 (`chains`) 31 | * data: 32 | * [`...*chain_hash`:`chains`] 33 | * type: 10 (`description`) 34 | * data: 35 | * [`...*utf8`:`description`] 36 | * type: 12 (`features`) 37 | * data: 38 | * [`...*byte`:`features`] 39 | * type: 14 (`absolute_expiry`) 40 | * data: 41 | * [`tu64`:`seconds_from_epoch`] 42 | * type: 16 (`paths`) 43 | * data: 44 | * [`...*blinded_path`:`paths`] 45 | * type: 20 (`vendor`) 46 | * data: 47 | * [`...*utf8`:`vendor`] 48 | * type: 60 (`node_ids`) 49 | * data: 50 | * [`...*point32`:`node_ids`] 51 | * type: 500 (`certsignature`) 52 | * data: 53 | * [`...*byte`:`sig`] 54 | * type: 501 (`cert`) 55 | * data: 56 | * [`...*byte`:`cert`] 57 | * type: 503 (`certchain`) 58 | * data: 59 | * [`...*byte`:`chain`] 60 | 61 | Only the `vendor`, `node_ids` and `certsignature` fields are required, 62 | the others are optional. 63 | 64 | ### Requirements 65 | 66 | The writer: 67 | 68 | - MUST set `vendor` to filename being served: 69 | - either username@domain or simply domain. 70 | - MUST set `node_ids` to zero or more node_ids which will be used to sign offers for this vendor. 71 | - MUST set `chains` to the chains these `node_ids` are valid for, or MAY not set `chains` if the `node_ids` are valid for Bitcoin. 72 | - MAY set `features`, `absolute_expiry`, `description` and `paths` (see BOLT 12). 73 | - MUST set `certsignature` to the RSA signature (using PSS padding mode maximum saltlen) of the BOLT-12 merkle root 74 | as per [BOLT12 Signature Calculation](https://bolt12.org/bolt12.html#signature-calculation) using the secret key for the domain in `vendor`. 75 | - MUST NOT set `description` unless it has an offer which is constructed using the other fields, and the offer's `node_id` set to the first of the `node_ids`. 76 | - If it is serving the `addressproof` over HTTPS: 77 | - MAY set `cert` and `certchain` 78 | - Otherwise: 79 | - MUST set both `cert` and `certchain` 80 | - If it sets `cert`: 81 | - MUST set it to the PEM-encoded certificate corresponding to the domain 82 | - If it sets `certchain`: 83 | - MUST set it to the PEM-encoded chain of certificates leading from 84 | `cert` to the root CA 85 | 86 | 87 | The reader: 88 | - MUST NOT accept the address proof if `vendor`, `node_ids` or 89 | `certsignature` is not present. 90 | - MUST NOT accept the address proof if an even unknown bit is set in `features`. 91 | - If it has NOT retrieved the `addressproof` over HTTPS: 92 | - MUST NOT accept the address proof if: 93 | - `cert` is not present, or not valid for the domain in `vendor`. 94 | - `certchain` is not present, or does not link `cert` to a root certificate authority. 95 | - `certsignature` field is not a valid signature for BOLT-12 merkle root using the key in `cert`. 96 | - otherwise: 97 | - MAY retrieve `cert` and `certchain` from the HTTPS connection. 98 | - MAY NOT accept the address proof as it would in the non-HTTPS case above. 99 | - If if has a previous, valid `addressproof` for this vendor: 100 | - MUST ONLY replace it with this address proof if: 101 | - `absolute_expiry` is set, AND 102 | - it is greater than the previous `absolute_expiry` OR the previous had 103 | no `absolute_expiry` field. 104 | - MUST consider the `addressproof` no longer valid if 105 | `absolute_expiry` is set and the current number of seconds since 106 | 1970 is greater than that value. 107 | - if `description` is present: 108 | - MAY use the fields of this `addressproof` as an unsigned offer. 109 | 110 | - When it encounters a `vendor` field in a BOLT12 offer or invoice: 111 | - if the vendor begins with a valid domain, up to a space character: 112 | - SHOULD WARN the user if it cannot find a current valid address proof. 113 | - SHOULD reject the offer or invoice if the node_id is not one of the 114 | `node_ids` in the offer. 115 | 116 | ### Text Encoding 117 | 118 | The human-readable prefix for addressproof is `lnap`, if you want it 119 | encoded as a string. 120 | 121 | ## What Does All This Do? 122 | 123 | This allows domain validation for bolt 12 offers, which already have a 124 | vendor field for this purpose. e.g if the vendor in an offer is 125 | "blockstream.com Get your blocks here!" then your wallet can reach out 126 | to blockstream.com to see if it the node_id in the offer really is under 127 | their control. 128 | 129 | It also allows node_id proofs for individual addresses. 130 | 131 | But you don't need to reach out to blockstream.com: anyone (including 132 | your wallet vendor, or the node claiming to be blockstream.com) can 133 | collect all the `addressproof`s and certificates for you, as they 134 | contain a signature using the existing web certificate infrastructure. 135 | Bundling these protects your privacy more than having to request to a 136 | vendor's website before making a payment. 137 | 138 | This format is a subset of the BOLT12 offer format, so if it has a 139 | `description` it is actually a valid (amountless) offer, allowing 140 | immediate tipping using it. 141 | 142 | You can also include zero node_ids, as a way of indicating that you do 143 | *not* have any lightning nodes. 144 | 145 | ## Examples 146 | 147 | You will need access to your privkey.pem, cert.pem and chain.pem files 148 | on your HTTPS webserver which serves the domain. 149 | 150 | This creates a proof that `bolt12.org` operates nodeid 151 | 4b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605 (note 152 | we omit the 02/03 prefix): 153 | 154 | ``` 155 | $ ./shell/make-addressproof.sh \ 156 | --vendor=bolt12.org \ 157 | --nodeid=4b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605 \ 158 | --privkeyfile=certs/privkey.pem \ 159 | --certfile=certs/cert.pem \ 160 | --chainfile=certs/chain.pem > .well-known/bolt12/bitcoin/bolt12.org 161 | ``` 162 | 163 | This does the same thing using the Python script: 164 | 165 | ``` 166 | $ ./python/bolt12address.py create \ 167 | --raw \ 168 | bolt12.org \ 169 | certs/privkey.pem \ 170 | certs/cert.pem \ 171 | certs/chain.pem \ 172 | 4b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605 \ 173 | > .well-known/bolt12/bitcoin/bolt12.org 174 | ``` 175 | 176 | This creates a *signet* signature for multiple nodeids, for the user 177 | *rusty@bolt12.org*, and adds a description so it can also serve as 178 | offer for sending unsolicited payments (note: see below!): 179 | 180 | ``` 181 | $ ./shell/make-addressproof.sh \ 182 | --vendor=rusty@bolt12.org \ 183 | --nodeid=4b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605 \ 184 | --nodeid=994b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc4496 \ 185 | --privkeyfile=certs/privkey.pem \ 186 | --certfile=certs/cert.pem \ 187 | --chainfile=certs/chain.pem \ 188 | --chain=signet \ 189 | --description='Unsolicited bolt12address donation' \ 190 | > .well-known/bolt12/signet/rusty@bolt12.org 191 | ``` 192 | 193 | And in Python: 194 | 195 | ``` 196 | $ ./python/bolt12address.py create \ 197 | --raw \ 198 | --description='Unsolicited bolt12address donation' \ 199 | --chain=signet \ 200 | rusty@bolt12.org \ 201 | certs/privkey.pem \ 202 | certs/cert.pem \ 203 | certs/chain.pem \ 204 | 4b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605 \ 205 | 994b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc4496 \ 206 | > .well-known/bolt12/signet/rusty@bolt12.org 207 | ``` 208 | 209 | We can check this using python: 210 | 211 | ``` 212 | $ ./python/bolt12address.py check --raw-stdin < .well-known/bolt12/signet/rusty@bolt12.org 213 | chains: ['f61eee3b63a380a477a063af32b2bbc97c9ff9f01f2c4225e973988108000000'] 214 | description: Unsolicited bolt12address donation 215 | vendor: rusty@bolt12.org 216 | node_ids: ['4b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605', '994b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc4496'] 217 | certsignature: 173e... 218 | 219 | offer_id: 3234297fb2414b62c16ac9751ac241050199ec5e2cd83e713136cc26974f09a8 220 | offer_id: 3139b327c9fa6637a7ef620149425a1163e19a1181c9f1cbdc7820360dd40c23 221 | ``` 222 | 223 | The `offer_id` at the end if `description` is populated (one for each 224 | node_id) is the offer_id anyone reading would expect to be able to 225 | send funds to. You should create this offer (with that description 226 | and no amount) on your node! 227 | 228 | ## Refreshing Existing Proofs 229 | 230 | There's a simple helper to refresh existing address proofs, such as 231 | when your certificate changes: 232 | 233 | ``` 234 | $ ./python/bolt12address.py refresh \ 235 | certs/privkey.pem \ 236 | certs/cert.pem \ 237 | certs/chain.pem \ 238 | .well-known/bolt12/signet/*bolt12.org 239 | .well-known/bolt12/signet/rusty@bolt12.org: REFRESHED 240 | ``` 241 | 242 | 243 | 244 | 245 | 246 | ## TODO 247 | 248 | This is a draft: I expect it to change after feedback (especially 249 | since the certinficates and signatures are large and clunky). 250 | 251 | The code does not check the certificate chain, and is generally could 252 | use polishing. 253 | 254 | We also need more routines in different languages to fetch and check 255 | the bolt12address, and a method so Lightning nodes can serve their 256 | addressproof directly. 257 | 258 | ## Feedback 259 | 260 | You can reach out to me as rusty@rustcorp.com.au or join the bolt12 261 | telegram group at https://t.me/bolt12org. 262 | 263 | Happy hacking! 264 | -------------------------------------------------------------------------------- /python/bolt12address.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | import argparse 3 | import generated 4 | import bolt12 5 | import os 6 | import tempfile 7 | import time 8 | import sys 9 | from typing import Tuple, Sequence, Optional 10 | from cryptography import x509 11 | from cryptography.hazmat.primitives import hashes 12 | from cryptography.hazmat.primitives.asymmetric import padding 13 | from cryptography.hazmat.primitives.asymmetric import utils 14 | from cryptography.exceptions import InvalidSignature 15 | from cryptography.hazmat.primitives.serialization import load_pem_private_key 16 | from cryptography.hazmat.primitives.asymmetric import rsa 17 | 18 | class AddressProof(bolt12.Bolt12): 19 | """Class for an address proof""" 20 | def __init__(self, proof: Optional[bytes]): 21 | super().__init__("lnap", generated.tlv_addressproof, proof) 22 | 23 | @classmethod 24 | def create(cls, vendor: str, 25 | node_ids: Sequence[bytes], 26 | privkey_pem: bytes, 27 | cert_pem: bytes = None, 28 | chain_pem: bytes = None, 29 | description: Optional[str] = None, 30 | features: bytes = None, 31 | absolute_expiry: Optional[int] = None, 32 | chain: bytes = None): 33 | self = cls(None) 34 | self.values = {'vendor': vendor, 35 | 'node_ids': node_ids} 36 | if features is not None: 37 | self.values['features'] = features 38 | if description is not None: 39 | self.values['description'] = description 40 | if absolute_expiry is not None: 41 | self.values['absolute_expiry'] = absolute_expiry 42 | if cert_pem is not None: 43 | self.values['cert'] = cert_pem 44 | if chain_pem is not None: 45 | self.values['certchain'] = chain_pem 46 | if chain is not None: 47 | self.values['chains'] = [chain] 48 | 49 | key = load_pem_private_key(privkey_pem, password=None) 50 | if not isinstance(key, rsa.RSAPrivateKey): 51 | raise ValueError("privkey_pem was not RSA: {}".format(type(key))) 52 | 53 | sig = key.sign(self.merkle(), 54 | padding.PSS(mgf=padding.MGF1(hashes.SHA256()), 55 | salt_length=padding.PSS.MAX_LENGTH), 56 | utils.Prehashed(hashes.SHA256())) 57 | self.values['certsignature'] = sig 58 | return self 59 | 60 | @classmethod 61 | def from_string(cls, proofstr: str): 62 | dec = bolt12.Decoder() 63 | if not dec.add(proof): 64 | raise ValueError("Incomplete string") 65 | # FIXME: expose this is Decoder! 66 | hrp, bytestr = simple_bech32_decode(re.sub(r'([A-Z0-9a-z])\+\s*([A-Z0-9a-z])', r'\1\2', 67 | dec.so_far)) 68 | if hrp != "lnap": 69 | raise ValueError("This is a {} not lnap".format(hrp)) 70 | return cls(bytestr) 71 | 72 | def check(self, needcert=True) -> Tuple[bool, str]: 73 | """Check it's OK: returns (True, '') or (False, reason)""" 74 | for fname in ('vendor', 'node_ids', 'certsignature'): 75 | if fname not in self.values: 76 | return False, 'Missing {}'.format(fname) 77 | 78 | whybad = self.check_features(self.values.get('features', bytes())) 79 | if whybad: 80 | return False, whybad 81 | 82 | if 'absolute_expiry' in self.values: 83 | if time.time() > self.values['absolute_expiry']: 84 | return False, "Expired {} seconds ago".format(self.values['absolute_expiry'] - int(time.time())) 85 | 86 | if needcert: 87 | for fname in ('cert', 'certchain'): 88 | if fname not in self.values: 89 | return False, 'Missing {}'.format(fname) 90 | 91 | if 'cert' in self.values: 92 | try: 93 | cert = x509.load_pem_x509_certificate(bytes(self.values['cert'])) 94 | except ValueError: 95 | return False, 'Unparsable PEM x509 certificate' 96 | 97 | key = cert.public_key() 98 | try: 99 | key.verify(bytes(self.values['certsignature']), self.merkle(), 100 | padding.PSS( 101 | mgf=padding.MGF1(hashes.SHA256()), 102 | salt_length=padding.PSS.MAX_LENGTH), 103 | utils.Prehashed(hashes.SHA256())) 104 | except InvalidSignature: 105 | return False, 'Invalid certsignature' 106 | 107 | # FIXME: check cert chain! 108 | 109 | return True, '' 110 | 111 | 112 | class AddressProofDecoder(bolt12.Decoder): 113 | def result(self) -> Tuple[Optional[AddressProof], str]: 114 | """One string is complete(), try decoding""" 115 | try: 116 | hrp, bytestr = self.raw_decode() 117 | except ValueError as e: 118 | return None, ' '.join(e.args) 119 | 120 | if hrp != 'lnap': 121 | return None, 'Is not an AddressProof' 122 | 123 | return AddressProof(bytestr), '' 124 | 125 | 126 | def create(args): 127 | if args.nodeid == [] and not args.no_nodeid: 128 | print("No nodeid specified (if you really want this, use --no-nodeids)", 129 | file=sys.stderr) 130 | sys.exit(1) 131 | 132 | if args.expiry and args.rel_expiry: 133 | print("Cannot specify both --expiry and --rel-expiry", 134 | file=sys.stderr) 135 | sys.exit(1) 136 | 137 | if args.rel_expiry: 138 | args.expiry = int(time.time()) + args.rel_expiry 139 | 140 | with open(args.privkeyfile, "rb") as f: 141 | privkey_pem = f.read() 142 | with open(args.certfile, "rb") as f: 143 | cert_pem = f.read() 144 | with open(args.chainfile, "rb") as f: 145 | chain_pem = f.read() 146 | 147 | # Feature fields are big-endian. Really. 148 | if args.feature: 149 | flen = (max(args.feature) + 7) / 8 150 | features = sum([(1 << f) for f in args.feature]).to_bytes(flen, 'big') 151 | else: 152 | features = None 153 | 154 | if args.chain: 155 | if args.chain == 'bitcoin': 156 | chainhash = bytes.fromhex('6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000') 157 | elif args.chain == 'testnet': 158 | chainhash = bytes.fromhex('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000') 159 | elif args.chain == 'regtest': 160 | chainhash = bytes.fromhex('06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f') 161 | elif args.chain == 'signet': 162 | chainhash = bytes.fromhex('f61eee3b63a380a477a063af32b2bbc97c9ff9f01f2c4225e973988108000000') 163 | else: 164 | # Should not happen: choices[] should restrict it. 165 | raise ValueError("Unknown type {}".format(args.chain)) 166 | else: 167 | # The default, bitcoin mainnet. 168 | chainhash = None 169 | 170 | ap = AddressProof.create(args.vendor, 171 | [bytes.fromhex(n) for n in args.nodeid], 172 | privkey_pem, 173 | cert_pem, 174 | chain_pem, 175 | args.description, 176 | features, 177 | args.expiry, 178 | chainhash) 179 | assert ap.check() == (True, '') 180 | 181 | if args.raw: 182 | sys.stdout.buffer.write(bolt12.helper_towire_tlv(ap.tlv_table, ap.values, ap.unknowns)) 183 | else: 184 | print(ap.encode()) 185 | 186 | 187 | def check(args): 188 | if args.raw_stdin: 189 | ap = AddressProof(sys.stdin.buffer.read()) 190 | else: 191 | dec = AddressProofDecoder() 192 | for a in args.ap: 193 | dec.add(a) 194 | if not dec.complete(): 195 | print("Incomplete bolt12 string", file=sys.stderr) 196 | sys.exit(1) 197 | 198 | ap, whybad = dec.result() 199 | if ap is None: 200 | print("Bad lnap encoding: {}".format(whybad), file=sys.stderr) 201 | sys.exit(1) 202 | 203 | ok, whybad = ap.check() 204 | if not ok: 205 | print("Invalid lnap: {}".format(whybad), file=sys.stderr) 206 | sys.exit(1) 207 | 208 | # FIXME: Get pretty! 209 | for k, v in ap.values.items(): 210 | if isinstance(v, bytes): 211 | val = v.hex() 212 | elif isinstance(v, list): 213 | if len(v) == 0: 214 | val = v 215 | elif isinstance(v[0], int): 216 | val = bytes(v).hex() 217 | elif isinstance(v[0], bytes): 218 | val = [b.hex() for b in v] 219 | else: 220 | val = v 221 | else: 222 | val = v 223 | print("{}: {}".format(k, val)) 224 | 225 | if 'description' in ap.values: 226 | for n in ap.values['node_ids']: 227 | offer = bolt12.Offer.create(description=ap.values['description'], 228 | node_id=n) 229 | print('offer_id: {}'.format(offer.merkle().hex())) 230 | 231 | 232 | def refresh(args): 233 | """Refresh: exits 1 if it can't refresh all of them""" 234 | if args.expiry and args.rel_expiry: 235 | print("Cannot specify both --expiry and --rel-expiry", 236 | file=sys.stderr) 237 | sys.exit(1) 238 | 239 | if args.rel_expiry: 240 | args.expiry = int(time.time()) + args.rel_expiry 241 | 242 | with open(args.privkeyfile, "rb") as f: 243 | privkey_pem = f.read() 244 | with open(args.certfile, "rb") as f: 245 | cert_pem = f.read() 246 | with open(args.chainfile, "rb") as f: 247 | chain_pem = f.read() 248 | 249 | allok = True 250 | # FIXME: If it's a directory, scan its contents? 251 | for fname in args.files: 252 | try: 253 | with open(fname, "rb") as f: 254 | ap = AddressProof(f.read()) 255 | except Exception as e: 256 | print("{}: NOT AN ADDRPROOF: {}".format(fname, e)) 257 | allok = False 258 | continue 259 | 260 | if args.expiry: 261 | values['absolute_expiry'] = args.expiry 262 | ap.values['cert'] = cert_pem 263 | ap.values['certchain'] = chain_pem 264 | 265 | key = load_pem_private_key(privkey_pem, password=None) 266 | sig = key.sign(ap.merkle(), 267 | padding.PSS(mgf=padding.MGF1(hashes.SHA256()), 268 | salt_length=padding.PSS.MAX_LENGTH), 269 | utils.Prehashed(hashes.SHA256())) 270 | ap.values['certsignature'] = sig 271 | ok, whybad = ap.check() 272 | 273 | if not ok: 274 | print("{}: FAILED REGEN CHECK? {}".format(fname, whybad)) 275 | allok = False 276 | continue 277 | 278 | # We want this in the same dir, for atomicity. 279 | tmpfd, tmpfname = tempfile.mkstemp(suffix=".refreshing", 280 | dir=os.path.dirname(fname)) 281 | tmpf = os.fdopen(tmpfd, 'wb') 282 | tmpf.write(bolt12.helper_towire_tlv(ap.tlv_table, 283 | ap.values, ap.unknowns)) 284 | tmpf.close() 285 | os.replace(tmpfname, fname) 286 | print("{}: REFRESHED".format(fname)) 287 | 288 | if not allok: 289 | sys.exit(1) 290 | 291 | 292 | if __name__ == "__main__": 293 | parser = argparse.ArgumentParser(description='Tool to create/validate bolt12 address proofs') 294 | subparsers = parser.add_subparsers() 295 | 296 | createparser = subparsers.add_parser('create') 297 | createparser.add_argument('vendor', 298 | help='Name, either DOMAIN or USER@DOMAIN') 299 | createparser.add_argument('privkeyfile', help='privkey.pem file for DOMAIN') 300 | createparser.add_argument('certfile', help='cert.pem file for DOMAIN') 301 | createparser.add_argument('chainfile', help='chain.pem file for DOMAIN') 302 | createparser.add_argument('--description', 303 | help='description for others to send funds (you must create an offer with this description, too!)') 304 | createparser.add_argument('--feature', action='append', 305 | help='Feature to set in feature field') 306 | createparser.add_argument('--chain', help='The name of the chain', 307 | choices=['bitcoin', 'regtest', 'testnet', 'signet']) 308 | createparser.add_argument('--expiry', 309 | help='The absolute time for the address proof to expire, in seconds since 1970', type=int) 310 | createparser.add_argument('--rel-expiry', 311 | help='The number of seconds from now for the addressproof to expire', type=int) 312 | # FIXME: Add blinded paths! 313 | createparser.add_argument('nodeid', nargs='*', 314 | help='Lightning node id for this vendor (can be multiple)') 315 | createparser.add_argument('--no-nodeids', 316 | help='Allows no nodeids to be specified') 317 | createparser.add_argument('--raw', action='store_true', 318 | help="Don't encode as lnap1, just output raw binary") 319 | createparser.set_defaults(func=create) 320 | 321 | checkparser = subparsers.add_parser('check') 322 | checkparser.add_argument('--raw-stdin', action='store_true', 323 | help='Read raw bytes from stdin instead of using cm1dline lnap1 format') 324 | checkparser.add_argument('ap', nargs='*', help='lnap1 string to check') 325 | checkparser.set_defaults(func=check) 326 | 327 | refreshparser = subparsers.add_parser('refresh') 328 | refreshparser.add_argument('privkeyfile', help='privkey.pem file for DOMAIN') 329 | refreshparser.add_argument('certfile', help='cert.pem file for DOMAIN') 330 | refreshparser.add_argument('chainfile', help='chain.pem file for DOMAIN') 331 | refreshparser.add_argument('files', nargs='+', help='Files to regenerate') 332 | refreshparser.add_argument('--expiry', 333 | help='The absolute time for the address proof to expire, in seconds since 1970', type=int) 334 | refreshparser.add_argument('--rel-expiry', 335 | help='The number of seconds from now for the addressproof to expire', type=int) 336 | refreshparser.set_defaults(func=refresh) 337 | 338 | args = parser.parse_args() 339 | args.func(args) 340 | --------------------------------------------------------------------------------