Skip to content

Commit

Permalink
fix(fat0): Add timestamp to sigs to prevent replays of txs that were …
Browse files Browse the repository at this point in the history
…invalid due to insufficient balance

Previously, if a fat0.Transaction is invalid solely due to insufficient
balance, then it could be replayed at any point in the future when the
balance was sufficient. We add a timestamp to the ExtIDs to salt the
signatures. The timestamp must fall within +/- 12 hours of the timestamp
of the entry containing the fat0.Transaction. This causes signatures to
expire after some time, which is controllable by how far in the past the
timestamp salt is chosen.

Since the timestamp provides a sufficient number of options for a salt,
the fat0.Transaction.Salt field was removed.

Additionally all ExtID validation, which is shared by the fat0 types, is
now deduplicated entirely by moving those member functions to the Entry
type that Transaction and Issuance both embed.
  • Loading branch information
AdamSLevy committed Dec 7, 2018
1 parent 979d441 commit 4742997
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 125 deletions.
60 changes: 60 additions & 0 deletions fat0/addressamountmap.go
@@ -0,0 +1,60 @@
package fat0

import (
"encoding/json"
"fmt"

"github.com/Factom-Asset-Tokens/fatd/factom"
)

// AddressAmountMap relates the RCDHash of an address to its amount in a
// Transaction.
type AddressAmountMap map[factom.Bytes32]uint64

// AddressAmount is used to marshal and unmarshal the JSON representation of a
// list of inputs or outputs in a Transaction.
type AddressAmount struct {
Address factom.Address `json:"address"`
Amount uint64 `json:"amount"`
}

// UnmarshalJSON unmarshals a list of addresses and amounts used in the inputs
// or outputs of a transaction. Duplicate addresses or addresses with a 0
// amount cause an error.
func (a *AddressAmountMap) UnmarshalJSON(data []byte) error {
aam := make(AddressAmountMap)
var aaS []AddressAmount
if err := json.Unmarshal(data, &aaS); err != nil {
return err
}
for _, aa := range aaS {
if aa.Amount == 0 {
return fmt.Errorf("invalid amount (0) for address: %v", aa.Address)
}
if _, duplicate := aam[aa.Address.RCDHash()]; duplicate {
return fmt.Errorf("duplicate address: %v", aa.Address)
}
aam[aa.Address.RCDHash()] = aa.Amount
}
*a = aam
return nil
}

