103 lines
4.5 KiB
Python
103 lines
4.5 KiB
Python
#!/usr/bin/env python
|
|
|
|
from pyasn1.type import univ, namedtype
|
|
from pyasn1.codec.der import encoder as der_encoder
|
|
from Crypto.PublicKey import RSA
|
|
from Crypto.Signature import PKCS1_PSS
|
|
from Crypto.Hash import SHA, SHA512
|
|
from Crypto.Random import random
|
|
|
|
class rsa_pk(univ.Sequence):
|
|
componentType = namedtype.NamedTypes(
|
|
namedtype.NamedType('n', univ.Integer()),
|
|
namedtype.NamedType('e', univ.Integer())
|
|
)
|
|
|
|
def keyid(pem_key_data):
|
|
"""Return the hexadecimal keyid for the public key represented by
|
|
pem_key_data. (This also works when given a private key; the keyid
|
|
returned is then the keyid of the corresponding public key.) Note that
|
|
this is only valid for an RSA key, not for any other type of public
|
|
key. The caller must verify that pem_key_data represents a valid RSA
|
|
public or private key. The keyid calculation is the same one commonly
|
|
performed by certificate authorities, as specified in RFC 5280."""
|
|
r = RSA.importKey(pem_key_data)
|
|
(n, e) = r.publickey().n, r.publickey().e
|
|
# Try to forget the other key parameters (in case it was a private key)
|
|
del r
|
|
pk = rsa_pk()
|
|
pk.setComponentByName("n",n)
|
|
pk.setComponentByName("e",e)
|
|
return SHA.new(der_encoder.encode(pk)).hexdigest()
|
|
|
|
class POPChallengeResponder(object):
|
|
def __init__(self, sought_id, server_nonce):
|
|
# XXX TODO: possibly we should enforce constraints on the length
|
|
# and structure of the server nonce (e.g. it must be exactly 32
|
|
# lowercase hex digits or we won't respond).
|
|
self.server_nonce = server_nonce
|
|
self.sought_id = sought_id
|
|
self.nonce = random.long_to_bytes(random.getrandbits(256)).encode("hex")
|
|
# XXX TODO: we should find the private key immediately when the
|
|
# responder object is created (if only we knew where to look for
|
|
# it!) -- equivalent to calling self.find_priv() with an appropriate
|
|
# list of candidate files.
|
|
self.privkey = None
|
|
|
|
def find_priv(self, candidate_paths):
|
|
"""Get the private key corresponding to the sought key ID if it is
|
|
available in any of the files in candidatepaths."""
|
|
for f in candidate_paths:
|
|
try:
|
|
with open(f) as candidate:
|
|
pem_data = candidate.read(65536)
|
|
if keyid(pem_data) == self.sought_id:
|
|
this_key = RSA.importKey(pem_data)
|
|
if this_key.has_private():
|
|
# Only private keys are appropriate here, even
|
|
# though keyid() is defined for both public and
|
|
# private keys!
|
|
self.privkey = this_key
|
|
del this_key
|
|
return
|
|
del this_key
|
|
except (IOError, ValueError) as e:
|
|
# If file can't be read or doesn't contain an RSA key,
|
|
# go on to the next file
|
|
continue
|
|
self.privkey = None
|
|
|
|
def respond_challenge(self):
|
|
if not self.privkey:
|
|
# If the matching private key wasn't found, the challenge can't
|
|
# be satisfied.
|
|
return None
|
|
to_sign = "chocolate protocol %s %s" % (self.nonce, self.server_nonce)
|
|
# XXX TODO What is an appropriate and safe RSA signature algorithm to
|
|
# use for creating signatures? Is the use of PKCS#1 PSS with SHA-512
|
|
# safe? Is this implementation free of timing attacks?
|
|
sig = PKCS1_PSS.new(self.privkey).sign(SHA512.new(to_sign))
|
|
# Try to forget the private key now that it's been used.
|
|
self.privkey = None
|
|
return (self.nonce, sig)
|
|
|
|
|
|
# Server-side things
|
|
|
|
# This makes a challenge given a public key. To get the public key from
|
|
# a particular certifcate: openssl x509 -in cert.pem -pubkey -noout
|
|
def make_challenge(pem_data):
|
|
"""Create a proof-of-possession challenge for a particular public key.
|
|
(The caller must verify that pem_data is a valid RSA public key.)"""
|
|
server_nonce = random.long_to_bytes(random.getrandbits(256)).encode("hex")
|
|
return keyid(pem_data), server_nonce
|
|
|
|
def verify_challenge_response(pubkey, challenge_string, client_nonce, sig):
|
|
"""Is sig a valid signature (verified using pubkey) for the combination
|
|
of the specified challenge_string and client_nonce?"""
|
|
try:
|
|
key = RSA.importKey(pubkey)
|
|
except:
|
|
return False
|
|
text = "chocolate protocol %s %s" % (client_nonce, challenge_string)
|
|
return PKCS1_PSS.new(key).verify(SHA512.new(text), sig)
|