migrate to bip340 schnorr signatures.

This commit is contained in:
fiatjaf 2020-11-17 18:55:45 -03:00
parent d80d020cd2
commit 3404dee7d0
9 changed files with 89 additions and 49 deletions

View File

@ -5,18 +5,18 @@ Basic protocol flow description.
--------------------------------
1. Each user has a keypair in the `secp256k1` curve;
1. Each user has a keypair. Signatures, public key and encodings are done according to the [Schnorr signatures standard for the curve `secp256k1`](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki).
2. Users can publish `events` to any compatible relay by calling `POST /save_update` with `Content-Type: application/json` and a JSON object describing an event:
```
{
id: <32-bytes sha256 of the the serialized event data, optional as will be recomputed by the relay>
pubkey: <33-bytes hex-encoded compressed public key of the event creator>,
pubkey: <32-bytes hex-encoded public key of the event creator>,
created_at: <unix timestamp>,
kind: <integer>,
ref: <32-bytes hex of the id of another event, optional>,
content: <arbitrary string>,
sig: <ECDSA signature of the sha256 hash of the serialized event data, which is the same as the "id" field>,
sig: <64-bytes signature of the sha256 hash of the serialized event data, which is the same as the "id" field>,
}
```
3. (The serialization function is strict and standardized, see source code for now)
@ -25,7 +25,7 @@ Basic protocol flow description.
- `0`: `set_metadata`: the `content` is set to a stringified JSON object `{name: <string>, about: <string>, picture: <url, string>}` describing the user who created the event. A relay may delete past `set_metadata` events once it gets a new one for the same pubkey.
- `1`: `text_note`: the `content` is set to the text content of a note, anything the user wants to say.
- `2`: `recommend_server`: the `content` is set to the URL (e.g., `https://somerelay.com`) of a relay the event creator wants to recommend to its followers.
6. A relay MUST serve an SSE (Server-Sent Events) stream at the path `GET /listen_updates`. As querystring parameters it MUST expect `?session=<arbitrary id chosen by the client>` any number of `&key=<33-bytes hex-encoded pubkey>`.
6. A relay MUST serve an SSE (Server-Sent Events) stream at the path `GET /listen_updates`. As querystring parameters it MUST expect `?session=<arbitrary id chosen by the client>` any number of `&key=<32-bytes hex-encoded pubkey>`.
7. Immediately upon receiving that request the relay MUST return the recent past events from all the received authors with the event type `history`, then it SHOULD keep the connection open and return new events from these same keys as they are received with the event type `happening`. The format of the messages is just the same event object above, as JSON:
```

View File

@ -7,7 +7,7 @@ import (
"errors"
"fmt"
"github.com/btcsuite/btcd/btcec"
"github.com/fiatjaf/schnorr"
)
const (
@ -45,12 +45,8 @@ func (evt *Event) Serialize() ([]byte, error) {
if err != nil {
return nil, err
}
pubkey, err := btcec.ParsePubKey(pubkeyb, btcec.S256())
if err != nil {
return nil, fmt.Errorf("error parsing pubkey: %w", err)
}
if evt.PubKey != hex.EncodeToString(pubkey.SerializeCompressed()) {
return nil, fmt.Errorf("pubkey is not serialized in compressed format")
if len(pubkeyb) != 32 {
return nil, fmt.Errorf("pubkey must be 32 bytes, not %d", len(pubkeyb))
}
if _, err = b.Write(pubkeyb); err != nil {
return nil, err
@ -96,19 +92,35 @@ func (evt *Event) Serialize() ([]byte, error) {
// (which is a hash of the serialized event content).
// returns an error if the signature itself is invalid.
func (evt Event) CheckSignature() (bool, error) {
// validity of these is checked by Serialize()
pubkeyb, _ := hex.DecodeString(evt.PubKey)
pubkey, _ := btcec.ParsePubKey(pubkeyb, btcec.S256())
// validity of these is checked by Serialize(), which should be called first
pubkey, _ := hex.DecodeString(evt.PubKey)
bsig, err := hex.DecodeString(evt.Sig)
hash, _ := hex.DecodeString(evt.ID)
if len(hash) != 32 {
return false, fmt.Errorf("invalid event id/hash when checking signature (%s)",
evt.ID)
}
sig, err := hex.DecodeString(evt.Sig)
if err != nil {
return false, fmt.Errorf("signature is invalid hex: %w", err)
}
signature, err := btcec.ParseDERSignature(bsig, btcec.S256())
if err != nil {
return false, fmt.Errorf("failed to parse DER signature: %w", err)
if len(sig) != 64 {
return false, fmt.Errorf("signature must be 64 bytes, not %d", len(sig))
}
hash, _ := hex.DecodeString(evt.ID)
return signature.Verify(hash, pubkey), nil
var p [32]byte
for i, b := range pubkey {
p[i] = b
}
var h [32]byte
for i, b := range hash {
h[i] = b
}
var s [64]byte
for i, b := range sig {
s[i] = b
}
return schnorr.Verify(p, h, s)
}

View File

@ -1,9 +1,9 @@
module github.com/fiatjaf/profiles
module github.com/fiatjaf/nostr-relay
go 1.15
require (
github.com/btcsuite/btcd v0.21.0-beta
github.com/fiatjaf/schnorr v0.2.1-hack
github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/gorilla/mux v1.8.0
github.com/jmoiron/sqlx v1.2.0

View File

@ -1,10 +1,9 @@
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.21.0-beta h1:At9hIZdJW0s9E/fAz28nrz6AmcNlSVucCH796ZteX1M=
github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94=
github.com/btcsuite/btcd v0.0.0-20190109040709-5bda5314ca95 h1:bmv+LE3sbjb/M06u2DBi92imeKj7KnCUBOvyZYqI8d8=
github.com/btcsuite/btcd v0.0.0-20190109040709-5bda5314ca95/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts=
github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v0.0.0-20190112041146-bf1e1be93589/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
@ -16,7 +15,8 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/fiatjaf/schnorr v0.2.1-hack h1:6NwQNN5O4+ZUm8KliT+l198vDVH8ovv8AJ8CiL4hvs0=
github.com/fiatjaf/schnorr v0.2.1-hack/go.mod h1:6aMsVxPxyO6awpdmNkfkJ8vXqsmUOeGCHp2CdG5LPR0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
@ -33,6 +33,7 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
@ -50,24 +51,26 @@ github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs=
github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/antage/eventsource.v1 v1.0.0-20150318155416-803f4c5af225 h1:xy+AV3uSExoRQc2qWXeZdbhFGwBFK/AmGlrBZEjbvuQ=
gopkg.in/antage/eventsource.v1 v1.0.0-20150318155416-803f4c5af225/go.mod h1:SiXNRpUllqhl+GIw2V/BtKI7BUlz+uxov9vBFtXHqh8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -91,9 +91,9 @@ func listenUpdates(w http.ResponseWriter, r *http.Request) {
// past events
inkeys := make([]string, 0, len(keys))
for _, key := range keys {
// to prevent sql attack here we will check if these keys are valid 33-byte hex
// to prevent sql attack here we will check if these keys are valid 32-byte hex
parsed, err := hex.DecodeString(key)
if err != nil || len(parsed) != 33 {
if err != nil || len(parsed) != 32 {
continue
}
inkeys = append(inkeys, fmt.Sprintf("'%x'", parsed))

View File

@ -1,8 +1,9 @@
{
"dependencies": {
"assert": "^2.0.0",
"bip-schnorr": "github:fiatjaf/bip-schnorr",
"buffer": "^6.0.2",
"dexie": "^3.0.2",
"elliptic": "^6.5.3",
"insort": "^0.4.0",
"pretty-date": "^0.2.0",
"quick-lru": "^5.1.1",

View File

@ -1,7 +1,5 @@
import elliptic from 'elliptic'
import Dexie from 'dexie'
export const ec = new elliptic.ec('secp256k1')
export const db = new Dexie('db')
db.version(1).stores({

View File

@ -1,19 +1,41 @@
import shajs from 'sha.js'
import BigInteger from 'bigi'
import schnorr from 'bip-schnorr'
import {ec} from './globals'
import {db} from './globals'
export function makeRandom32() {
var array = new Uint32Array(32)
window.crypto.getRandomValues(array)
return Buffer.from(array)
}
export function pubkeyFromPrivate(privateHex) {
return schnorr.convert
.pubKeyFromPrivate(new BigInteger(privateHex, 16))
.toString('hex')
}
export function verifySignature(evt) {
return true // TODO
try {
schnorr.verify(
Buffer.from(evt.pubkey, 'hex'),
Buffer.from(evt.id, 'hex'),
Buffer.from(evt.sig, 'hex')
)
return true
} catch (err) {
return false
}
}
export function publishEvent(evt, key, hosts) {
let hash = shajs('sha256').update(serializeEvent(evt)).digest()
evt.id = hash.toString('hex')
evt.sig = ec
.keyFromPrivate(key, 'hex')
.sign(hash, {canonical: true})
.toDER('hex')
evt.sig = schnorr
.sign(new BigInteger(key, 16), hash, makeRandom32())
.toString('hex')
for (let i = 0; i < hosts.length; i++) {
let host = hosts[i]

View File

@ -2,8 +2,13 @@ import {createStore, createLogger} from 'vuex'
import {SortedMap} from 'insort'
import LRU from 'quick-lru'
import {verifySignature, publishEvent} from './helpers'
import {ec, db} from './globals'
import {
pubkeyFromPrivate,
makeRandom32,
verifySignature,
publishEvent
} from './helpers'
import {db} from './globals'
export default createStore({
plugins: (process.env.NODE_ENV !== 'production'
@ -29,7 +34,7 @@ export default createStore({
haveEventSource,
session: new Date().getTime() + '' + Math.round(Math.random() * 100000),
relays,
key: ec.genKeyPair().getPrivate('hex'),
key: makeRandom32().toString('hex'),
following: [],
home: new SortedMap(),
metadata: new LRU({maxSize: 100}),
@ -37,8 +42,7 @@ export default createStore({
}
},
getters: {
pubKeyHex: state =>
ec.keyFromPrivate(state.key, 'hex').getPublic(true, 'hex'),
pubKeyHex: state => pubkeyFromPrivate(state.key),
writeServers: state =>
state.relays
.filter(({policy}) => policy.indexOf('w') !== -1)
@ -54,7 +58,7 @@ export default createStore({
state.key = key
state.following = following.concat(
// always be following thyself
ec.keyFromPrivate(state.key, 'hex').getPublic(true, 'hex')
pubkeyFromPrivate(state.key)
)
state.home = home
state.metadata = metadata