AJ ONeal
4 years ago
4 changed files with 623 additions and 1 deletions
@ -1,3 +1,7 @@ |
|||
# hashcash |
|||
|
|||
HTTP Hashcash impremented in Go. |
|||
HTTP Hashcash implemented in Go. |
|||
|
|||
Explanation at https://therootcompany.com/blog/http-hashcash/ |
|||
|
|||
Go docs at https://godoc.org/git.rootprojects.org/root/hashcash |
|||
|
@ -0,0 +1,3 @@ |
|||
module git.rootprojects.org/root/hashcash |
|||
|
|||
go 1.15 |
@ -0,0 +1,284 @@ |
|||
package hashcash |
|||
|
|||
import ( |
|||
"crypto/rand" |
|||
"crypto/sha256" |
|||
"encoding/base64" |
|||
"encoding/binary" |
|||
"errors" |
|||
"strconv" |
|||
"strings" |
|||
"time" |
|||
) |
|||
|
|||
// ErrParse is returned when fewer than 6 or more than 7 segments are split
|
|||
var ErrParse = errors.New("could not split the hashcash parts") |
|||
|
|||
// ErrInvalidTag is returned when the Hashcash version is unsupported
|
|||
var ErrInvalidTag = errors.New("expected tag to be 'H'") |
|||
|
|||
// ErrInvalidDifficulty is returned when the difficulty is outside of the acceptable range
|
|||
var ErrInvalidDifficulty = errors.New("the number of bits of difficulty is too low or too high") |
|||
|
|||
// ErrInvalidDate is returned when the date cannot be parsed as a positive int64
|
|||
var ErrInvalidDate = errors.New("invalid date") |
|||
|
|||
// ErrExpired is returned when the current time is past that of ExpiresAt
|
|||
var ErrExpired = errors.New("expired hashcash") |
|||
|
|||
// ErrInvalidSubject is returned when the subject is invalid or does not match that passed to Verify()
|
|||
var ErrInvalidSubject = errors.New("the subject is invalid or rejected") |
|||
|
|||
// ErrInvalidNonce is returned when the nonce
|
|||
//var ErrInvalidNonce = errors.New("the nonce has been used or is invalid")
|
|||
|
|||
// ErrUnsupportedAlgorithm is returned when the given algorithm is not supported
|
|||
var ErrUnsupportedAlgorithm = errors.New("the given algorithm is invalid or not supported") |
|||
|
|||
// ErrInvalidSolution is returned when the given hashcash is not properly solved
|
|||
var ErrInvalidSolution = errors.New("the given solution is not valid") |
|||
|
|||
// MaxDifficulty is the upper bound for all Solve() operations
|
|||
var MaxDifficulty = 26 |
|||
|
|||
// Sep is the separator character to use
|
|||
var Sep = ":" |
|||
|
|||
// no milliseconds
|
|||
//var isoTS = "2006-01-02T15:04:05Z"
|
|||
|
|||
// Hashcash represents a parsed Hashcash string
|
|||
type Hashcash struct { |
|||
Tag string `json:"tag"` // Always "H" for "HTTP"
|
|||
Difficulty int `json:"difficulty"` // Number of "partial pre-image" (zero) bits in the hashed code
|
|||
ExpiresAt time.Time `json:"exp"` // The timestamp that the hashcash expires, as seconds since the Unix epoch
|
|||
Subject string `json:"sub"` // Resource data string being transmitted, e.g., a domain or URL
|
|||
Nonce string `json:"nonce"` // Unique string of random characters, encoded as url-safe base-64
|
|||
Alg string `json:"alg"` // always SHA-256 for now
|
|||
Solution string `json:"solution"` // Binary counter, encoded as url-safe base-64
|
|||
} |
|||
|
|||
// New returns a Hashcash with reasonable defaults
|
|||
func New(h Hashcash) *Hashcash { |
|||
h.Tag = "H" |
|||
|
|||
if 0 == h.Difficulty { |
|||
// safe for WebCrypto
|
|||
h.Difficulty = 10 |
|||
} |
|||
|
|||
if h.ExpiresAt.IsZero() { |
|||
h.ExpiresAt = time.Now().Add(5 * time.Minute) |
|||
} |
|||
h.ExpiresAt = h.ExpiresAt.UTC().Truncate(time.Second) |
|||
|
|||
if "" == h.Subject { |
|||
h.Subject = "*" |
|||
} |
|||
|
|||
if "" == h.Nonce { |
|||
nonce := make([]byte, 16) |
|||
if _, err := rand.Read(nonce); nil != err { |
|||
panic(err) |
|||
return nil |
|||
} |
|||
h.Nonce = base64.RawURLEncoding.EncodeToString(nonce) |
|||
} |
|||
|
|||
if "" == h.Alg { |
|||
h.Alg = "SHA-256" |
|||
} |
|||
/* |
|||
if "SHA-256" != h.Alg { |
|||
// TODO error
|
|||
} |
|||
*/ |
|||
|
|||
return &h |
|||
} |
|||
|
|||
// Parse will (obviously) parse the hashcash string, without verifying any
|
|||
// of the parameters.
|
|||
func Parse(hc string) (*Hashcash, error) { |
|||
parts := strings.Split(hc, Sep) |
|||
n := len(parts) |
|||
if n < 6 || n > 7 { |
|||
return nil, ErrParse |
|||
} |
|||
|
|||
tag := parts[0] |
|||
if "H" != tag { |
|||
return nil, ErrInvalidTag |
|||
} |
|||
|
|||
bits, err := strconv.Atoi(parts[1]) |
|||
if nil != err || bits < 0 { |
|||
return nil, ErrInvalidDifficulty |
|||
} |
|||
|
|||
// Allow empty ExpiresAt
|
|||
var exp time.Time |
|||
if "" != parts[2] { |
|||
expAt, err := strconv.ParseInt(parts[2], 10, 64) |
|||
if nil != err || expAt < 0 { |
|||
return nil, ErrInvalidDate |
|||
} |
|||
exp = time.Unix(int64(expAt), 0).UTC() |
|||
} |
|||
|
|||
/* |
|||
exp, err := time.ParseInLocation(isoTS, parts[2], time.UTC) |
|||
if nil != err { |
|||
return nil, ErrInvalidDate |
|||
} |
|||
*/ |
|||
|
|||
sub := parts[3] |
|||
|
|||
nonce := parts[4] |
|||
|
|||
alg := parts[5] |
|||
|
|||
var solution string |
|||
if n > 6 { |
|||
solution = parts[6] |
|||
} |
|||
|
|||
h := &Hashcash{ |
|||
Tag: tag, |
|||
Difficulty: bits, |
|||
ExpiresAt: exp.UTC().Truncate(time.Second), |
|||
Subject: sub, |
|||
Nonce: nonce, |
|||
Alg: alg, |
|||
Solution: solution, |
|||
} |
|||
|
|||
return h, nil |
|||
} |
|||
|
|||
// String will return the formatted Hashcash, omitting the solution if it has not be solved.
|
|||
func (h *Hashcash) String() string { |
|||
var solution string |
|||
if "" != h.Solution { |
|||
solution = Sep + h.Solution |
|||
} |
|||
|
|||
var expAt string |
|||
if !h.ExpiresAt.IsZero() { |
|||
expAt = strconv.FormatInt(h.ExpiresAt.UTC().Truncate(time.Second).Unix(), 10) |
|||
} |
|||
return strings.Join( |
|||
[]string{ |
|||
"H", |
|||
strconv.Itoa(h.Difficulty), |
|||
//h.ExpiresAt.UTC().Format(isoTS),
|
|||
expAt, |
|||
h.Subject, |
|||
h.Nonce, |
|||
h.Alg, |
|||
}, |
|||
Sep, |
|||
) + solution |
|||
} |
|||
|
|||
// Verify the Hashcash based on Difficulty, Algorithm, ExpiresAt, Subject and,
|
|||
// of course, the Solution and hash.
|
|||
func (h *Hashcash) Verify(subject string) error { |
|||
if h.Difficulty < 0 { |
|||
return ErrInvalidDifficulty |
|||
} |
|||
|
|||
if "SHA-256" != h.Alg { |
|||
return ErrUnsupportedAlgorithm |
|||
} |
|||
|
|||
if !h.ExpiresAt.IsZero() && h.ExpiresAt.Sub(time.Now()) < 0 { |
|||
return ErrExpired |
|||
} |
|||
|
|||
if subject != h.Subject { |
|||
return ErrInvalidSubject |
|||
} |
|||
|
|||
bits := h.Difficulty |
|||
hash := sha256.Sum256([]byte(h.String())) |
|||
n := bits / 8 // 10 / 8 = 1
|
|||
m := bits % 8 // 10 % 8 = 2
|
|||
if m > 0 { |
|||
n++ // 10 bits = 2 bytes
|
|||
} |
|||
|
|||
if !verifyBits(hash[:n], bits, n) { |
|||
return ErrInvalidSolution |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
func verifyBits(hash []byte, bits, n int) bool { |
|||
for i := 0; i < n; i++ { |
|||
if bits > 8 { |
|||
bits -= 8 |
|||
if 0 != hash[i] { |
|||
return false |
|||
} |
|||
continue |
|||
} |
|||
|
|||
// (bits % 8) == bits
|
|||
pad := 8 - bits |
|||
if 0 != hash[i]>>pad { |
|||
return false |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
// 0 == bits
|
|||
return true |
|||
} |
|||
|
|||
// Solve will search for a solution, returning an error if the difficulty is
|
|||
// above the local or global MaxDifficulty, the Algorithm is unsupported.
|
|||
func (h *Hashcash) Solve(maxDifficulty int) error { |
|||
if "SHA-256" != h.Alg { |
|||
return ErrUnsupportedAlgorithm |
|||
} |
|||
|
|||
if h.Difficulty < 0 { |
|||
return ErrInvalidDifficulty |
|||
} |
|||
|
|||
if h.Difficulty > maxDifficulty || h.Difficulty > MaxDifficulty { |
|||
return ErrInvalidDifficulty |
|||
} |
|||
|
|||
if "" != h.Solution { |
|||
if nil == h.Verify(h.Subject) { |
|||
return nil |
|||
} |
|||
h.Solution = "" |
|||
} |
|||
|
|||
hashcash := h.String() |
|||
bits := h.Difficulty |
|||
n := bits / 8 // 10 / 8 = 1
|
|||
m := bits % 8 // 10 % 8 = 2
|
|||
if m > 0 { |
|||
n++ // 10 bits = 2 bytes
|
|||
} |
|||
|
|||
var solution uint32 = 0 |
|||
sb := make([]byte, 4) |
|||
for { |
|||
// Note: it's not actually important what method of change or encoding is used
|
|||
// but incrementing by 1 on an int32 is good enough, and makes for a small base64 encoding
|
|||
binary.LittleEndian.PutUint32(sb, solution) |
|||
h.Solution = base64.RawURLEncoding.EncodeToString(sb) |
|||
hash := sha256.Sum256([]byte(hashcash + Sep + h.Solution)) |
|||
if verifyBits(hash[:n], bits, n) { |
|||
return nil |
|||
} |
|||
solution++ |
|||
} |
|||
} |
@ -0,0 +1,331 @@ |
|||
package hashcash |
|||
|
|||
import ( |
|||
"crypto/rand" |
|||
"encoding/base64" |
|||
"errors" |
|||
"testing" |
|||
"time" |
|||
) |
|||
|
|||
var never = time.Date(3006, time.January, 1, 15, 4, 5, 0, time.UTC) |
|||
|
|||
func TestNew(t *testing.T) { |
|||
h1 := New(Hashcash{}) |
|||
|
|||
h2, err := Parse(h1.String()) |
|||
if nil != err { |
|||
t.Fatal(err) |
|||
return |
|||
} |
|||
|
|||
if h1.Tag != h2.Tag { |
|||
t.Fatal("wrong tag version") |
|||
return |
|||
} |
|||
|
|||
if h1.Difficulty != h2.Difficulty { |
|||
t.Fatal("wrong difficulty") |
|||
return |
|||
} |
|||
|
|||
if h1.ExpiresAt != h2.ExpiresAt { |
|||
t.Fatal("wrong expiresat") |
|||
return |
|||
} |
|||
|
|||
if h1.Subject != h2.Subject { |
|||
t.Fatal("wrong subject") |
|||
return |
|||
} |
|||
|
|||
if h1.Nonce != h2.Nonce { |
|||
t.Fatal("wrong nonce") |
|||
return |
|||
} |
|||
|
|||
if h1.Alg != h2.Alg { |
|||
t.Fatal("wrong algorithm") |
|||
return |
|||
} |
|||
|
|||
if h1.Solution != h2.Solution { |
|||
t.Fatal("wrong solution") |
|||
return |
|||
} |
|||
} |
|||
|
|||
func TestExplicit(t *testing.T) { |
|||
nonce := make([]byte, 16) |
|||
_, _ = rand.Read(nonce) |
|||
h0 := Hashcash{ |
|||
Tag: "H", |
|||
Difficulty: 1000, |
|||
ExpiresAt: time.Now().UTC().Truncate(time.Second), |
|||
Subject: "example.com", |
|||
Nonce: base64.RawURLEncoding.EncodeToString(nonce), |
|||
Alg: "FOOBAR", |
|||
Solution: "incorrect", |
|||
} |
|||
h1 := New(h0) |
|||
|
|||
h2, err := Parse(h1.String()) |
|||
if nil != err { |
|||
t.Fatal(err) |
|||
return |
|||
} |
|||
h3 := New(*h2) |
|||
|
|||
if h0.Tag != h3.Tag { |
|||
t.Fatal("wrong tag version") |
|||
return |
|||
} |
|||
|
|||
if h0.Difficulty != h3.Difficulty { |
|||
t.Fatal("wrong difficulty") |
|||
return |
|||
} |
|||
|
|||
if h0.ExpiresAt != h3.ExpiresAt { |
|||
t.Fatal("wrong expiresat") |
|||
return |
|||
} |
|||
|
|||
if h0.Subject != h3.Subject { |
|||
t.Fatal("wrong subject") |
|||
return |
|||
} |
|||
|
|||
if h0.Nonce != h3.Nonce { |
|||
t.Fatal("wrong nonce") |
|||
return |
|||
} |
|||
|
|||
if h0.Alg != h3.Alg { |
|||
t.Fatal("wrong algorithm") |
|||
return |
|||
} |
|||
|
|||
if h0.Solution != h3.Solution { |
|||
t.Fatal("wrong solution") |
|||
return |
|||
} |
|||
} |
|||
|
|||
func TestExpired(t *testing.T) { |
|||
h := New(Hashcash{ |
|||
Alg: "SHA-256", |
|||
Difficulty: 10, |
|||
ExpiresAt: time.Now().Add(-5 * time.Second), |
|||
}) |
|||
|
|||
err := h.Verify(h.Subject) |
|||
if nil == err { |
|||
t.Error("verified expired token") |
|||
return |
|||
} |
|||
if err != ErrExpired { |
|||
t.Error("expired token error is not ErrExpired") |
|||
return |
|||
} |
|||
} |
|||
|
|||
func TestNoExpiry(t *testing.T) { |
|||
h := New(Hashcash{}) |
|||
h.ExpiresAt = time.Time{} |
|||
|
|||
if err := h.Solve(20); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
|
|||
if err := h.Verify(h.Subject); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
} |
|||
|
|||
func TestSolve0(t *testing.T) { |
|||
h := New(Hashcash{ |
|||
ExpiresAt: never, |
|||
Nonce: "DeadBeef", |
|||
}) |
|||
h.Difficulty = 0 |
|||
|
|||
if err := h.Solve(1); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
|
|||
if err := h.Verify(h.Subject); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
} |
|||
|
|||
func TestSolve1(t *testing.T) { |
|||
h := New(Hashcash{ |
|||
Difficulty: 1, |
|||
ExpiresAt: never, |
|||
Nonce: "DeadBeef", |
|||
}) |
|||
|
|||
if err := h.Solve(2); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
|
|||
if err := h.Verify(h.Subject); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
} |
|||
|
|||
func TestSolve2(t *testing.T) { |
|||
h := New(Hashcash{ |
|||
Difficulty: 2, |
|||
ExpiresAt: never, |
|||
Nonce: "DeadBeef", |
|||
}) |
|||
|
|||
if err := h.Solve(3); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
|
|||
if err := h.Verify(h.Subject); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
} |
|||
|
|||
func TestSolve7(t *testing.T) { |
|||
h := New(Hashcash{ |
|||
Difficulty: 7, |
|||
ExpiresAt: never, |
|||
Nonce: "DeadBeef", |
|||
}) |
|||
|
|||
if err := h.Solve(8); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
|
|||
if err := h.Verify(h.Subject); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
} |
|||
|
|||
func TestSolve8(t *testing.T) { |
|||
h := New(Hashcash{ |
|||
Difficulty: 8, |
|||
ExpiresAt: never, |
|||
Nonce: "DeadBeef", |
|||
}) |
|||
|
|||
if err := h.Solve(9); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
|
|||
if err := h.Verify(h.Subject); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
} |
|||
|
|||
func TestSolve9(t *testing.T) { |
|||
h := New(Hashcash{ |
|||
Difficulty: 9, |
|||
ExpiresAt: never, |
|||
Nonce: "DeadBeef", |
|||
}) |
|||
|
|||
if err := h.Solve(10); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
|
|||
if err := h.Verify(h.Subject); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
} |
|||
|
|||
func TestSolve15(t *testing.T) { |
|||
h := New(Hashcash{ |
|||
Difficulty: 15, |
|||
ExpiresAt: never, |
|||
Nonce: "DeadBeef", |
|||
}) |
|||
|
|||
if err := h.Solve(16); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
|
|||
if err := h.Verify(h.Subject); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
} |
|||
|
|||
func TestSolve16(t *testing.T) { |
|||
h := New(Hashcash{ |
|||
Difficulty: 16, |
|||
ExpiresAt: never, |
|||
Nonce: "DeadBeef", |
|||
}) |
|||
|
|||
if err := h.Solve(17); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
|
|||
if err := h.Verify(h.Subject); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
} |
|||
|
|||
func TestSolve17(t *testing.T) { |
|||
h := New(Hashcash{ |
|||
Difficulty: 17, |
|||
ExpiresAt: never, |
|||
Nonce: "DeadBeef", |
|||
}) |
|||
|
|||
if err := h.Solve(18); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
|
|||
if err := h.Verify(h.Subject); nil != err { |
|||
t.Error(err) |
|||
return |
|||
} |
|||
|
|||
if "H:17:32693036645:*:DeadBeef:SHA-256:R7sIAA" != h.String() { |
|||
t.Errorf("unexpected hashcash string: %s, has the implementation changed?", h.String()) |
|||
} |
|||
} |
|||
|
|||
func TestTooHard(t *testing.T) { |
|||
h := New(Hashcash{ |
|||
Alg: "SHA-256", |
|||
Difficulty: 24, |
|||
ExpiresAt: never, |
|||
Nonce: "DeadBeef", |
|||
}) |
|||
|
|||
err := h.Solve(20) |
|||
if nil == err { |
|||
t.Error(errors.New("the challenge is too hard, should've quite")) |
|||
return |
|||
} |
|||
if ErrInvalidDifficulty != err { |
|||
t.Error(errors.New("incorrect error")) |
|||
return |
|||
} |
|||
} |
Loading…
Reference in new issue