Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cmswww/api/v1/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ const (
// values for each line item in the CSV.
PolicyInvoiceFieldDelimiterChar rune = ','

// PolicyMaxAttachments is the maximum number of attachments that a
// contractor can submit along with an invoice.
PolicyMaxAttachments = 10

// PolicyMaxAttachmentSize is the maximum file size (in bytes)
// accepted for an attachment when creating a new invoice.
PolicyMaxAttachmentSize = 512 * 1024

// ListPageSize is the maximum number of entries returned
// for the routes that return lists
ListPageSize = 25
Expand Down
4 changes: 4 additions & 0 deletions cmswww/api/v1/enums.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const (
ErrorStatusMalformedInvoiceFile ErrorStatusT = 27
ErrorStatusInvoicePaymentNotFound ErrorStatusT = 28
ErrorStatusDuplicateInvoice ErrorStatusT = 29
ErrorStatusMaxAttachmentsExceeded ErrorStatusT = 30
ErrorStatusMaxAttachmentSizeExceeded ErrorStatusT = 31

// Invoice status codes
InvoiceStatusInvalid InvoiceStatusT = 0 // Invalid status
Expand Down Expand Up @@ -97,6 +99,8 @@ var (
ErrorStatusMalformedInvoiceFile: "malformed invoice file",
ErrorStatusInvoicePaymentNotFound: "invoice payment not found",
ErrorStatusDuplicateInvoice: "duplicate invoice for this month and year",
ErrorStatusMaxAttachmentsExceeded: "max number of attachments exceeded",
ErrorStatusMaxAttachmentSizeExceeded: "max attachment size exceeded",
}

// InvoiceStatus converts propsal status codes to human readable text
Expand Down
8 changes: 5 additions & 3 deletions cmswww/api/v1/route_protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ var (
// directory structure must be flattened. The server side SHALL verify MIME
// and Digest.
type File struct {
Name string `json:"name"` // Filename
MIME string `json:"mime"` // MIME type of file
Digest string `json:"digest"` // Digest of unencoded payload
Payload string `json:"payload"` // File content, base64 encoded
}
Expand Down Expand Up @@ -70,7 +72,7 @@ type InvoiceRecord struct {
Username string `json:"username"` // Username of user who submitted invoice
PublicKey string `json:"publickey"` // User's public key, used to verify signature.
Signature string `json:"signature"` // Signature of file digest
File *File `json:"file"` // Actual invoice file
Files []File `json:"files"` // Actual invoice file and attachments
Version string `json:"version"` // Record version

CensorshipRecord CensorshipRecord `json:"censorshiprecord"`
Expand Down Expand Up @@ -207,7 +209,7 @@ type LogoutReply struct{}
type SubmitInvoice struct {
Month uint16 `json:"month"`
Year uint16 `json:"year"`
File File `json:"file"` // Invoice file
Files []File `json:"files"` // Invoice file and any attachments
PublicKey string `json:"publickey"` // Key used to verify signature
Signature string `json:"signature"` // Signature of file hash
}
Expand All @@ -220,7 +222,7 @@ type SubmitInvoiceReply struct {
// EditInvoice attempts to submit an edit to an existing invoice.
type EditInvoice struct {
Token string `json:"token"` // Invoice token
File File `json:"file"` // Invoice file
Files []File `json:"files"` // Invoice file and any attachments
PublicKey string `json:"publickey"` // Key used to verify signature
Signature string `json:"signature"` // Signature of file hash

Expand Down
58 changes: 50 additions & 8 deletions cmswww/cmd/cmswwwcli/client/util.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package client

import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"

"github.com/decred/dcrtime/merkle"
"github.com/decred/politeia/politeiad/api/v1/identity"
"github.com/decred/politeia/util"

Expand All @@ -28,16 +30,56 @@ func invoiceSignature(file v1.File, id *identity.FullIdentity) (string, error) {
return hex.EncodeToString(sig[:]), nil
}

// verifyProposal verifies the integrity of a proposal by verifying the
// proposal's merkle root (if the files are present), the proposal signature,
// merkleRoot converts the passed in list of files into SHA256 digests, then
// calculates and returns the merkle root of the digests.
func merkleRoot(files []v1.File) (string, error) {
if len(files) == 0 {
return "", fmt.Errorf("no invoice files found")
}

digests := make([]*[sha256.Size]byte, len(files))
for i, f := range files {
d, ok := util.ConvertDigest(f.Digest)
if !ok {
return "", fmt.Errorf("could not convert digest")
}
digests[i] = &d
}

return hex.EncodeToString(merkle.Root(digests)[:]), nil
}

// SignMerkleRoot calculates the merkle root of the passed in list of files,
// signs the merkle root with the passed in identity and returns the signature.
func SignMerkleRoot(files []v1.File, id *identity.FullIdentity) (string, error) {
if len(files) == 0 {
return "", fmt.Errorf("no invoice files found")
}
mr, err := merkleRoot(files)
if err != nil {
return "", err
}
sig := id.SignMessage([]byte(mr))
return hex.EncodeToString(sig[:]), nil
}

// VerifyInvoice verifies the integrity of an invoice by verifying the
// invoice's merkle root (if the files are present), the signature,
// and the censorship record signature.
func verifyProposal(record v1.InvoiceRecord, serverPubKey string) error {
// Verify the file digest.
if record.File.Digest != record.CensorshipRecord.Merkle {
return fmt.Errorf("digests do not match")
func VerifyInvoice(record v1.InvoiceRecord, serverPubKey string) error {
// Verify merkle root if the invoice files are present.
if len(record.Files) > 0 {
mr, err := merkleRoot(record.Files)
if err != nil {
return err
}
if mr != record.CensorshipRecord.Merkle {
return fmt.Errorf("merkle roots do not match; expected %v, got %v",
mr, record.CensorshipRecord.Merkle)
}
}

// Verify proposal signature.
// Verify invoice signature.
pid, err := util.IdentityFromString(record.PublicKey)
if err != nil {
return err
Expand All @@ -47,7 +89,7 @@ func verifyProposal(record v1.InvoiceRecord, serverPubKey string) error {
return err
}
if !pid.VerifyMessage([]byte(record.CensorshipRecord.Merkle), sig) {
return fmt.Errorf("could not verify proposal signature")
return fmt.Errorf("could not verify invoice signature")
}

// Verify censorship record signature.
Expand Down
4 changes: 2 additions & 2 deletions cmswww/cmd/cmswwwcli/commands/changepassword.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ func (cmd *ChangePasswordCmd) Execute(args []string) error {

// Send the change password request.
cp := v1.ChangePassword{
CurrentPassword: cmd.Args.CurrentPassword,
NewPassword: cmd.Args.NewPassword,
CurrentPassword: DigestSHA3(cmd.Args.CurrentPassword),
NewPassword: DigestSHA3(cmd.Args.NewPassword),
}

var cpr v1.ChangePasswordReply
Expand Down
17 changes: 12 additions & 5 deletions cmswww/cmd/cmswwwcli/commands/editinvoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io/ioutil"

"github.com/decred/contractor-mgmt/cmswww/api/v1"
"github.com/decred/contractor-mgmt/cmswww/cmd/cmswwwcli/client"
"github.com/decred/contractor-mgmt/cmswww/cmd/cmswwwcli/config"
)

Expand Down Expand Up @@ -63,10 +64,10 @@ func (cmd *EditInvoiceCmd) Execute(args []string) error {

ei := v1.EditInvoice{
Token: token,
File: v1.File{
Files: []v1.File{{
Digest: digest,
Payload: base64.StdEncoding.EncodeToString(payload),
},
}},
PublicKey: hex.EncodeToString(id.Public.Key[:]),
Signature: hex.EncodeToString(signature[:]),
}
Expand All @@ -77,9 +78,15 @@ func (cmd *EditInvoiceCmd) Execute(args []string) error {
return err
}

if eir.Invoice.CensorshipRecord.Merkle != digest {
return fmt.Errorf("Digest returned from server did not match client's"+
" digest: %v %v", digest, eir.Invoice.CensorshipRecord.Merkle)
ir := v1.InvoiceRecord{
Files: ei.Files,
PublicKey: ei.PublicKey,
Signature: ei.Signature,
CensorshipRecord: eir.Invoice.CensorshipRecord,
}
err = client.VerifyInvoice(ir, config.ServerPublicKey)
if err != nil {
return err
}

// Store the revision record in case the submitter ever needs it.
Expand Down
15 changes: 15 additions & 0 deletions cmswww/cmd/cmswwwcli/commands/invoicedetails.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ func (cmd *InvoiceDetailsCmd) Execute(args []string) error {
fmt.Printf(" Submitted by: %v\n", idr.Invoice.Username)
fmt.Printf(" at: %v\n", time.Unix(idr.Invoice.Timestamp, 0))
fmt.Printf(" For: %v\n", date.Format("January 2006"))

var attachments string
for idx, file := range idr.Invoice.Files {
if idx == 0 {
continue
}

if idx == 1 {
attachments += fmt.Sprintf("%v %v\n", idx, file.Name)
} else {
attachments += fmt.Sprintf(" %v %v\n", idx,
file.Name)
}
}
fmt.Printf(" Attachments: %v\n", attachments)
}

return nil
Expand Down
4 changes: 2 additions & 2 deletions cmswww/cmd/cmswwwcli/commands/resetpassword.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (cmd *ResetPasswordCmd) Execute(args []string) error {

if cmd.Token != "" {
rp.VerificationToken = cmd.Token
rp.NewPassword = cmd.NewPassword
rp.NewPassword = DigestSHA3(cmd.NewPassword)
}

var rpr v1.ResetPasswordReply
Expand All @@ -65,7 +65,7 @@ func (cmd *ResetPasswordCmd) Execute(args []string) error {
// Automatic 2nd reset password call
rp = &v1.ResetPassword{
Email: cmd.Args.Email,
NewPassword: cmd.NewPassword,
NewPassword: DigestSHA3(cmd.NewPassword),
VerificationToken: rpr.VerificationToken,
}

Expand Down
85 changes: 59 additions & 26 deletions cmswww/cmd/cmswwwcli/commands/submitinvoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,32 @@ package commands

import (
"bufio"
"crypto/sha256"
"encoding/base64"
"encoding/csv"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"

"github.com/decred/politeia/politeiad/api/v1/mime"
"github.com/decred/politeia/util"

"github.com/decred/contractor-mgmt/cmswww/api/v1"
"github.com/decred/contractor-mgmt/cmswww/cmd/cmswwwcli/client"
"github.com/decred/contractor-mgmt/cmswww/cmd/cmswwwcli/config"
)

type SubmitInvoiceCmd struct {
Args struct {
Month string `positional-arg-name:"month"`
Year uint16 `positional-arg-name:"year"`
Attachments []string `positional-arg-name:"attachmentFilepaths"`
} `positional-args:"true" optional:"true"`
InvoiceFilename string `long:"invoice" optional:"true" description:"Filepath to an invoice CSV"`
Month string `long:"month" optional:"true" description:"Month to specify a prebuilt invoice"`
Year uint16 `long:"year" optional:"true" description:"Year to specify a prebuilt invoice"`
}

// SubmissionRecord is a record of an invoice submission to the server,
Expand Down Expand Up @@ -100,12 +105,12 @@ func (cmd *SubmitInvoiceCmd) Execute(args []string) error {
}

var filename string
if cmd.Args.Month != "" && cmd.Args.Year != 0 {
month, err := ParseMonth(cmd.Args.Month)
if cmd.Month != "" && cmd.Year != 0 {
month, err := ParseMonth(cmd.Month)
if err != nil {
return err
}
year = cmd.Args.Year
year = cmd.Year

filename, err = config.GetInvoiceFilename(month, year)
if err != nil {
Expand All @@ -123,43 +128,71 @@ func (cmd *SubmitInvoiceCmd) Execute(args []string) error {
return err
}

// Read attachment files into memory and convert to type File
files := make([]v1.File, 0, len(cmd.Args.Attachments)+1)

// Add the invoice file.
payload, err := ioutil.ReadFile(filename)
if err != nil {
return err
return fmt.Errorf("error reading invoice file %v: %v", filename, err)
}

files = append(files, v1.File{
Digest: hex.EncodeToString(util.Digest(payload)),
Payload: base64.StdEncoding.EncodeToString(payload),
})

for _, path := range cmd.Args.Attachments {
attachment, err := ioutil.ReadFile(path)
if err != nil {
return fmt.Errorf("error reading invoice attachment file %v: %v",
path, err)
}

files = append(files, v1.File{
Name: filepath.Base(path),
MIME: mime.DetectMimeType(attachment),
Digest: hex.EncodeToString(util.Digest(attachment)),
Payload: base64.StdEncoding.EncodeToString(attachment),
})
}

h := sha256.New()
h.Write(payload)
digest := hex.EncodeToString(h.Sum(nil))
signature := id.SignMessage([]byte(digest))
// Compute merkle root and sign it.
sig, err := client.SignMerkleRoot(files, id)
if err != nil {
return fmt.Errorf("SignMerkleRoot: %v", err)
}

ni := v1.SubmitInvoice{
Month: month,
Year: year,
File: v1.File{
Digest: digest,
Payload: base64.StdEncoding.EncodeToString(payload),
},
si := v1.SubmitInvoice{
Month: month,
Year: year,
Files: files,
PublicKey: hex.EncodeToString(id.Public.Key[:]),
Signature: hex.EncodeToString(signature[:]),
Signature: sig,
}

var nir v1.SubmitInvoiceReply
err = Ctx.Post(v1.RouteSubmitInvoice, ni, &nir)
var sir v1.SubmitInvoiceReply
err = Ctx.Post(v1.RouteSubmitInvoice, si, &sir)
if err != nil {
return err
}

if nir.CensorshipRecord.Merkle != digest {
return fmt.Errorf("Digest returned from server did not match client's"+
" digest: %v %v", digest, nir.CensorshipRecord.Merkle)
ir := v1.InvoiceRecord{
Files: si.Files,
PublicKey: si.PublicKey,
Signature: si.Signature,
CensorshipRecord: sir.CensorshipRecord,
}
err = client.VerifyInvoice(ir, config.ServerPublicKey)
if err != nil {
return err
}

// Store the submission record in case the submitter ever needs it.
submissionRecord := SubmissionRecord{
ServerPublicKey: config.ServerPublicKey,
Submission: ni,
CensorshipRecord: nir.CensorshipRecord,
Submission: si,
CensorshipRecord: sir.CensorshipRecord,
}
data, err := json.MarshalIndent(submissionRecord, "", " ")
if err != nil {
Expand Down
Loading