use hashcash in protocol

This commit is contained in:
Seth Schoen 2012-07-14 14:34:24 -07:00
parent bb272f16ca
commit 064148df29
4 changed files with 230 additions and 3 deletions

View file

@ -4,8 +4,14 @@ from chocolate_protocol_pb2 import chocolatemessage
from Crypto.Hash import SHA256
import CSR
from CSR import M2Crypto
import urllib2, os, sys, time, random, sys
import urllib2, os, sys, time, random, sys, hashcash
# CSR.py here should be a symlink to ../server-ca/CSR.py
# hashcash.py here should be a symlink to ../server-ca/hashcash.py
difficulty = 20
# TODO: unfortunately, the C hashcash implementation seems to be about
# 2^6 times faster than the native Python implementation, so
# calibrating the difficulty is a bit of a problem.
def sha256(m):
return SHA256.new(m).hexdigest()
@ -29,9 +35,11 @@ def init(m):
m.session = ""
def make_request(m, csr):
m.request.recipient = os.environ["CHOCOLATESERVER"]
server = os.environ["CHOCOLATESERVER"]
m.request.recipient = server
m.request.timestamp = int(time.time())
m.request.csr = csr
m.request.clientpuzzle = hashcash.mint(server, difficulty)
def sign(k, m):
m.request.sig = CSR.sign(k, ("(%d) (%s) (%s)" % (m.request.timestamp, m.request.recipient, m.request.csr)))
@ -42,6 +50,8 @@ init(k)
init(m)
make_request(m, csr=open("req.pem").read())
sign(open("key.pem").read(), m)
print m
assert False
r=decode(do(m))
print r
while r.proceed.IsInitialized():

View file

@ -0,0 +1 @@
../server-ca/hashcash.py

View file

@ -4,6 +4,7 @@ import web, redis, time
import CSR
import hashlib
import hmac
import hashcash
from CSR import M2Crypto
from Crypto import Random
from chocolate_protocol_pb2 import chocolatemessage
@ -12,6 +13,8 @@ from google.protobuf.message import DecodeError
MaximumSessionAge = 100 # seconds, to demonstrate session timeout
MaximumChallengeAge = 600 # to demonstrate challenge timeout
difficulty = 20 # bits of hashcash required with new requests
try:
chocolate_server_name = open("SERVERNAME").read().rstrip()
except IOError:
@ -201,6 +204,12 @@ class session(object):
# It is mandatory to make a signing request at the outset of a session.
self.die(r, r.BadRequest, uri="https://ca.example.com/failures/missingrequest")
return
# Check hashcash before doing any crypto or database access.
if not m.request.clientpuzzle or not hashcash.check(m.request.clientpuzzle, chocolate_server_name, difficulty):
# TODO: should enforce hashcash expiry and use the database to store valid
# ones in order to prevent double-spending.
self.die(r, r.NeedClientPuzzle, uri="https://ca.example.com/failures/hashcash")
return
if self.request_made():
# Can't make new signing requests if there have already been requests in
# this session. (All signing requests should occur together at the
@ -208,7 +217,6 @@ class session(object):
self.die(r, r.BadRequest, uri="https://ca.example.com/failures/priorrequest")
return
# Process the request.
# TODO: check client puzzle before processing request
timestamp = m.request.timestamp
recipient = m.request.recipient
csr = m.request.csr

208
server-ca/hashcash.py Normal file
View file

