Skip to content
Permalink
Browse files

Add independent anchor+merkle checker (#49)

  • Loading branch information...
marcopeereboom committed Mar 29, 2019
1 parent 435250a commit 56572d4777019bd118e58f206616d1ab1d15e3f6
Showing with 303 additions and 26 deletions.
  1. +7 −20 cmd/dcrtime/dcrtime.go
  2. +56 −0 cmd/dcrtime_checker/README.md
  3. +143 −0 cmd/dcrtime_checker/dcrtime_checker.go
  4. +2 −2 go.mod
  5. +4 −4 go.sum
  6. +68 −0 util/dcrdata.go
  7. +23 −0 util/files.go
@@ -19,8 +19,9 @@ import (
"os"
"strconv"

"github.com/decred/dcrtime/api/v1"
v1 "github.com/decred/dcrtime/api/v1"
"github.com/decred/dcrtime/merkle"
"github.com/decred/dcrtime/util"
)

const (
@@ -29,7 +30,8 @@ const (

var (
testnet = flag.Bool("testnet", false, "Use testnet port")
printJson = flag.Bool("json", false, "Print JSON")
debug = flag.Bool("debug", false, "Print JSON that is sent to server")
printJson = flag.Bool("json", false, "Print JSON response from server")
fileOnly = flag.Bool("file", false, "Treat digests and timestamps "+
"as file names")
host = flag.String("h", "", "Timestamping host")
@@ -75,21 +77,6 @@ func isTimestamp(timestamp string) bool {
return v1.RegexpTimestamp.MatchString(timestamp)
}

// digestFile returns the SHA256 of a file.
func digestFile(filename string) (string, error) {
h := sha256.New()
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close()
if _, err = io.Copy(h, f); err != nil {
return "", err
}

return hex.EncodeToString(h.Sum(nil)), nil
}

// getError returns the error that is embedded in a JSON reply.
func getError(r io.Reader) (string, error) {
var e interface{}
@@ -171,7 +158,7 @@ func download(questions []string) error {
return err
}

if *printJson {
if *debug {
fmt.Println(string(b))
}

@@ -328,7 +315,7 @@ func upload(digests []string, exists map[string]string) error {
return err
}

if *printJson {
if *debug {
fmt.Println(string(b))
}

@@ -418,7 +405,7 @@ func _main() error {
for _, a := range flag.Args() {
// Try to see if argument is a valid file.
if isFile(a) || *fileOnly {
d, err := digestFile(a)
d, err := util.DigestFile(a)
if err != nil {
return err
}
@@ -0,0 +1,56 @@
dcrtime_checker
==============

There are circumstances where one may want to verify a prior server anchor
against the blockchain directly. This tool takes the original anchor response
and verifies proof of existence using the following process:
1. Ensure file digest is included in the anchor record
2. Verify the merkle root and path
3. Verify that the anchor exists in the blockchain


## Flags

```
-f Original filename
-h Non default block explorer host. Defaults based on -testnet flag.
-p Original JSON anchor record
-testnet Use testnet.
-v Verbose
```

## Important

One *must* store the original anchor record in order to use this tool.

For example, use the dcrtime command line utility to verify that the record has
been anchored.
```
$ dcrtime -v d1721918b1acc9af5db62947a7ae52738b7c4c55e2d1189c506beb72d1079517
d1721918b1acc9af5db62947a7ae52738b7c4c55e2d1189c506beb72d1079517 Verify
d1721918b1acc9af5db62947a7ae52738b7c4c55e2d1189c506beb72d1079517 OK
Chain Timestamp: 1531224216
Merkle Root : fe9680ca605b6af488b8ded0d5ef28506758745eedca56c220d6b7c89226c85c
TxID : 0066578e01764c65fbc48870d8315d9bb7ead3a25e0318a0d8e7fdb52ef4ff87
```

Store proof:
```
$ dcrtime -json d1721918b1acc9af5db62947a7ae52738b7c4c55e2d1189c506beb72d1079517 > proof.json
```

This proof looks like this:
```
{"id":"dcrtime cli","digests":[{"digest":"d1721918b1acc9af5db62947a7ae52738b7c4c55e2d1189c506beb72d1079517","servertimestamp":1553799600,"result":0,"chaininformation":{"chaintimestamp":1531224216,"transaction":"0066578e01764c65fbc48870d8315d9bb7ead3a25e0318a0d8e7fdb52ef4ff87","merkleroot":"fe9680ca605b6af488b8ded0d5ef28506758745eedca56c220d6b7c89226c85c","merklepath":{"NumLeaves":3,"Hashes":[[127,70,80,89,228,171,127,105,76,251,211,241,162,43,49,165,124,141,27,134,120,100,70,162,253,181,237,147,238,244,53,98],[209,114,25,24,177,172,201,175,93,182,41,71,167,174,82,115,139,124,76,85,226,209,24,156,80,107,235,114,209,7,149,23]],"Flags":"DQ=="}}}],"timestamps":[]}
```

## Examples

Verify proof of existence:
```
$ dcrtime_checker -v -f LICENSE -p proof.json
d1721918b1acc9af5db62947a7ae52738b7c4c55e2d1189c506beb72d1079517 LICENSE
d1721918b1acc9af5db62947a7ae52738b7c4c55e2d1189c506beb72d1079517 Proof OK
d1721918b1acc9af5db62947a7ae52738b7c4c55e2d1189c506beb72d1079517 Anchor OK
```

@@ -0,0 +1,143 @@
package main

import (
"bytes"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"os"
"strings"

v1 "github.com/decred/dcrtime/api/v1"
"github.com/decred/dcrtime/merkle"
"github.com/decred/dcrtime/util"
)

var (
proof = flag.String("p", "", "Proof file")
file = flag.String("f", "", "Original file")
dcrdataHost = flag.String("h", "", "dcrdata host")
testnet = flag.Bool("testnet", false, "Use testnet port")
verbose = flag.Bool("v", false, "Verbose")
)

func _main() error {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "dcrtime_checker [-h {dcrdatahost}|"+
"-testnet|-v] -f {file} -p {proof}\n\n")
flag.PrintDefaults()
}
flag.Parse()

// require -f
if *file == "" {
return fmt.Errorf("must provide -f")
}

// require -p
if *proof == "" {
return fmt.Errorf("must provide -p")
}

// Ensure proof looks correct
fProof, err := os.Open(*proof)
if err != nil {
return err
}
var vr v1.VerifyReply
decoder := json.NewDecoder(fProof)
if err := decoder.Decode(&vr); err != nil {
return fmt.Errorf("Could node decode VerifyReply: %v", err)
}

// Handle dcrtime host
if *dcrdataHost == "" {
if *testnet {
*dcrdataHost = "https://testnet.dcrdata.org/api/tx/"
} else {
*dcrdataHost = "https://explorer.dcrdata.org/api/tx/"
}
} else {
if !strings.HasSuffix(*dcrdataHost, "/") {
*dcrdataHost += "/"
}
}
// Get file digest
d, err := util.DigestFile(*file)
if err != nil {
return err
}

if *verbose {
fmt.Printf("%v %v\n", d, *file)
}

// Ensure file digest exists in the proof and that the saved answer was
// correct
found := -1
for k, v := range vr.Digests {
if v.Digest != d {
continue
}

found = k
break
}
if found == -1 {
return fmt.Errorf("file digest not found in proof")
}
v := vr.Digests[found]

// Verify result of matching digest
if _, ok := v1.Result[v.Result]; !ok {
return fmt.Errorf("%v invalid error code %v\n", v.Digest,
v.Result)
}

// Verify merkle path.
root, err := merkle.VerifyAuthPath(&v.ChainInformation.MerklePath)
if err != nil {
if err != merkle.ErrEmpty {
return fmt.Errorf("%v invalid auth path %v\n",
v.Digest, err)
}
return fmt.Errorf("%v Not anchored\n", v.Digest)
}

// Verify merkle root.
merkleRoot, err := hex.DecodeString(v.ChainInformation.MerkleRoot)
if err != nil {
return fmt.Errorf("invalid merkle root: %v\n", err)
}
// This is silly since we check against returned root.
if !bytes.Equal(root[:], merkleRoot) {
return fmt.Errorf("%v invalid merkle root\n", v.Digest)
}

// If we made it here we have a valid proof
if *verbose {
fmt.Printf("%v Proof OK\n", d)
}

// Verify against dcrdata
err = util.VerifyAnchor(*dcrdataHost,
vr.Digests[found].ChainInformation.Transaction, root[:])
if err != nil {
return err
}

if *verbose {
fmt.Printf("%v Anchor OK\n", d)
}

return nil
}

func main() {
err := _main()
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
4 go.mod
@@ -22,9 +22,9 @@ require (
github.com/jessevdk/go-flags v1.4.0
github.com/jrick/logrotate v1.0.0
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
github.com/syndtr/goleveldb v0.0.0-20190203031304-2f17a3356c66
github.com/syndtr/goleveldb v1.0.0
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 // indirect
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 // indirect
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd // indirect
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952 // indirect
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 // indirect
8 go.sum
@@ -235,8 +235,8 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/syndtr/goleveldb v0.0.0-20190203031304-2f17a3356c66 h1:AwmkkZT+TucFotNCL+aNJ/0KCMsRtlXN9fs8uoOMSRk=
github.com/syndtr/goleveldb v0.0.0-20190203031304-2f17a3356c66/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/vmihailenco/msgpack v4.0.1+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
go.etcd.io/bbolt v1.3.0/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -263,8 +263,8 @@ golang.org/x/net v0.0.0-20181207154023-610586996380 h1:zPQexyRtNYBc7bcHmehl1dH6T
golang.org/x/net v0.0.0-20181207154023-610586996380/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181217023233-e147a9138326 h1:iCzOf0xz39Tstp+Tu/WwyGjUXCk34QhQORRxBeXXTA4=
golang.org/x/net v0.0.0-20181217023233-e147a9138326/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -0,0 +1,68 @@
package util

import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"

"github.com/decred/dcrd/txscript"
"github.com/decred/dcrdata/v4/api/types"
)

// VerifyAnchor verifies proof of existence of the supplied merkle root on the
// blockchain.
func VerifyAnchor(url, tx string, mr []byte) error {
u := url + tx + "/out"
r, err := http.Get(u)
if err != nil {
return fmt.Errorf("HTTP Get: %v", err)
}
defer r.Body.Close()

if r.StatusCode != http.StatusOK {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return fmt.Errorf("invalid body: %v %v",
r.StatusCode, body)
}
return fmt.Errorf("invalid dcrdata answer: %v %s",
r.StatusCode, body)
}

var txOuts []types.TxOut
d := json.NewDecoder(r.Body)
if err := d.Decode(&txOuts); err != nil {
return err
}

var done bool
for _, v := range txOuts {
if !types.IsNullDataScript(v.ScriptPubKeyDecoded.Type) {
continue
}
script, err := hex.DecodeString(v.ScriptPubKeyDecoded.Hex)
if err != nil {
return fmt.Errorf("invalid dcrdata script: %v", err)
}
data, err := txscript.PushedData(script)
if err != nil {
return fmt.Errorf("invalid script: %v", err)
}
if !bytes.Equal(data[0], mr) {
continue
}

// Everything is cool so mark it and break out
done = true
break
}
if !done {
return fmt.Errorf("merkle root not found: tx %v merkle %x",
tx, mr)
}

return nil
}
@@ -0,0 +1,23 @@
package util

import (
"crypto/sha256"
"encoding/hex"
"io"
"os"
)

// DigestFile returns the SHA256 of a file.
func DigestFile(filename string) (string, error) {
h := sha256.New()
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close()
if _, err = io.Copy(h, f); err != nil {
return "", err
}

return hex.EncodeToString(h.Sum(nil)), nil
}

0 comments on commit 56572d4

Please sign in to comment.
You can’t perform that action at this time.