Pure Python implementation of hashcash v.1
"David Mertz, Ph.D." <[email protected]>
| Newsgroups | gmane.mail.spam.hashcash |
|---|---|
| Message-ID | <[email protected]> |
I saw David McNab's Python implementation. Unfortunately, as near as I can tell (and suggested in the list archives also), McNab actually implements a different algorithm than hashcash. It might (or might not) be equivalent in security; but I cannot see any directly way to transform what he computes into an actual hashcash stamp. I created a Pure Python module today that seems to conform with the hashcash v.1 standard. But I probably missed something. It's just barely tested--call this an alpha version. Still, I figured I'd send it out here.... if anyone sees something I've done wrong, please let me know so I can correct it before distributing the module more widely. Yours, David... P.S. I'm doing an article for IBM developerWorks on hashcash. Look for it.
hashcash.py
(application/text, 5.9 KB)
"""Implement Hashcash version 1 protocol in Python
Double spend database not implemented in this module, but stub
for callbacks is provided in the 'check()' function
The function 'check()' will validate hashcash v.1 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
def mint(resource, bits=20, saltchars=8, stamp_seconds=False, now=None):
"Mint a new hashcash stamp for 'resource' with 'bits' of collision"
version = "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))
claim = str(bits)
ext = ''
salt = _salt(saltchars)
challenge = "%s:"*6 % (version, claim, ts, resource, ext, salt)
return challenge + _mint(challenge, bits)
def _salt(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
"""
prefix_hash = sha(challenge)
counter = 0
hex_digits = int(ceil(bits/4.))
while 1:
candidate = prefix_hash.copy()
candidate.update(hex(counter)[2:])
digest = candidate.hexdigest()
if digest.startswith('0'*hex_digits):
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, "[email protected]", bits=20, ds_callback=is_doublespent)
This placeholder simply reports stamps as not being double spent.
"""
return False