@ -0,0 +1,208 @@
#!/usr/bin/env python2.3
"""Implement Hashcash version 1 protocol in Python
+-------------------------------------------------------+
| Written by David Mertz; released to the Public Domain |
+-------------------------------------------------------+
Double spend database not implemented in this module, but stub
for callbacks is provided in the 'check()' function
The function 'check()' will validate hashcash v1 and v0 tokens, as well as
'generalized hashcash' tokens generically. Future protocol version are
treated as generalized tokens (should a future version be published w/o
this module being correspondingly updated).
A 'generalized hashcash' is implemented in the '_mint()' function, with the
public function 'mint()' providing a wrapper for actual hashcash protocol.
The generalized form simply finds a suffix that creates zero bits in the
hash of the string concatenating 'challenge' and 'suffix' without specifying
any particular fields or delimiters in 'challenge'. E.g., you might get:
>>> from hashcash import mint, _mint
>>> mint('foo', bits=16)
'1:16:040922:foo::+ArSrtKd:164b3'
>>> _mint('foo', bits=16)
'9591'
>>> from sha import sha
>>> sha('foo9591').hexdigest()
'0000de4c9b27cec9b20e2094785c1c58eaf23948'
>>> sha('1:16:040922:foo::+ArSrtKd:164b3').hexdigest()
'0000a9fe0c6db2efcbcab15157735e77c0877f34'
Notice that '_mint()' behaves deterministically, finding the same suffix
every time it is passed the same arguments. 'mint()' incorporates a random
salt in stamps (as per the hashcash v.1 protocol).
"""
import sys
from string import ascii_letters
from math import ceil, floor
from sha import sha
from random import choice
from time import strftime, localtime, time
ERR = sys.stderr # Destination for error messages
DAYS = 60 * 60 * 24 # Seconds in a day
tries = [0] # Count hashes performed for benchmark
def mint(resource, bits=20, now=None, ext='', saltchars=8, stamp_seconds=False):
"""Mint a new hashcash stamp for 'resource' with 'bits' of collision
20 bits of collision is the default.
'ext' lets you add your own extensions to a minted stamp. Specify an
extension as a string of form 'name1=2,3;name2;name3=var1=2,2,val'
FWIW, urllib.urlencode(dct).replace('&',';') comes close to the
hashcash extension format.
'saltchars' specifies the length of the salt used; this version defaults
8 chars, rather than the C version's 16 chars. This still provides about
17 million salts per resource, per timestamp, before birthday paradox
collisions occur. Really paranoid users can use a larger salt though.
'stamp_seconds' lets you add the option time elements to the datestamp.
If you want more than just day, you get all the way down to seconds,
even though the spec also allows hours/minutes without seconds.
"""
ver = "1"
now = now or time()
if stamp_seconds: ts = strftime("%y%m%d%H%M%S", localtime(now))
else: ts = strftime("%y%m%d", localtime(now))
challenge = "%s:"*6 % (ver, bits, ts, resource, ext, _salt(saltchars))
return challenge + _mint(challenge, bits)
def _salt(l):
"Return a random string of length 'l'"
alphabet = ascii_letters + "+/="
return ''.join([choice(alphabet) for _ in [None]*l])
def _mint(challenge, bits):
"""Answer a 'generalized hashcash' challenge'
Hashcash requires stamps of form 'ver:bits:date:res:ext:rand:counter'
This internal function accepts a generalized prefix 'challenge',
and returns only a suffix that produces the requested SHA leading zeros.
NOTE: Number of requested bits is rounded up to the nearest multiple of 4
"""
counter = 0
hex_digits = int(ceil(bits/4.))
zeros = '0'*hex_digits
while 1:
digest = sha(challenge+hex(counter)[2:]).hexdigest()
if digest[:hex_digits] == zeros:
tries[0] = counter
return hex(counter)[2:]
counter += 1
def check(stamp, resource=None, bits=None,
check_expiration=None, ds_callback=None):
"""Check whether a stamp is valid
Optionally, the stamp may be checked for a specific resource, and/or
it may require a minimum bit value, and/or it may be checked for
expiration, and/or it may be checked for double spending.
If 'check_expiration' is specified, it should contain the number of
seconds old a date field may be. Indicating days might be easier in
many cases, e.g.
>>> from hashcash import DAYS
>>> check(stamp, check_expiration=28*DAYS)
NOTE: Every valid (version 1) stamp must meet its claimed bit value
NOTE: Check floor of 4-bit multiples (overly permissive in acceptance)
"""
if stamp.startswith('0:'): # Version 0
try:
date, res, suffix = stamp[2:].split(':')
except ValueError:
ERR.write("Malformed version 0 hashcash stamp!\n")
return False
if resource is not None and resource != res:
return False
elif check_expiration is not None:
good_until = strftime("%y%m%d%H%M%S", localtime(time()-check_expiration))
if date < good_until:
return False
elif callable(ds_callback) and ds_callback(stamp):
return False
elif type(bits) is not int:
return True
else:
hex_digits = int(floor(bits/4))
return sha(stamp).hexdigest().startswith('0'*hex_digits)
elif stamp.startswith('1:'): # Version 1
try:
claim, date, res, ext, rand, counter = stamp[2:].split(':')
except ValueError:
ERR.write("Malformed version 1 hashcash stamp!\n")
return False
if resource is not None and resource != res:
return False
elif type(bits) is int and bits > int(claim):
return False
elif check_expiration is not None:
good_until = strftime("%y%m%d%H%M%S", localtime(time()-check_expiration))
if date < good_until:
return False
elif callable(ds_callback) and ds_callback(stamp):
return False
else:
hex_digits = int(floor(int(claim)/4))
return sha(stamp).hexdigest().startswith('0'*hex_digits)
else: # Unknown ver or generalized hashcash
ERR.write("Unknown hashcash version: Minimal authentication!\n")
if type(bits) is not int:
return True
elif resource is not None and stamp.find(resource) < 0:
return False
else:
hex_digits = int(floor(bits/4))
return sha(stamp).hexdigest().startswith('0'*hex_digits)
def is_doublespent(stamp):
"""Placeholder for double spending callback function
The check() function may accept a 'ds_callback' argument, e.g.
check(stamp, "mertz@gnosis.cx", bits=20, ds_callback=is_doublespent)
This placeholder simply reports stamps as not being double spent.
"""
return False
if __name__=='__main__':
# Import Psyco if available
try:
import psyco
psyco.bind(_mint)
except ImportError:
pass
import optparse
out, err = sys.stdout.write, sys.stderr.write
parser = optparse.OptionParser(version="%prog 0.1",
usage="%prog -c|-m [-b bits] [string|STDIN]")
parser.add_option('-b', '--bits', type='int', dest='bits', default=20,
help="Specify required collision bits" )
parser.add_option('-m', '--mint', help="Mint a new stamp",
action='store_true', dest='mint')
parser.add_option('-c', '--check', help="Check a stamp for validity",
action='store_true', dest='check')
parser.add_option('-s', '--timer', help="Time the operation performed",
action='store_true', dest='timer')
parser.add_option('-n', '--raw', help="Suppress trailing newline",
action='store_true', dest='raw')
(options, args) = parser.parse_args()
start = time()
if options.mint: action = mint
elif options.check: action = check
else:
out("Try: %s --help\n" % sys.argv[0])
sys.exit()
if args: out(str(action(args[0], bits=options.bits)))
else: out(str(action(sys.stdin.read(), bits=options.bits)))
if not options.raw: sys.stdout.write('\n')
if options.timer:
timer = time()-start
err("Completed in %0.4f seconds (%d hashes per second)\n" %
(timer, tries[0]/timer))