github.com/decred/politeia@v1.4.0/politeiad/api/v1/v1.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 v1
     6  
     7  import (
     8  	"crypto/sha256"
     9  	"encoding/base64"
    10  	"encoding/hex"
    11  	"errors"
    12  	"regexp"
    13  
    14  	"github.com/decred/dcrtime/merkle"
    15  	"github.com/decred/politeia/politeiad/api/v1/identity"
    16  	"github.com/decred/politeia/politeiad/api/v1/mime"
    17  )
    18  
    19  type ErrorStatusT int
    20  type RecordStatusT int
    21  
    22  const (
    23  	// Routes
    24  	IdentityRoute             = "/v1/identity/"       // Retrieve identity
    25  	NewRecordRoute            = "/v1/newrecord/"      // New record
    26  	UpdateUnvettedRoute       = "/v1/updateunvetted/" // Update unvetted record
    27  	UpdateVettedRoute         = "/v1/updatevetted/"   // Update vetted record
    28  	UpdateVettedMetadataRoute = "/v1/updatevettedmd/" // Update vetted metadata
    29  	GetUnvettedRoute          = "/v1/getunvetted/"    // Retrieve unvetted record
    30  	GetVettedRoute            = "/v1/getvetted/"      // Retrieve vetted record
    31  
    32  	// Auth required
    33  	InventoryRoute         = "/v1/inventory/"                  // Inventory records
    34  	SetUnvettedStatusRoute = "/v1/setunvettedstatus/"          // Set unvetted status
    35  	SetVettedStatusRoute   = "/v1/setvettedstatus/"            // Set vetted status
    36  	PluginCommandRoute     = "/v1/plugin/"                     // Send a command to a plugin
    37  	PluginInventoryRoute   = PluginCommandRoute + "inventory/" // Inventory all plugins
    38  	UpdateReadmeRoute      = "/v1/updatereadme/"               // Update README
    39  
    40  	ChallengeSize      = 32         // Size of challenge token in bytes
    41  	TokenSize          = 32         // Size of token
    42  	MetadataStreamsMax = uint64(16) // Maximum number of metadata streams
    43  
    44  	// Error status codes
    45  	ErrorStatusInvalid                       ErrorStatusT = 0
    46  	ErrorStatusInvalidRequestPayload         ErrorStatusT = 1
    47  	ErrorStatusInvalidChallenge              ErrorStatusT = 2
    48  	ErrorStatusInvalidFilename               ErrorStatusT = 3
    49  	ErrorStatusInvalidFileDigest             ErrorStatusT = 4
    50  	ErrorStatusInvalidBase64                 ErrorStatusT = 5
    51  	ErrorStatusInvalidMIMEType               ErrorStatusT = 6
    52  	ErrorStatusUnsupportedMIMEType           ErrorStatusT = 7
    53  	ErrorStatusInvalidRecordStatusTransition ErrorStatusT = 8
    54  	ErrorStatusEmpty                         ErrorStatusT = 9
    55  	ErrorStatusInvalidMDID                   ErrorStatusT = 10
    56  	ErrorStatusDuplicateMDID                 ErrorStatusT = 11
    57  	ErrorStatusDuplicateFilename             ErrorStatusT = 12
    58  	ErrorStatusFileNotFound                  ErrorStatusT = 13
    59  	ErrorStatusNoChanges                     ErrorStatusT = 14
    60  	ErrorStatusRecordFound                   ErrorStatusT = 15
    61  	ErrorStatusInvalidRPCCredentials         ErrorStatusT = 16
    62  	ErrorStatusLast                          ErrorStatusT = 17
    63  
    64  	// Record status codes (set and get)
    65  	RecordStatusInvalid           RecordStatusT = 0 // Invalid status
    66  	RecordStatusNotFound          RecordStatusT = 1 // Record not found
    67  	RecordStatusNotReviewed       RecordStatusT = 2 // Record has not been reviewed
    68  	RecordStatusCensored          RecordStatusT = 3 // Record has been censored
    69  	RecordStatusPublic            RecordStatusT = 4 // Record is publicly visible
    70  	RecordStatusUnreviewedChanges RecordStatusT = 5 // Unvetted record that has been changed
    71  	RecordStatusArchived          RecordStatusT = 6 // Vetted record that has been archived
    72  	RecordStatusLast              RecordStatusT = 7 // Unit test only
    73  
    74  	// Default network bits
    75  	DefaultMainnetHost = "politeia.decred.org"
    76  	DefaultMainnetPort = "49374"
    77  	DefaultTestnetHost = "politeia-testnet.decred.org"
    78  	DefaultTestnetPort = "59374"
    79  
    80  	Forward = "X-Forwarded-For"
    81  )
    82  
    83  var (
    84  	// ErrorStatus converts error status codes to human readable text.
    85  	ErrorStatus = map[ErrorStatusT]string{
    86  		ErrorStatusInvalid:                       "invalid status",
    87  		ErrorStatusInvalidRequestPayload:         "invalid request payload",
    88  		ErrorStatusInvalidChallenge:              "invalid challenge",
    89  		ErrorStatusInvalidFilename:               "invalid filename",
    90  		ErrorStatusInvalidFileDigest:             "invalid file digest",
    91  		ErrorStatusInvalidBase64:                 "corrupt base64 string",
    92  		ErrorStatusInvalidMIMEType:               "invalid MIME type detected",
    93  		ErrorStatusUnsupportedMIMEType:           "unsupported MIME type",
    94  		ErrorStatusInvalidRecordStatusTransition: "invalid record status transition",
    95  		ErrorStatusEmpty:                         "empty record",
    96  		ErrorStatusInvalidMDID:                   "invalid metadata id",
    97  		ErrorStatusDuplicateMDID:                 "duplicate metadata id",
    98  		ErrorStatusDuplicateFilename:             "duplicate filename",
    99  		ErrorStatusFileNotFound:                  "file not found",
   100  		ErrorStatusNoChanges:                     "no changes in record",
   101  		ErrorStatusRecordFound:                   "record found",
   102  		ErrorStatusInvalidRPCCredentials:         "invalid RPC client credentials",
   103  	}
   104  
   105  	// RecordStatus converts record status codes to human readable text.
   106  	RecordStatus = map[RecordStatusT]string{
   107  		RecordStatusInvalid:           "invalid status",
   108  		RecordStatusNotFound:          "not found",
   109  		RecordStatusNotReviewed:       "not reviewed",
   110  		RecordStatusCensored:          "censored",
   111  		RecordStatusPublic:            "public",
   112  		RecordStatusUnreviewedChanges: "unreviewed changes",
   113  		RecordStatusArchived:          "archived",
   114  	}
   115  
   116  	// Input validation
   117  	RegexpSHA256 = regexp.MustCompile("[A-Fa-f0-9]{64}")
   118  
   119  	// Verification errors
   120  	ErrInvalidHex    = errors.New("corrupt hex string")
   121  	ErrInvalidBase64 = errors.New("corrupt base64")
   122  	ErrInvalidMerkle = errors.New("merkle roots do not match")
   123  	ErrCorrupt       = errors.New("signature verification failed")
   124  
   125  	// Length of prefix of token used for lookups. The length 7 was selected to
   126  	// match github's abbreviated hash length This is a var so that it can be
   127  	// updated during testing.
   128  	TokenPrefixLength = 7
   129  )
   130  
   131  // Verify ensures that a CensorshipRecord properly describes the array of
   132  // files.
   133  func Verify(pid identity.PublicIdentity, csr CensorshipRecord, files []File) error {
   134  	digests := make([]*[sha256.Size]byte, 0, len(files))
   135  	for _, file := range files {
   136  		payload, err := base64.StdEncoding.DecodeString(file.Payload)
   137  		if err != nil {
   138  			return ErrInvalidBase64
   139  		}
   140  
   141  		// MIME
   142  		mimeType := mime.DetectMimeType(payload)
   143  		if !mime.MimeValid(mimeType) {
   144  			return mime.ErrUnsupportedMimeType
   145  		}
   146  
   147  		// Digest
   148  		h := sha256.New()
   149  		h.Write(payload)
   150  		d := h.Sum(nil)
   151  		var digest [sha256.Size]byte
   152  		copy(digest[:], d)
   153  
   154  		digests = append(digests, &digest)
   155  	}
   156  
   157  	// Verify merkle root
   158  	root := merkle.Root(digests)
   159  	if hex.EncodeToString(root[:]) != csr.Merkle {
   160  		return ErrInvalidMerkle
   161  	}
   162  
   163  	s, err := hex.DecodeString(csr.Signature)
   164  	if err != nil {
   165  		return ErrInvalidHex
   166  	}
   167  	var signature [identity.SignatureSize]byte
   168  	copy(signature[:], s)
   169  	r := hex.EncodeToString(root[:])
   170  	if !pid.VerifyMessage([]byte(r+csr.Token), signature) {
   171  		return ErrCorrupt
   172  	}
   173  
   174  	return nil
   175  }
   176  
   177  // CensorshipRecord contains the proof that a record was accepted for review.
   178  // The proof is verifiable on the client side.
   179  //
   180  // The Merkle field contains the ordered merkle root of all files in the record.
   181  // The Token field contains a random censorship token that is signed by the
   182  // server private key.  The token can be used on the client to verify the
   183  // authenticity of the CensorshipRecord.
   184  type CensorshipRecord struct {
   185  	Token     string `json:"token"`     // Censorship token
   186  	Merkle    string `json:"merkle"`    // Merkle root of record
   187  	Signature string `json:"signature"` // Signature of merkle+token
   188  }
   189  
   190  // Identity requests the record server identity.
   191  type Identity struct {
   192  	Challenge string `json:"challenge"` // Random challenge
   193  }
   194  
   195  // IdentityReply contains the server public identity.
   196  type IdentityReply struct {
   197  	Response  string `json:"response"`  // Signature of Challenge
   198  	PublicKey string `json:"publickey"` // Public key
   199  }
   200  
   201  // File describes an individual file that is part of the record.  The
   202  // directory structure must be flattened.  The server side SHALL verify MIME
   203  // and Digest.
   204  type File struct {
   205  	Name    string `json:"name"`    // Suggested filename
   206  	MIME    string `json:"mime"`    // Mime type
   207  	Digest  string `json:"digest"`  // Payload digest
   208  	Payload string `json:"payload"` // File content
   209  }
   210  
   211  // MetadataStream identifies a metadata stream by its identity.
   212  type MetadataStream struct {
   213  	ID      uint64 `json:"id"`      // Stream identity
   214  	Payload string `json:"payload"` // String encoded metadata
   215  }
   216  
   217  // Record is an entire record and it's content.
   218  type Record struct {
   219  	Status    RecordStatusT `json:"status"`    // Current status
   220  	Timestamp int64         `json:"timestamp"` // Last update
   221  
   222  	CensorshipRecord CensorshipRecord `json:"censorshiprecord"`
   223  
   224  	// User data
   225  	Version  string           `json:"version"`  // Version of this record
   226  	Metadata []MetadataStream `json:"metadata"` // Metadata streams
   227  	Files    []File           `json:"files"`    // Files that make up the record
   228  }
   229  
   230  // NewRecord creates a new record.  It must include all files that are part of
   231  // the record and it may contain an optional metatda record.  Thet optional
   232  // metadatarecord must be string encoded.
   233  type NewRecord struct {
   234  	Challenge string           `json:"challenge"` // Random challenge
   235  	Metadata  []MetadataStream `json:"metadata"`  // Metadata streams
   236  	Files     []File           `json:"files"`     // Files that make up record
   237  }
   238  
   239  // NewRecordReply returns the CensorshipRecord that is associated with a valid
   240  // record.  A valid record is not always going to be published.
   241  type NewRecordReply struct {
   242  	Response         string           `json:"response"` // Challenge response
   243  	CensorshipRecord CensorshipRecord `json:"censorshiprecord"`
   244  }
   245  
   246  // GetUnvetted requests an unvetted record from the server.
   247  type GetUnvetted struct {
   248  	Challenge string `json:"challenge"` // Random challenge
   249  	Token     string `json:"token"`     // Censorship token
   250  }
   251  
   252  // GetUnvettedReply returns an unvetted record.  It retrieves the censorship
   253  // record and the actual files.
   254  type GetUnvettedReply struct {
   255  	Response string `json:"response"` // Challenge response
   256  	Record   Record `json:"record"`
   257  }
   258  
   259  // GetVetted requests a vetted record from the server.
   260  type GetVetted struct {
   261  	Challenge string `json:"challenge"` // Random challenge
   262  	Token     string `json:"token"`     // Censorship token
   263  	Version   string `json:"version"`   // Record version
   264  }
   265  
   266  // GetVettedReply returns a vetted record.  It retrieves the censorship
   267  // record and the latest files in the record.
   268  type GetVettedReply struct {
   269  	Response string `json:"response"` // Challenge response
   270  	Record   Record `json:"record"`
   271  }
   272  
   273  // SetUnvettedStatus updates the status of an unvetted record.  This is used
   274  // to either promote a record to the public viewable repository or to censor
   275  // it. Additionally, metadata updates may travel along.
   276  type SetUnvettedStatus struct {
   277  	Challenge   string           `json:"challenge"`   // Random challenge
   278  	Token       string           `json:"token"`       // Censorship token
   279  	Status      RecordStatusT    `json:"status"`      // New status of record
   280  	MDAppend    []MetadataStream `json:"mdappend"`    // Metadata streams to append
   281  	MDOverwrite []MetadataStream `json:"mdoverwrite"` // Metadata streams to overwrite
   282  }
   283  
   284  // SetUnvettedStatus is a response to a SetUnvettedStatus.  It returns the
   285  // potentially modified record without the Files.
   286  type SetUnvettedStatusReply struct {
   287  	Response string `json:"response"` // Challenge response
   288  }
   289  
   290  // SetVettedStatus updates the status of a vetted record. This is used to
   291  // archive a vetted proposal. Additionally, metadata updates may travel along.
   292  type SetVettedStatus struct {
   293  	Challenge   string           `json:"challenge"`   // Random challenge
   294  	Token       string           `json:"token"`       // Censorship token
   295  	Status      RecordStatusT    `json:"status"`      // New status of record
   296  	MDAppend    []MetadataStream `json:"mdappend"`    // Metadata streams to append
   297  	MDOverwrite []MetadataStream `json:"mdoverwrite"` // Metadata streams to overwrite
   298  }
   299  
   300  // SetVettedStatusReply is a response to SetVettedStatus. It returns the
   301  // potentially modified record without the Files.
   302  type SetVettedStatusReply struct {
   303  	Response string `json:"response"` // Challenge response
   304  }
   305  
   306  // UpdateRecord update an unvetted record.
   307  type UpdateRecord struct {
   308  	Challenge   string           `json:"challenge"`   // Random challenge
   309  	Token       string           `json:"token"`       // Censorship token
   310  	MDAppend    []MetadataStream `json:"mdappend"`    // Metadata streams to append
   311  	MDOverwrite []MetadataStream `json:"mdoverwrite"` // Metadata streams to overwrite
   312  	FilesDel    []string         `json:"filesdel"`    // Files that will be deleted
   313  	FilesAdd    []File           `json:"filesadd"`    // Files that are modified or added
   314  }
   315  
   316  // UpdateRecordReply returns a CensorshipRecord which may or may not have
   317  // changed.  Metadata only updates do not create a new CensorshipRecord.
   318  type UpdateRecordReply struct {
   319  	Response string `json:"response"` // Challenge response
   320  }
   321  
   322  // UpdateVettedMetadata update a vetted metadata.  This is allowed for
   323  // priviledged users.  The record itself may not change.
   324  type UpdateVettedMetadata struct {
   325  	Challenge   string           `json:"challenge"`   // Random challenge
   326  	Token       string           `json:"token"`       // Censorship token
   327  	MDAppend    []MetadataStream `json:"mdappend"`    // Metadata streams to append
   328  	MDOverwrite []MetadataStream `json:"mdoverwrite"` // Metadata streams to overwrite
   329  }
   330  
   331  // UpdateVettedMetadataReply returns a response challenge to an
   332  // UpdateVettedMetadata command.
   333  type UpdateVettedMetadataReply struct {
   334  	Response string `json:"response"` // Challenge response
   335  }
   336  
   337  // UpdateReadme updated the README.md file in the vetted and unvetted repos.
   338  type UpdateReadme struct {
   339  	Challenge string `json:"challenge"` // Random challenge
   340  	Content   string `json:"content"`   // New content of README.md
   341  }
   342  
   343  // UpdateReadmeReply returns a response challenge to an
   344  // UpdateReadme command.
   345  type UpdateReadmeReply struct {
   346  	Response string `json:"response"` // Challenge response
   347  }
   348  
   349  // Inventory sends an (expensive and therefore authenticated) inventory request
   350  // for vetted records (master branch) and branches (censored, unpublished etc)
   351  // records.  This is a very expensive call and should be only issued at start
   352  // of day.  The client should cache the reply.
   353  // The IncludeFiles flag indicates if the records contain the record payload
   354  // as well.  This can quickly become very large and should only be used when
   355  // recovering the client side.
   356  type Inventory struct {
   357  	Challenge string `json:"challenge"` // Random challenge
   358  	// XXX add IncludeMD
   359  	IncludeFiles bool `json:"includefiles"` // Include files in records
   360  	// XXX add BranchesStart
   361  	VettedCount   uint `json:"vettedcount"`   // Last N vetted records
   362  	VettedStart   uint `json:"vettedstart"`   // Index to begin vetted records count
   363  	BranchesCount uint `json:"branchescount"` // Last N branches (censored, new etc)
   364  	AllVersions   bool `json:"allversions"`   // Return all versions of the proposals
   365  }
   366  
   367  // InventoryReply returns vetted and unvetted records.  If the Inventory
   368  // command had IncludeFiles set to true the returned Records will also include
   369  // the record files.  This obviously enlarges the payload size and should
   370  // therefore be used only in disaster recovery scenarios.
   371  type InventoryReply struct {
   372  	Response string   `json:"response"` // Challenge response
   373  	Vetted   []Record `json:"vetted"`   // Last N vetted records
   374  	Branches []Record `json:"branches"` // Last N branches (censored, new etc)
   375  }
   376  
   377  // UserErrorReply returns details about an error that occurred while trying to
   378  // execute a command due to bad input from the client.
   379  type UserErrorReply struct {
   380  	ErrorCode    ErrorStatusT `json:"errorcode"`              // Numeric error code
   381  	ErrorContext []string     `json:"errorcontext,omitempty"` // Additional error information
   382  }
   383  
   384  // ServerErrorReply returns an error code that can be correlated with
   385  // server logs.
   386  type ServerErrorReply struct {
   387  	ErrorCode int64 `json:"code"` // Server error code
   388  }
   389  
   390  // PluginSetting is a structure that holds key/value pairs of a plugin setting.
   391  type PluginSetting struct {
   392  	Key   string `json:"key"`   // Name of setting
   393  	Value string `json:"value"` // Value of setting
   394  }
   395  
   396  // Plugin describes a plugin and its settings.
   397  type Plugin struct {
   398  	ID       string          `json:"id"`       // Identifier
   399  	Version  string          `json:"version"`  // Version
   400  	Settings []PluginSetting `json:"settings"` // Settings
   401  }
   402  
   403  // PluginInventory retrieves all active plugins and their settings.
   404  type PluginInventory struct {
   405  	Challenge string `json:"challenge"` // Random challenge
   406  }
   407  
   408  // PluginInventoryReply returns all plugins and their settings.
   409  type PluginInventoryReply struct {
   410  	Response string   `json:"response"` // Challenge response
   411  	Plugins  []Plugin `json:"plugins"`  // Plugins and their settings
   412  }
   413  
   414  // PluginCommand sends a command to a plugin.
   415  type PluginCommand struct {
   416  	Challenge string `json:"challenge"` // Random challenge
   417  	ID        string `json:"id"`        // Plugin identifier
   418  	Command   string `json:"command"`   // Command identifier
   419  	CommandID string `json:"commandid"` // User setable command identifier
   420  	Payload   string `json:"payload"`   // Actual command
   421  }
   422  
   423  // PluginCommandReply is the reply to a PluginCommand.
   424  type PluginCommandReply struct {
   425  	Response  string `json:"response"`  // Challenge response
   426  	ID        string `json:"id"`        // Plugin identifier
   427  	Command   string `json:"command"`   // Command identifier
   428  	CommandID string `json:"commandid"` // User setable command identifier
   429  	Payload   string `json:"payload"`   // Actual command reply
   430  }