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 }