diff -r ee9e580e5000 distutils2/index/errors.py --- a/distutils2/index/errors.py Mon Feb 28 21:53:27 2011 -0500 +++ b/distutils2/index/errors.py Thu Mar 03 23:00:02 2011 -0500 @@ -34,8 +34,12 @@ class UnsupportedHashName(DistutilsIndex class UnableToDownload(DistutilsIndexError): """All mirrors have been tried, without success""" class InvalidSearchField(DistutilsIndexError): """An invalid search field has been used""" + + +class ServerKeyError(DistutilsIndexError): + """An error occurred while retrieving the serverkey""" diff -r ee9e580e5000 distutils2/index/mirrors.py --- a/distutils2/index/mirrors.py Mon Feb 28 21:53:27 2011 -0500 +++ b/distutils2/index/mirrors.py Thu Mar 03 23:00:02 2011 -0500 @@ -1,16 +1,37 @@ """Utilities related to the mirror infrastructure defined in PEP 381. See http://www.python.org/dev/peps/pep-0381/ """ +import os +import re +import socket +import urllib2 + +try: + import ssl +except ImportError: + # FIXME Need to backport the ssl module for python < 2.6 compatibility. + # The SSL module hosted here http://pypi.python.org/pypi/ssl/1.15 works + # for python < 2.6. Should this be added to _backports? + # from distutils2._backport import ssl + raise + from string import ascii_lowercase -import socket +from httplib import HTTPSConnection +from urllib2 import HTTPSHandler, build_opener +from distutils2 import logger +from distutils2.index.verify import verify, load_key +from distutils2.index.errors import ServerKeyError + DEFAULT_MIRROR_URL = "last.pypi.python.org" +DEFAULT_SERVER_KEY_URL = "https://pypi.python.org" +_ca_certs = None def get_mirrors(hostname=None): """Return the list of mirrors from the last record found on the DNS entry:: >>> from distutils2.index.mirrors import get_mirrors >>> get_mirrors() @@ -47,8 +68,186 @@ def string_range(last): def product(*args, **kwds): pools = map(tuple, args) * kwds.get('repeat', 1) result = [[]] for pool in pools: result = [x + [y] for x in result for y in pool] for prod in result: yield tuple(prod) + + +# FIXME CertificateError, match_hostname(), and _dnsname_to_pat functions +# taken from http://pypi.python.org/pypi/backports.ssl_match_hostname/3.2a3 +# Should this be added to _backports? +class CertificateError(ValueError): + pass + + +def _dnsname_to_pat(dn): + pats = [] + for frag in dn.split(r'.'): + if frag == '*': + # When '*' is a fragment by itself, it matches a non-empty dotless + # fragment. + pats.append('[^.]+') + else: + # Otherwise, '*' matches any dotless fragment. + frag = re.escape(frag) + pats.append(frag.replace(r'\*', '[^.]*')) + return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) + + +def match_hostname(cert, hostname): + """Verify that *cert* (in decoded format as returned by + SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules + are mostly followed, but IP addresses are not accepted for *hostname*. + + CertificateError is raised on failure. On success, the function + returns nothing. + """ + if not cert: + raise ValueError("empty or no certificate") + dnsnames = [] + san = cert.get('subjectAltName', ()) + for key, value in san: + if key == 'DNS': + if _dnsname_to_pat(value).match(hostname): + return + dnsnames.append(value) + if not san: + # The subject is only checked when subjectAltName is empty + for sub in cert.get('subject', ()): + for key, value in sub: + # XXX according to RFC 2818, the most specific Common Name + # must be used. + if key == 'commonName': + if _dnsname_to_pat(value).match(hostname): + return + dnsnames.append(value) + if len(dnsnames) > 1: + raise CertificateError("hostname %r " + "doesn't match either of %s" + % (hostname, ', '.join(map(repr, dnsnames)))) + elif len(dnsnames) == 1: + raise CertificateError("hostname %r " + "doesn't match %r" + % (hostname, dnsnames[0])) + else: + raise CertificateError("no appropriate commonName or " + "subjectAltName fields were found") +# end backports.ssl_match_hostname + + +class _VerifiedHTTPSConnection(HTTPSConnection): + def connect(self): + sock = socket.socket(socket.AF_INET) + sock.connect((self.host, self.port)) + self.sock = ssl.wrap_socket(sock, cert_reqs=ssl.CERT_REQUIRED, + ca_certs=_ca_certs) + try: + match_hostname(self.sock.getpeercert(), self.host) + except CertificateError: + self.sock.shutdown(socket.SHUT_RDWR) + self.sock.close() + raise + + +class _PyPIHTTPSHandler(HTTPSHandler): + def https_open(self, req): + return self.do_open(_VerifiedHTTPSConnection, req) + + +def _get_serverkey(url, ca_certs): + global _ca_certs + _ca_certs = ca_certs + url_opener = build_opener(_PyPIHTTPSHandler()) + return url_opener.open(url + '/serverkey').read() + + +def get_server_key(url=DEFAULT_SERVER_KEY_URL, path=None, ca_certs=None, + use_cache=False): + """Retrieve the official serverkey from PyPI directly or + use a local cache as required by PEP 381. + + This function protects against a possible security threat described + in PEP 381 under the 'Mirror Authenticity' section. + + A man in the middle between the central index and the end user, or + between a mirror and the end user might tamper with datagrams. + + To detect man-in-the-middle attacks verify the SSL server certificate + when retrieving a new server key. + + param: url: PyPI server key URL + param: path: Location of the server_key cache file + param: ca_certs: path to ca_certs file + param: use_cache: read server_key from path or try and retrieve a new + one from the url provided + + Returns a string representing the serverkey in the PEM format as + generated by "openssl dsa -pubout". Raises ServerKeyError if the + serverkey cannot be retrieved or validated. + """ + if path is not None and use_cache: + # Check if we already have a valid server key. + # This key is not guaranteed to be up-to-date. + if os.path.isfile(path): + server_key = open(path, 'rb').read() + return server_key + else: + raise ServerKeyError("missing server key cache file: " + path) + else: + if ca_certs is None: + raise ServerKeyError('CA certs required for SSL validation') + if not os.path.isfile(ca_certs): + raise ServerKeyError("cannot locate CA cert file: " + ca_certs) + return _get_serverkey(url, ca_certs) + + +def _get_project_data(url, project): + return urllib2.urlopen(url + "/simple/" + project).read() + + +def _get_serversig(url, project): + return urllib2.urlopen(url + "/serversig/" + project).read() + + +def is_trustable(url, project, server_key): + """Verify a project being served from a PyPI mirror. + + This function protects against a possible security threat described in + PEP 381 under the 'Mirror Authenticity' section. + + The central index is assumed to be trusted, but the mirrors might be + tampered. + + In this case validate the project's simple page using the recommended + verification algorithm proposed by PEP 381 + + :param: url: the PyPI mirror to use + :param: project: the project name to verify. + Case sensitive (Django not django) + :param: server_key: PEP formated PyPI server key string as returned + by 'get_server_key()' + + Returns True if the mirror is trustable for this project, False otherwise. + """ + if url and project: + data = _get_project_data(url, project) + sig = _get_serversig(url, project) + else: + logger.error('could not retrieve project data or serversig ' + 'for %s' % project) + return False + key = load_key(server_key) + try: + # verify function taken from pypi tools + # https://svn.python.org/packages/trunk/pypi/tools/verify.py + if verify(key, data, sig): + return True + else: + return False + except ValueError: + # FIXME pypi seems to be case sensitive when trying to verify + # a project. Verifying Django works, but not django. + logger.warn('project name is case sensitive') + return False diff -r ee9e580e5000 distutils2/index/verify.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/distutils2/index/verify.py Thu Mar 03 23:00:02 2011 -0500 @@ -0,0 +1,157 @@ +"""Verify a DSA signature, for use with PyPI mirrors. + +Verification should use the following steps: +1. Download the DSA key from http://pypi.python.org/serverkey, as key_string +2. key = load_key(key_string) +3. Download the package page, from /simple//, as data +4. Download the package signature, from /serversig/, as sig +5. Check verify(key, data, sig) + +This code was taken from pypi tools +https://svn.python.org/packages/trunk/pypi/tools/verify.py +""" +import sha +import base64 + + +_SEQUENCE = 0x30 # cons +_INTEGER = 2 # prim +_BITSTRING = 3 # prim +_OID = 6 # prim + + +# DSA signature algorithm, taken from pycrypto 2.0.1 +# The license terms are the same as the ones for this module. +def _inverse(u, v): + """_inverse(u:long, u:long):long + Return the inverse of u mod v. + """ + u3, v3 = long(u), long(v) + u1, v1 = 1L, 0L + while v3 > 0: + q=u3 / v3 + u1, v1 = v1, u1 - v1*q + u3, v3 = v3, u3 - v3*q + while u1<0: + u1 = u1 + v + return u1 + + +def _verify(key, M, sig): + p, q, g, y = key + r, s = sig + if r<=0 or r>=q or s<=0 or s>=q: + return False + w=_inverse(s, q) + u1, u2 = (M*w) % q, (r*w) % q + v1 = pow(g, u1, p) + v2 = pow(y, u2, p) + v = ((v1*v2) % p) + v = v % q + return v==r +# END OF pycrypto + + +def _bytes2int(b): + value = 0 + for c in b: + value = value*256 + ord(c) + return value + + +def _asn1parse(string): + tag = ord(string[0]) + assert tag & 31 != 31 # only support one-byte tags + length = ord(string[1]) + assert length != 128 # indefinite length not supported + pos = 2 + if length > 128: + # multi-byte length + val = 0 + length -= 128 + val = _bytes2int(string[pos:pos+length]) + pos += length + length = val + data = string[pos:pos+length] + rest = string[pos+length:] + assert pos+length <= len(string) + if tag == _SEQUENCE: + result = [] + while data: + value, data = _asn1parse(data) + result.append(value) + elif tag == _INTEGER: + assert ord(data[0]) < 128 # negative numbers not supported + result = 0 + for c in data: + result = result*256 + ord(c) + elif tag == _BITSTRING: + result = data + elif tag == _OID: + result = data + else: + raise ValueError, "Unsupported tag %x" % tag + return (tag, result), rest + + +def load_key(string): + """load_key(string) -> key + + Convert a PEM format public DSA key into + an internal representation.""" + lines = [line.strip() for line in string.splitlines()] + assert lines[0] == "-----BEGIN PUBLIC KEY-----" + assert lines[-1] == "-----END PUBLIC KEY-----" + data = base64.decodestring(''.join(lines[1:-1])) + spki, rest = _asn1parse(data) + assert not rest + # SubjectPublicKeyInfo ::= SEQUENCE { + # algorithm AlgorithmIdentifier, + # subjectPublicKey BIT STRING } + assert spki[0] == _SEQUENCE + algoid, key = spki[1] + assert key[0] == _BITSTRING + key = key[1] + # AlgorithmIdentifier ::= SEQUENCE { + # algorithm OBJECT IDENTIFIER, + # parameters ANY DEFINED BY algorithm OPTIONAL } + assert algoid[0] == _SEQUENCE + algorithm, parameters = algoid[1] + assert algorithm[0] == _OID and algorithm[1] == '*\x86H\xce8\x04\x01' # dsaEncryption + # Dss-Parms ::= SEQUENCE { + # p INTEGER, + # q INTEGER, + # g INTEGER } + assert parameters[0] == _SEQUENCE + p, q, g = parameters[1] + assert p[0] == q[0] == g[0] == _INTEGER + p, q, g = p[1], q[1], g[1] + # Parse bit string value as integer + assert key[0] == '\0' # number of bits multiple of 8 + y, rest = _asn1parse(key[1:]) + assert not rest + assert y[0] == _INTEGER + y = y[1] + return p,q,g,y + + +def verify(key, data, sig): + """verify(key, data, sig) -> bool + + Verify autenticity of the signature created by key for + data. data is the bytes that got signed; signature is the + bytes that represent the signature, using the sha1+DSA + algorithm. key is an internal representation of the DSA key + as returned from load_key.""" + data = sha.new(data).digest() + data = _bytes2int(data) + # Dss-Sig-Value ::= SEQUENCE { + # r INTEGER, + # s INTEGER } + sig, rest = _asn1parse(sig) + assert not rest + assert sig[0] == _SEQUENCE + r, s = sig[1] + assert r[0] == s[0] == _INTEGER + sig = r[1], s[1] + return _verify(key, data, sig) diff -r ee9e580e5000 distutils2/tests/CA_CERTS --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/distutils2/tests/CA_CERTS Thu Mar 03 23:00:02 2011 -0500 @@ -0,0 +1,76 @@ +-----BEGIN CERTIFICATE----- +MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290 +IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB +IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA +Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO +BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi +MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ +ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ +8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6 +zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y +fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7 +w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc +G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k +epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q +laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ +QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU +fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826 +YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w +ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY +gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe +MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0 +IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy +dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw +czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0 +dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl +aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC +AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg +b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB +ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc +nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg +18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c +gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl +Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY +sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T +SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF +CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum +GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk +zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW +omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCDCCA/CgAwIBAgIBATANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290 +IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB +IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA +Y2FjZXJ0Lm9yZzAeFw0wNTEwMTQwNzM2NTVaFw0zMzAzMjgwNzM2NTVaMFQxFDAS +BgNVBAoTC0NBY2VydCBJbmMuMR4wHAYDVQQLExVodHRwOi8vd3d3LkNBY2VydC5v +cmcxHDAaBgNVBAMTE0NBY2VydCBDbGFzcyAzIFJvb3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCrSTURSHzSJn5TlM9Dqd0o10Iqi/OHeBlYfA+e2ol9 +4fvrcpANdKGWZKufoCSZc9riVXbHF3v1BKxGuMO+f2SNEGwk82GcwPKQ+lHm9WkB +Y8MPVuJKQs/iRIwlKKjFeQl9RrmK8+nzNCkIReQcn8uUBByBqBSzmGXEQ+xOgo0J +0b2qW42S0OzekMV/CsLj6+YxWl50PpczWejDAz1gM7/30W9HxM3uYoNSbi4ImqTZ +FRiRpoWSR7CuSOtttyHshRpocjWr//AQXcD0lKdq1TuSfkyQBX6TwSyLpI5idBVx +bgtxA+qvFTia1NIFcm+M+SvrWnIl+TlG43IbPgTDZCciECqKT1inA62+tC4T7V2q +SNfVfdQqe1z6RgRQ5MwOQluM7dvyz/yWk+DbETZUYjQ4jwxgmzuXVjit89Jbi6Bb +6k6WuHzX1aCGcEDTkSm3ojyt9Yy7zxqSiuQ0e8DYbF/pCsLDpyCaWt8sXVJcukfV +m+8kKHA4IC/VfynAskEDaJLM4JzMl0tF7zoQCqtwOpiVcK01seqFK6QcgCExqa5g +eoAmSAC4AcCTY1UikTxW56/bOiXzjzFU6iaLgVn5odFTEcV7nQP2dBHgbbEsPyyG +kZlxmqZ3izRg0RS0LKydr4wQ05/EavhvE/xzWfdmQnQeiuP43NJvmJzLR5iVQAX7 +6QIDAQABo4G/MIG8MA8GA1UdEwEB/wQFMAMBAf8wXQYIKwYBBQUHAQEEUTBPMCMG +CCsGAQUFBzABhhdodHRwOi8vb2NzcC5DQWNlcnQub3JnLzAoBggrBgEFBQcwAoYc +aHR0cDovL3d3dy5DQWNlcnQub3JnL2NhLmNydDBKBgNVHSAEQzBBMD8GCCsGAQQB +gZBKMDMwMQYIKwYBBQUHAgEWJWh0dHA6Ly93d3cuQ0FjZXJ0Lm9yZy9pbmRleC5w +aHA/aWQ9MTAwDQYJKoZIhvcNAQEEBQADggIBAH8IiKHaGlBJ2on7oQhy84r3HsQ6 +tHlbIDCxRd7CXdNlafHCXVRUPIVfuXtCkcKZ/RtRm6tGpaEQU55tiKxzbiwzpvD0 +nuB1wT6IRanhZkP+VlrRekF490DaSjrxC1uluxYG5sLnk7mFTZdPsR44Q4Dvmw2M +77inYACHV30eRBzLI++bPJmdr7UpHEV5FpZNJ23xHGzDwlVks7wU4vOkHx4y/CcV +Bc/dLq4+gmF78CEQGPZE6lM5+dzQmiDgxrvgu1pPxJnIB721vaLbLmINQjRBvP+L +ivVRIqqIMADisNS8vmW61QNXeZvo3MhN+FDtkaVSKKKs+zZYPumUK5FQhxvWXtaM +zPcPEAxSTtAWYeXlCmy/F8dyRlecmPVsYGN6b165Ti/Iubm7aoW8mA3t+T6XhDSU +rgCvoeXnkm5OvfPi2RSLXNLrAWygF6UtEOucekq9ve7O/e0iQKtwOIj1CodqwqsF +YMlIBdpTwd5Ed2qz8zw87YC8pjhKKSRf/lk7myV6VmMAZLldpGJ9VzZPrYPvH5JT +oI53V93lYRE9IwCQTDz6o2CTBKOvNfYOao9PSmCnhQVsRqGP9Md246FZV/dxssRu +FFxtbUFm3xuTsdQAw+7Lzzw9IYCpX2Nl/N3gX6T0K/CFcUHUZyX7GrGXrtaZghNB +0m6lG5kngOcLqagA +-----END CERTIFICATE----- diff -r ee9e580e5000 distutils2/tests/SERVER_KEY --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/distutils2/tests/SERVER_KEY Thu Mar 03 23:00:02 2011 -0500 @@ -0,0 +1,20 @@ +-----BEGIN PUBLIC KEY----- +MIIDOjCCAi0GByqGSM44BAEwggIgAoIBAQCBwcANyffVNhKIM4y8SxA8RyAAVGpc +6Bwzrje77sPqb4eBVunUrX9+2IhW7LFPBcm3OL1GPaz7wj7rou3L+N6itPqGQSD8 +PWrqm3zWe0NiqbRDY3Sd1EBmkNArrYmTEZgOYr30VOy0+u/lEc4PwJQ0jzEGQmT0 +WzMsU/tFF6BiZk0wrtrFC6gUkfTjxJkfyy3xIpkJJtUMCh+3KGTU6yw4sIj9ktgE +J6vZ5qF9vv++CrGK6gYBL6K1lEuIyIxFqNgv4h3odl3zLnnrwz7UUivHNiTAvwUN +Gw0B3rmglkCPnuGEHxV/5iS0bGglSuGvhqB+z8RSIqCJHucT/XIads//AhUAzKzS +Qr6lzqYUXG+HoRHLxaAbkucCggEAA/vRTkurr5Q36IEg//pDiSC2YjAzLy8X1tV8 +VV89nLHM7CCG9V1AUhOzKoI+gxbEREh+E6QaH7dhpmVYQaIaUDFlMx+RKfn+/rfz +f4JZJ37409I6aYKRXgCI80h8CcebSEVI7QNUYi9krYmrr3GRKun9zlgSQNy1gFbb +YMqLZWoJj5O3s5SSrbJZrRECW9IS4lSdCNb+i8njOY0gNSRwch117RMMM0wGCgRM +qMM+meZ+rd16Ds9oVA6I0eKgMkDDgax8b6Y/T2puG+zByGRFhSoDhgThnbHaerve +Dq9NOZSQsWX1IumxjC3YSZ3gtX62VNSyuBhknRFe2p/4K7HQHAOCAQUAAoIBADUc +jPu0m55V8GNeRuMVrq/SA1+/WkAMZb0m+bCajGjTBOkLiFdKazACytWyUiufbXG3 +2X3USapl1OmnJo5fnAuWXDB4KCuZW+iQaGomEYMvcRE2ez1b//5SdyCAO1kRZ2FK +kBUJAiVbQdomw86i6Bf5Bvz6W/lcWFuAIab3V1ivSQbDjTjB9JtylYv9aEMaxN4Z +4t2fZTr/2l+eisOfrwT/tM6ERmEWILon4BM+o8V0+HsyKzQbU8gfabtChTDOCMNe +GXzg7Upg7q+SAikm86bowBJaO2b2QL91d2ETWSdlmNJRUebtQatR8hkO4oIRxT5D +evqIn9NmpIix3KH0nfQ= +-----END PUBLIC KEY----- diff -r ee9e580e5000 distutils2/tests/pypiserver/test_project_verification/serverkey --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/distutils2/tests/pypiserver/test_project_verification/serverkey Thu Mar 03 23:00:02 2011 -0500 @@ -0,0 +1,20 @@ +-----BEGIN PUBLIC KEY----- +MIIDOjCCAi0GByqGSM44BAEwggIgAoIBAQCBwcANyffVNhKIM4y8SxA8RyAAVGpc +6Bwzrje77sPqb4eBVunUrX9+2IhW7LFPBcm3OL1GPaz7wj7rou3L+N6itPqGQSD8 +PWrqm3zWe0NiqbRDY3Sd1EBmkNArrYmTEZgOYr30VOy0+u/lEc4PwJQ0jzEGQmT0 +WzMsU/tFF6BiZk0wrtrFC6gUkfTjxJkfyy3xIpkJJtUMCh+3KGTU6yw4sIj9ktgE +J6vZ5qF9vv++CrGK6gYBL6K1lEuIyIxFqNgv4h3odl3zLnnrwz7UUivHNiTAvwUN +Gw0B3rmglkCPnuGEHxV/5iS0bGglSuGvhqB+z8RSIqCJHucT/XIads//AhUAzKzS +Qr6lzqYUXG+HoRHLxaAbkucCggEAA/vRTkurr5Q36IEg//pDiSC2YjAzLy8X1tV8 +VV89nLHM7CCG9V1AUhOzKoI+gxbEREh+E6QaH7dhpmVYQaIaUDFlMx+RKfn+/rfz +f4JZJ37409I6aYKRXgCI80h8CcebSEVI7QNUYi9krYmrr3GRKun9zlgSQNy1gFbb +YMqLZWoJj5O3s5SSrbJZrRECW9IS4lSdCNb+i8njOY0gNSRwch117RMMM0wGCgRM +qMM+meZ+rd16Ds9oVA6I0eKgMkDDgax8b6Y/T2puG+zByGRFhSoDhgThnbHaerve +Dq9NOZSQsWX1IumxjC3YSZ3gtX62VNSyuBhknRFe2p/4K7HQHAOCAQUAAoIBADUc +jPu0m55V8GNeRuMVrq/SA1+/WkAMZb0m+bCajGjTBOkLiFdKazACytWyUiufbXG3 +2X3USapl1OmnJo5fnAuWXDB4KCuZW+iQaGomEYMvcRE2ez1b//5SdyCAO1kRZ2FK +kBUJAiVbQdomw86i6Bf5Bvz6W/lcWFuAIab3V1ivSQbDjTjB9JtylYv9aEMaxN4Z +4t2fZTr/2l+eisOfrwT/tM6ERmEWILon4BM+o8V0+HsyKzQbU8gfabtChTDOCMNe +GXzg7Upg7q+SAikm86bowBJaO2b2QL91d2ETWSdlmNJRUebtQatR8hkO4oIRxT5D +evqIn9NmpIix3KH0nfQ= +-----END PUBLIC KEY----- diff -r ee9e580e5000 distutils2/tests/pypiserver/test_project_verification/serversig/distutils2/index.html Binary file distutils2/tests/pypiserver/test_project_verification/serversig/distutils2/index.html has changed diff -r ee9e580e5000 distutils2/tests/pypiserver/test_project_verification/simple/distutils2/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/distutils2/tests/pypiserver/test_project_verification/simple/distutils2/index.html Thu Mar 03 23:00:02 2011 -0500 @@ -0,0 +1,3 @@ +Links for test

Links for test

test-2.3.4.5.tar.gz
+2.3.4.5 home_page
+ \ No newline at end of file diff -r ee9e580e5000 distutils2/tests/test_index_mirrors.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/distutils2/tests/test_index_mirrors.py Thu Mar 03 23:00:02 2011 -0500 @@ -0,0 +1,89 @@ +"""Tests for the distutils2.index.mirrors module.""" + +import os + +from distutils2.index import mirrors +from distutils2.tests.support import LoggingCatcher, unittest +from distutils2.tests.pypi_server import use_http_server +from distutils2.index.mirrors import (is_trustable, get_server_key, + CertificateError) +from distutils2.index.errors import ServerKeyError + + +CWD = os.path.dirname(__file__) +SERVER_KEY = os.path.join(CWD, 'SERVER_KEY') +CA_CERTS = os.path.join(CWD, 'CA_CERTS') +MISSING_FILE = os.path.join(CWD, 'MISSING_FILE') + + +class MockSSLPyPI(object): + def __init__(self, return_value=None, _exception=None): + self._return_value = return_value + self._exception = _exception + + def __call__(self, *args, **kwargs): + if self._exception: + raise self._exception + return self._return_value + + +class MirrorVerificationTestCase(LoggingCatcher, unittest.TestCase): + + @use_http_server(static_filesystem_paths=['test_project_verification'], + static_uri_paths=['serverkey', 'simple', 'serversig']) + def test_get_server_key(self, server): + url = server.full_address + expected = open(SERVER_KEY, 'rb').read() + + # Test retrieving a server key when the server key cache is missing. + self.assertRaises(ServerKeyError, get_server_key, path=MISSING_FILE, + use_cache=True) + + # Test retrieving a server key when the server key cache is available. + server_key = get_server_key(path=SERVER_KEY, use_cache=True) + self.assertEqual(expected, server_key) + + # Test retrieving a server key when the SSL cert is invalid + # because no ca_certs where supplied. + self.assertRaises(ServerKeyError, get_server_key, use_cache=False, + url=url) + + # Test retrieving a server key when the SSL cert is valid. + # 'url' is not a valid SSL url causing 'url_opener.open' to skip + # SSL validation. + server_key = get_server_key(url, use_cache=False, ca_certs=CA_CERTS) + self.assertEqual(expected, server_key) + + # Test retrieving a server key when the SSL cert is invalid. + # Need a better way to test SSL; is a test PyPI SSL server needed? + _get_serverkey = mirrors._get_serverkey + mirrors._get_serverkey = MockSSLPyPI(_exception=CertificateError) + self.assertRaises(CertificateError, get_server_key, use_cache=False, + url=url, ca_certs=CA_CERTS) + mirrors._get_serverkey = _get_serverkey + + @use_http_server(static_filesystem_paths=['test_project_verification'], + static_uri_paths=['serverkey', 'simple', 'serversig']) + def test_is_trustable(self, server): + # Test verifying a mirror when all arguments are set to None. + url, project, server_key = None, None, None + results = is_trustable(url, project, server_key) + self.assertFalse(results) + + # Test verifying a mirror. + url = server.full_address + # Need to add the '/' here to make sure the test http server + # finds the index.html page. This is not required when verifying + # against PyPI mirrors. + project = 'distutils2/' + server_key = get_server_key(url, use_cache=False, ca_certs=CA_CERTS) + results = is_trustable(url, project, server_key) + self.assertTrue(results) + + +def test_suite(): + return unittest.makeSuite(MirrorVerificationTestCase) + + +if __name__ == '__main__': + unittest.main(defaultTest="test_suite") diff -r ee9e580e5000 docs/source/distutils/packageindex.rst --- a/docs/source/distutils/packageindex.rst Mon Feb 28 21:53:27 2011 -0500 +++ b/docs/source/distutils/packageindex.rst Thu Mar 03 23:00:02 2011 -0500 @@ -97,8 +97,53 @@ listed in the *index-servers* variable:: repository to work with:: python setup.py register -r http://example.com/pypi For convenience, the name of the section that describes the repository may also be used:: python setup.py register -r other + + +*************************** +Using Package Index Mirrors +*************************** + +The Python Package Index (PyPI) supports a network of mirrors as described +in `PEP 381 `_ -- Mirroring +infrastructure for PyPI. + + +.. _pypi-mirrors: + +Mirror listings +=============== + +You can get a list of available mirrors using the :func:`get_mirrors()` method:: + + >>> from distutils2.index.mirrors import get_mirrors + >>> get_mirrors() + ['a.pypi.python.org', 'b.pypi.python.org', 'c.pypi.python.org'] + + +.. _mirror-authenticity: + +Mirror Authenticity +=================== + +Clients need to perform the following steps to validate a project being +hosted by a PyPI mirror:: + + >>> from distutils2.index.mirrors import get_mirrors + >>> from distutils2.index.mirrors import get_server_key + >>> from distutils2.index.mirrors import is_trustable + >>> mirror_url = get_mirrors()[1] + >>> mirror_url + 'http://b.pypi.python.org' + >>> project = 'Django' + >>> server_key_url = 'https://pypi.python.org' + >>> serverkey = get_server_key(server_key_url, use_cache=False, ca_certs='/tmp/CA_CERTS') + >>> is_trustable(mirror_url, project, serverkey) + True + +Once a mirror 'is_trustable' for a given project, you can safely download +distributions from the project's simple page hosted on the trusted mirror.