github.com/decred/politeia@v1.4.0/politeiawww/cmd/politeiaverify/politeiaverify.go (about)

     1  // Copyright (c) 2020-2021 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"encoding/hex"
     9  	"flag"
    10  	"fmt"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  
    15  	"github.com/decred/politeia/politeiad/api/v1/identity"
    16  	rcv1 "github.com/decred/politeia/politeiawww/api/records/v1"
    17  	"github.com/decred/politeia/politeiawww/client"
    18  	"github.com/decred/politeia/util"
    19  )
    20  
    21  var (
    22  	// CLI flags
    23  	publicKey = flag.String("k", "", "server public key")
    24  	token     = flag.String("t", "", "record censorship token")
    25  	signature = flag.String("s", "", "record censorship signature")
    26  )
    27  
    28  // loadFiles loads and returns a politeiawww records v1 File for each provided
    29  // file path.
    30  func loadFiles(paths []string) ([]rcv1.File, error) {
    31  	files := make([]rcv1.File, 0, len(paths))
    32  	for _, fp := range paths {
    33  		fp = util.CleanAndExpandPath(fp)
    34  		mime, digest, payload, err := util.LoadFile(fp)
    35  		if err != nil {
    36  			return nil, err
    37  		}
    38  		files = append(files, rcv1.File{
    39  			Name:    filepath.Base(fp),
    40  			MIME:    mime,
    41  			Digest:  digest,
    42  			Payload: payload,
    43  		})
    44  	}
    45  	return files, nil
    46  }
    47  
    48  // verifyCensorshipRecord verifies a censorship record signature for a politeia
    49  // record submission. This requires passing in the server public key, the
    50  // censorship token, the censorship record signature, and the file paths of all
    51  // files that are part of the record.
    52  func verifyCensorshipRecord(serverPubKey, token, signature string, filepaths []string) error {
    53  	// Verify all args are present
    54  	switch {
    55  	case serverPubKey == "":
    56  		return fmt.Errorf("server public key not provided")
    57  	case token == "":
    58  		return fmt.Errorf("censorship token not provided")
    59  	case signature == "":
    60  		return fmt.Errorf("censorship record signature not provided")
    61  	case len(filepaths) == 0:
    62  		return fmt.Errorf("record files not provided")
    63  	}
    64  
    65  	// Load record files
    66  	files, err := loadFiles(filepaths)
    67  	if err != nil {
    68  		return err
    69  	}
    70  
    71  	// Calc merkle root of files
    72  	digests := make([]string, 0, len(files))
    73  	for _, v := range files {
    74  		digests = append(digests, v.Digest)
    75  	}
    76  	mr, err := util.MerkleRoot(digests)
    77  	if err != nil {
    78  		return err
    79  	}
    80  	merkle := hex.EncodeToString(mr[:])
    81  
    82  	// Load identity
    83  	pid, err := identity.PublicIdentityFromString(serverPubKey)
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	// Verify record
    89  	r := rcv1.Record{
    90  		Files: files,
    91  		CensorshipRecord: rcv1.CensorshipRecord{
    92  			Token:     token,
    93  			Merkle:    merkle,
    94  			Signature: signature,
    95  		},
    96  	}
    97  	err = client.RecordVerify(r, pid.String())
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	fmt.Printf("Server key : %s\n", serverPubKey)
   103  	fmt.Printf("Token      : %s\n", token)
   104  	fmt.Printf("Merkle root: %s\n", merkle)
   105  	fmt.Printf("Signature  : %s\n\n", signature)
   106  	fmt.Println("Record successfully verified")
   107  
   108  	return nil
   109  }
   110  
   111  var (
   112  	// Regexps for matching file bundles downloaded from politeiagui
   113  	expJSONFile          = `.json$`
   114  	expRecord            = `^[0-9a-f]{7,16}-v[\d]{1,2}.json$`
   115  	expRecordTimestamps  = `^[0-9a-f]{7,16}-v[\d]{1,2}-timestamps.json$`
   116  	expComments          = `^[0-9a-f]{7,16}-comments.json$`
   117  	expCommentTimestamps = `^[0-9a-f]{7,16}-comments-timestamps.json$`
   118  	expVotes             = `^[0-9a-f]{7,16}-votes.json$`
   119  	expVoteTimestamps    = `^[0-9a-f]{7,16}-votes-timestamps.json$`
   120  
   121  	regexpJSONFile          = regexp.MustCompile(expJSONFile)
   122  	regexpRecord            = regexp.MustCompile(expRecord)
   123  	regexpRecordTimestamps  = regexp.MustCompile(expRecordTimestamps)
   124  	regexpComments          = regexp.MustCompile(expComments)
   125  	regexpCommentTimestamps = regexp.MustCompile(expCommentTimestamps)
   126  	regexpVotes             = regexp.MustCompile(expVotes)
   127  	regexpVoteTimestamps    = regexp.MustCompile(expVoteTimestamps)
   128  )
   129  
   130  // verifyFile verifies a data file downloaded from politeiagui. This can be
   131  // one of the data bundles or one of the timestamp files. The file name MUST
   132  // be the same file name that was downloaded from politeiagui.
   133  //
   134  // Files that this function accepts:
   135  // Record bundle     : [token]-[version].json
   136  // Record timestamps : [token]-[version]-timestamps.json
   137  // Comments bundle   : [token]-comments.json
   138  // Comment timestamps: [token]-comments-timestamps.json
   139  // Votes bundle      : [token]-votes.json
   140  // Vote timestamps   : [token]-votes-timestamps.json
   141  func verifyFile(fp string) error {
   142  	fp = util.CleanAndExpandPath(fp)
   143  	filename := filepath.Base(fp)
   144  
   145  	// Match file type
   146  	switch {
   147  	case regexpRecord.FindString(filename) != "":
   148  		return verifyRecordBundle(fp)
   149  	case regexpRecordTimestamps.FindString(filename) != "":
   150  		return verifyRecordTimestamps(fp)
   151  	case regexpComments.FindString(filename) != "":
   152  		return verifyCommentsBundle(fp)
   153  	case regexpCommentTimestamps.FindString(filename) != "":
   154  		return verifyCommentTimestamps(fp)
   155  	case regexpVotes.FindString(filename) != "":
   156  		return verifyVotesBundle(fp)
   157  	case regexpVoteTimestamps.FindString(filename) != "":
   158  		return verifyVoteTimestamps(fp)
   159  	}
   160  
   161  	return fmt.Errorf("file not recognized")
   162  }
   163  
   164  func _main() error {
   165  	// Parse CLI arguments
   166  	flag.Parse()
   167  	args := flag.Args()
   168  	if len(args) == 0 {
   169  		return fmt.Errorf("no arguments provided")
   170  	}
   171  
   172  	// Check if the user is trying to verify a record submission
   173  	// manually. This requires passing in the server public key, the
   174  	// censorship token, the censorship record signature, and all of
   175  	// the record filepaths.
   176  	manual := (*publicKey != "") || (*token != "") || (*signature != "")
   177  	if manual {
   178  		// The user is trying to verify manually
   179  		return verifyCensorshipRecord(*publicKey, *token, *signature, args)
   180  	}
   181  
   182  	// The user is trying to verify a bundle file that was downloaded
   183  	// from politeiagui.
   184  	fp := args[0]
   185  	if regexpJSONFile.FindString(fp) == "" {
   186  		return fmt.Errorf("'%v' is not a json file", fp)
   187  	}
   188  	err := verifyFile(fp)
   189  	if err != nil {
   190  		return fmt.Errorf("ERR COULD NOT VERIFY: %v", err)
   191  	}
   192  
   193  	return nil
   194  }
   195  
   196  func main() {
   197  	err := _main()
   198  	if err != nil {
   199  		fmt.Fprintf(os.Stderr, "%v\n", err)
   200  		os.Exit(1)
   201  	}
   202  }