mirror of
https://github.com/certbot/certbot.git
synced 2026-06-06 07:12:54 -04:00
use hashcash in protocol
This commit is contained in:
parent
bb272f16ca
commit
064148df29
4 changed files with 230 additions and 3 deletions
|
|
@ -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():
|
||||
|
|
|
|||
1
client-webserver/hashcash.py
Symbolic link
1
client-webserver/hashcash.py
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../server-ca/hashcash.py
|
||||
|
|
@ -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
208
server-ca/hashcash.py
Normal 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))
|
||||
|
||||
Loading…
Reference in a new issue