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

     1  // Copyright (c) 2017-2019 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  	"bytes"
     9  	"crypto/sha256"
    10  	"encoding/base64"
    11  	"encoding/hex"
    12  	"fmt"
    13  
    14  	"github.com/decred/dcrtime/merkle"
    15  	"github.com/decred/politeia/politeiad/api/v1/identity"
    16  	v1 "github.com/decred/politeia/politeiawww/api/www/v1"
    17  	"github.com/decred/politeia/politeiawww/cmd/shared"
    18  	"github.com/decred/politeia/util"
    19  )
    20  
    21  // BatchProposalsCmd retrieves a set of proposals.
    22  type BatchProposalsCmd struct{}
    23  
    24  // Execute executes the batch proposals command.
    25  func (cmd *BatchProposalsCmd) Execute(args []string) error {
    26  	// Get server's public key
    27  	vr, err := client.Version()
    28  	if err != nil {
    29  		return err
    30  	}
    31  
    32  	// Get proposals
    33  	bpr, err := client.BatchProposals(&v1.BatchProposals{
    34  		Tokens: args,
    35  	})
    36  	if err != nil {
    37  		return err
    38  	}
    39  
    40  	// Verify proposal censorship records
    41  	for _, p := range bpr.Proposals {
    42  		err = verifyProposal(p, vr.PubKey)
    43  		if err != nil {
    44  			return fmt.Errorf("unable to verify proposal %v: %v",
    45  				p.CensorshipRecord.Token, err)
    46  		}
    47  	}
    48  
    49  	// Print proposals
    50  	return shared.PrintJSON(bpr)
    51  }
    52  
    53  // batchProposalsHelpMsg is the output for the help command when
    54  // 'batchproposals' is specified.
    55  const batchProposalsHelpMsg = `batchproposals
    56  
    57  Fetch a list of proposals.
    58  
    59  Example:
    60  batchproposals token1 token2
    61  
    62  Result:
    63  {
    64    "proposals": [
    65      {
    66      "name":          (string)  Suggested short proposal name 
    67      "state":         (PropStateT)  Current state of proposal
    68      "status":        (PropStatusT)  Current status of proposal
    69      "timestamp":     (int64)  Timestamp of last update of proposal
    70      "userid":        (string)  ID of user who submitted proposal
    71      "username":      (string)  Username of user who submitted proposal
    72      "publickey":     (string)  Public key used to sign proposal
    73      "signature":     (string)  Signature of merkle root
    74      "files": [],
    75      "numcomments":   (uint)  Number of comments on the proposal
    76      "version": 		 (string)  Version of proposal
    77      "censorshiprecord": {	
    78        "token":       (string)  Censorship token
    79        "merkle":      (string)  Merkle root of proposal
    80        "signature":   (string)  Server side signature of []byte(Merkle+Token)
    81        }
    82      }
    83    ]
    84  }`
    85  
    86  func merkleRoot(files []v1.File, md []v1.Metadata) (string, error) {
    87  	digests := make([]*[sha256.Size]byte, 0, len(files))
    88  
    89  	// Calculate file digests
    90  	for _, f := range files {
    91  		b, err := base64.StdEncoding.DecodeString(f.Payload)
    92  		if err != nil {
    93  			return "", err
    94  		}
    95  		digest := util.Digest(b)
    96  		var hf [sha256.Size]byte
    97  		copy(hf[:], digest)
    98  		digests = append(digests, &hf)
    99  	}
   100  
   101  	// Calculate metadata digests
   102  	for _, v := range md {
   103  		b, err := base64.StdEncoding.DecodeString(v.Payload)
   104  		if err != nil {
   105  			return "", err
   106  		}
   107  		digest := util.Digest(b)
   108  		var hv [sha256.Size]byte
   109  		copy(hv[:], digest)
   110  		digests = append(digests, &hv)
   111  	}
   112  
   113  	// Return merkle root
   114  	return hex.EncodeToString(merkle.Root(digests)[:]), nil
   115  }
   116  
   117  // validateDigests receives a list of files and metadata to verify their
   118  // digests. It compares digests that came with the file/md with digests
   119  // calculated from their respective payloads.
   120  func validateDigests(files []v1.File, md []v1.Metadata) error {
   121  	// Validate file digests
   122  	for _, f := range files {
   123  		b, err := base64.StdEncoding.DecodeString(f.Payload)
   124  		if err != nil {
   125  			return fmt.Errorf("file: %v decode payload err %v",
   126  				f.Name, err)
   127  		}
   128  		digest := util.Digest(b)
   129  		d, ok := util.ConvertDigest(f.Digest)
   130  		if !ok {
   131  			return fmt.Errorf("file: %v invalid digest %v",
   132  				f.Name, f.Digest)
   133  		}
   134  		if !bytes.Equal(digest, d[:]) {
   135  			return fmt.Errorf("file: %v digests do not match",
   136  				f.Name)
   137  		}
   138  	}
   139  	// Validate metadata digests
   140  	for _, v := range md {
   141  		b, err := base64.StdEncoding.DecodeString(v.Payload)
   142  		if err != nil {
   143  			return fmt.Errorf("metadata: %v decode payload err %v",
   144  				v.Hint, err)
   145  		}
   146  		digest := util.Digest(b)
   147  		d, ok := util.ConvertDigest(v.Digest)
   148  		if !ok {
   149  			return fmt.Errorf("metadata: %v invalid digest %v",
   150  				v.Hint, v.Digest)
   151  		}
   152  		if !bytes.Equal(digest, d[:]) {
   153  			return fmt.Errorf("metadata: %v digests do not match metadata",
   154  				v.Hint)
   155  		}
   156  	}
   157  	return nil
   158  }
   159  
   160  // verifyProposal verifies a proposal's merkle root, author signature, and
   161  // censorship record.
   162  func verifyProposal(p v1.ProposalRecord, serverPubKey string) error {
   163  	if len(p.Files) > 0 {
   164  		// Verify digests
   165  		err := validateDigests(p.Files, p.Metadata)
   166  		if err != nil {
   167  			return err
   168  		}
   169  		// Verify merkle root
   170  		mr, err := merkleRoot(p.Files, p.Metadata)
   171  		if err != nil {
   172  			return err
   173  		}
   174  		if mr != p.CensorshipRecord.Merkle {
   175  			return fmt.Errorf("merkle roots do not match")
   176  		}
   177  	}
   178  
   179  	// Verify proposal signature
   180  	pid, err := identity.PublicIdentityFromString(p.PublicKey)
   181  	if err != nil {
   182  		return err
   183  	}
   184  	sig, err := util.ConvertSignature(p.Signature)
   185  	if err != nil {
   186  		return err
   187  	}
   188  	if !pid.VerifyMessage([]byte(p.CensorshipRecord.Merkle), sig) {
   189  		return fmt.Errorf("could not verify proposal signature")
   190  	}
   191  
   192  	// Verify censorship record signature
   193  	id, err := identity.PublicIdentityFromString(serverPubKey)
   194  	if err != nil {
   195  		return err
   196  	}
   197  	s, err := util.ConvertSignature(p.CensorshipRecord.Signature)
   198  	if err != nil {
   199  		return err
   200  	}
   201  	msg := []byte(p.CensorshipRecord.Merkle + p.CensorshipRecord.Token)
   202  	if !id.VerifyMessage(msg, s) {
   203  		return fmt.Errorf("could not verify censorship record signature")
   204  	}
   205  
   206  	return nil
   207  }