initial commit

This commit is contained in:
AJ ONeal 2020-08-15 04:44:24 -06:00
parent 86b384e9ca
commit badaa411f2
4 changed files with 623 additions and 1 deletions

View File

@ -1,3 +1,7 @@
# hashcash # 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

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.rootprojects.org/root/hashcash
go 1.15

284
hashcash.go Normal file
View File

@ -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++
}
}

331
hashcash_test.go Normal file
View File

@ -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
}
}