// MarshalJSON marshals a list of addresses and amounts used in the inputs or
// outputs of a transaction. Addresses with a 0 amount are omitted.
func (a AddressAmountMap) MarshalJSON() ([]byte, error) {
as := make([]AddressAmount, 0, len(a))
for rcdHash, amount := range a {
rcdHash := rcdHash
// Omit addresses with 0 amounts.
if amount == 0 {
continue
}

as = append(as, AddressAmount{
Address: factom.NewAddress(&rcdHash),
Amount: amount,
})
}
return json.Marshal(as)
}
99 changes: 81 additions & 18 deletions fat0/entry.go
Expand Up @@ -3,7 +3,10 @@ package fat0
import (
"bytes"
"encoding/json"
"fmt"
"math/rand"
"strconv"
"time"

"github.com/Factom-Asset-Tokens/fatd/factom"
"github.com/FactomProject/ed25519"
Expand All @@ -24,36 +27,96 @@ func (e Entry) unmarshalEntry(v interface{}) error {
return d.Decode(v)
}

func (e *Entry) marshalEntry(v interface{ ValidData() error }) error {
if err := v.ValidData(); err != nil {
return err
}
data, err := json.Marshal(v)
if err != nil {
return err
}
e.Content = factom.Bytes(data)
return nil
}

// validExtIDs validates the structure of the external IDs of the entry to make
// sure that it has the correct number of RCD/signature pairs. ValidExtIDs does
// not validate the content of the RCD or signature. ValidExtIDs assumes that
// the entry content has been unmarshaled and that ValidData returns nil.
func (e Entry) ValidExtIDs() error {
if len(e.ExtIDs) < 3 || len(e.ExtIDs)%2 != 1 {
return fmt.Errorf("invalid number of ExtIDs")
}
if err := e.validTimestamp(); err != nil {
return err
}
extIDs := e.ExtIDs[1:]
for i := 0; i < len(extIDs)/2; i++ {
rcd := extIDs[i*2]
if len(rcd) != factom.RCDSize {
return fmt.Errorf("ExtIDs[%v]: invalid RCD size", i+1)
}
if rcd[0] != factom.RCDType {
return fmt.Errorf("ExtIDs[%v]: invalid RCD type", i+1)
}
sig := extIDs[i*2+1]
if len(sig) != factom.SignatureSize {
return fmt.Errorf("ExtIDs[%v]: invalid signature size", i+1)
}
}
return e.validSignatures()
}

func (e Entry) validTimestamp() error {
sec, err := strconv.ParseInt(string(e.ExtIDs[0]), 10, 64)
if err != nil {
return fmt.Errorf("timestamp salt: %v", err)
}
ts := time.Unix(sec, 0)
diff := e.Timestamp.Sub(ts)
if -12*time.Hour > diff || diff > 12*time.Hour {
return fmt.Errorf("timestamp salt expired")
}
return nil
}

// validSignatures returns true if the first num RCD/signature pairs in the
// ExtIDs are valid.
func (e Entry) validSignatures(num int) bool {
if num <= 0 || num*2 > len(e.ExtIDs) {
return false
}
msg := append(e.ChainID[:], e.Content...)
func (e Entry) validSignatures() error {
num := len(e.ExtIDs) / 2
timeSalt := e.ExtIDs[0]
salt := append(timeSalt, e.ChainID[:]...)
msg := append(salt, e.Content...)
pubKey := new([ed25519.PublicKeySize]byte)
sig := new([ed25519.SignatureSize]byte)
extIDs := e.ExtIDs[1:]
for sigID := 0; sigID < num; sigID++ {
copy(pubKey[:], e.ExtIDs[sigID*2][1:])
copy(sig[:], e.ExtIDs[sigID*2+1])
salt := []byte(strconv.FormatInt(int64(sigID), 10))
msg := append(salt, msg...)
copy(pubKey[:], extIDs[sigID*2][1:])
copy(sig[:], extIDs[sigID*2+1])
extIDSalt := []byte(strconv.FormatInt(int64(sigID), 10))
msg := append(extIDSalt, msg...)
if !ed25519.VerifyCanonical(pubKey, msg, sig) {
return false
return fmt.Errorf("ExtIDs[%v]: invalid signature", sigID*2+2)
}
}
return true
return nil
}

// Sign the Sig ID + chain ID + content of the factom.Entry and add the RCD +
// signature pairs for the given addresses to the ExtIDs. This clears any
// existing ExtIDs.
// Sign the ExtIDIndex Salt + Timestamp Salt + Chain ID Salt + Content of the
// factom.Entry and add the RCD + signature pairs for the given addresses to
// the ExtIDs. This clears any existing ExtIDs.
func (e *Entry) Sign(as ...factom.Address) {
msg := append(e.ChainID[:], e.Content...)
e.ExtIDs = nil
e.Timestamp = &factom.Time{Time: time.Now()}
ts := time.Now().Add(time.Duration(
-rand.Int63n(int64(12 * time.Hour))))
timeSalt := []byte(strconv.FormatInt(ts.Unix(), 10))
salt := append(timeSalt, e.ChainID[:]...)
msg := append(salt, e.Content...)
e.ExtIDs = make([]factom.Bytes, 1, len(as)*2+1)
e.ExtIDs[0] = timeSalt
for sigID, a := range as {
salt := []byte(strconv.FormatInt(int64(sigID), 10))
msg := append(salt, msg...)
extIDSalt := []byte(strconv.FormatInt(int64(sigID), 10))
msg := append(extIDSalt, msg...)
e.ExtIDs = append(e.ExtIDs, a.RCD(), ed25519.Sign(a.PrivateKey, msg)[:])
}
}
43 changes: 15 additions & 28 deletions fat0/issuance.go
Expand Up @@ -55,23 +55,25 @@ func (i *Issuance) UnmarshalEntry() error {
return i.unmarshalEntry(i)
}

// MarshalEntry marshals the entry content as an Issuance.
func (i *Issuance) MarshalEntry() error {
return i.marshalEntry(i)
}

// Valid performs all validation checks and returns nil if i is a valid
// Issuance.
func (i *Issuance) Valid(idKey factom.Bytes32) error {
if err := i.ValidExtIDs(); err != nil {
return err
}
if i.RCDHash() != idKey {
return fmt.Errorf("invalid RCD")
}
if err := i.UnmarshalEntry(); err != nil {
return err
}
if err := i.ValidData(); err != nil {
return err
}
if !i.ValidSignature() {
return fmt.Errorf("invalid signature")
if err := i.ValidExtIDs(); err != nil {
return err
}
if i.RCDHash() != idKey {
return fmt.Errorf("invalid RCD")
}
return nil
}
Expand All @@ -82,7 +84,7 @@ func (i Issuance) ValidData() error {
if i.Type != "FAT-0" {
return fmt.Errorf(`invalid "type": %#v`, i.Type)
}
if i.Supply == 0 {
if i.Supply == 0 || i.Supply < -1 {
return fmt.Errorf(`invalid "supply": must be positive or -1`)
}
return nil
Expand All @@ -92,31 +94,16 @@ func (i Issuance) ValidData() error {
// sure that it has an RCD and signature. It does not validate the content of
// the RCD or signature.
func (i Issuance) ValidExtIDs() error {
if len(i.ExtIDs) < 2 {
return fmt.Errorf("insufficient number of ExtIDs")
}
if len(i.ExtIDs[0]) != factom.RCDSize {
return fmt.Errorf("invalid RCD size")
}
if i.ExtIDs[0][0] != factom.RCDType {
return fmt.Errorf("invalid RCD type")
if len(i.ExtIDs) != 3 {
return fmt.Errorf("incorrect number of ExtIDs")
}
if len(i.ExtIDs[1]) != factom.SignatureSize {
return fmt.Errorf("invalid signature size")
}
return nil
return i.Entry.ValidExtIDs()
}

// RCDHash returns the SHA256d hash of the first external ID of the entry,
// which should be the RCD of the IDKey of the issuing Identity.
func (i Issuance) RCDHash() [sha256.Size]byte {
return sha256d(i.ExtIDs[0])
}

// ValidSignature returns true if the RCD/signature pair is valid.
// ValidSignature assumes that ValidExtIDs returns nil.
func (i Issuance) ValidSignature() bool {
return i.validSignatures(1)
return sha256d(i.ExtIDs[1])
}

// sha256d computes two rounds of the sha256 hash.
Expand Down

0 comments on commit 4742997

Please sign in to comment.