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 Mon Feb 28 22:27:27 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 Mon Feb 28 22:27:27 2011 -0500 @@ -1,16 +1,31 @@ """Utilities related to the mirror infrastructure defined in PEP 381. See http://www.python.org/dev/peps/pep-0381/ """ +import os +import socket +import urllib2 + +try: + import ssl +except ImportError: + # Need to backport the ssl module for python < 2.6 compatibility. + # from distutils2._backport import ssl + pass + from string import ascii_lowercase -import socket +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" 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 +62,127 @@ 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) + + +def validate_cert(url, ca_certs=None, port=443): + """Validate the target URL's SSL cert. + + param: url: url protected via SSL + param: ca_certs: path to ca_certs file + + Returns True if the SSL cert is valid, False otherwise. + """ + if ca_certs is None: + logger.warn("CA certs required for SSL validation") + return False + if not os.path.isfile(ca_certs): + logger.warn("cannot locate CA cert file: " + ca_certs) + return False + + if url and ca_certs and port: + return ssl.get_server_certificate((url, port), ca_certs=ca_certs) + else: + logger.warn("a valid url is required for SSL validation") + return False + + +def get_server_key(url=DEFAULT_SERVER_KEY_URL, path=None, ca_certs=None, + use_cache=True, validate_ssl_cert=True): + """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 + param: validate_ssl_cert: whether to validate the key server's SSL cert + + Returns None on error or a string representing the serverkey in the + PEM format as generated by "openssl dsa -pubout". + """ + 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) + elif not use_cache: + try: + if validate_ssl_cert and url: + if validate_cert(url, ca_certs): + logger.info("ssl cert valid for " + url) + else: + raise ServerKeyError("ssl cert invalid for " + url) + else: + logger.info("skipping ssl cert validation for " + url) + return urllib2.urlopen(url + '/serverkey').read() + except ssl.SSLError: + logger.error("can't validate ssl cert for " + url) + raise + else: + raise ServerKeyError("can't validate or invalid ssl cert for " + url) + + +def _get_package_data(url, package): + return urllib2.urlopen(url + "/simple/" + package).read() + + +def _get_serversig(url, package): + return urllib2.urlopen(url + "/serversig/" + package).read() + + +def verify_package(url, package, server_key): + """Verify a package 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 package using the recommend verification + algorithm proposed by PEP 381 + + :param: url: the PyPI mirror to use + :param: package: the package 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 package is valid, False otherwise. + """ + if url and package: + data = _get_package_data(url, package) + sig = _get_serversig(url, package) + else: + logger.error('could not retrieve package data or serversig ' + 'for %s' % package) + return False + key = load_key(server_key) + try: + if verify(key, data, sig): + return True + else: + return False + except ValueError: + # FIXME pypi seems to be case sensitive when trying to verify + # packages. Verifying Django works, but not django. + logger.warn('package 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 Mon Feb 28 22:27:27 2011 -0500 @@ -0,0 +1,154 @@ +"""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) +""" +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 Mon Feb 28 22:27:27 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 Mon Feb 28 22:27:27 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_package_verification/serverkey --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/distutils2/tests/pypiserver/test_package_verification/serverkey Mon Feb 28 22:27:27 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_package_verification/serversig/distutils2/index.html Binary file distutils2/tests/pypiserver/test_package_verification/serversig/distutils2/index.html has changed diff -r ee9e580e5000 distutils2/tests/pypiserver/test_package_verification/simple/distutils2/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/distutils2/tests/pypiserver/test_package_verification/simple/distutils2/index.html Mon Feb 28 22:27:27 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 Mon Feb 28 22:27:27 2011 -0500 @@ -0,0 +1,114 @@ +"""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 (validate_cert, get_server_key, + verify_package, DEFAULT_SERVER_KEY_URL) + + +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): + self._return_value = return_value + + def __call__(self, *args, **kwargs): + return self._return_value + + +class PackageVerificationTestCase(LoggingCatcher, unittest.TestCase): + + def test_validate_cert(self): + url = DEFAULT_SERVER_KEY_URL + + # test validating a SSL cert when the ca_certs are set to None. + results = validate_cert(url, ca_certs=None) + self.assertFalse(results) + + # test validating a valid SSL cert. + mirrors.ssl.get_server_certificate = MockSSLPyPI(return_value=True) + results = validate_cert(url, ca_certs=CA_CERTS) + self.assertTrue(results) + + # test validating a valid SSL cert with missing CA certs. + mirrors.ssl.get_server_certificate = MockSSLPyPI(return_value=True) + results = validate_cert(url, ca_certs=MISSING_FILE) + self.assertFalse(results) + + # test validating an invalid SSL cert. + mirrors.ssl.get_server_certificate = MockSSLPyPI(return_value=False) + results = validate_cert(url, ca_certs=CA_CERTS) + self.assertFalse(results) + + @use_http_server(static_filesystem_paths=['test_package_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. + server_key = get_server_key(path=MISSING_FILE, use_cache=True) + self.assertIsNone(server_key) + + # 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 skipping SSL cert validation + # and not using a cached server key. + server_key = get_server_key(url, use_cache=False, + validate_ssl_cert=False) + self.assertEqual(expected, server_key) + + # test retrieving a server key when the SSL cert is invalid + # because no ca_certs where supplied. + mirrors.ssl.get_server_certificate = MockSSLPyPI(return_value=True) + server_key = get_server_key(url, use_cache=False, + validate_ssl_cert=True) + self.assertIsNone(server_key) + + # test retrieving a server key when the SSL cert is invalid + mirrors.ssl.get_server_certificate = MockSSLPyPI(return_value=False) + server_key = get_server_key(url, use_cache=False, ca_certs=CA_CERTS) + self.assertIsNone(server_key) + + # test retrieving a server key when the SSL cert is valid. + mirrors.ssl.get_server_certificate = MockSSLPyPI(return_value=True) + server_key = get_server_key(url, use_cache=False, ca_certs=CA_CERTS) + self.assertEqual(expected, server_key) + + @use_http_server(static_filesystem_paths=['test_package_verification'], + static_uri_paths=['serverkey', 'simple', 'serversig']) + def test_verify_package(self, server): + # test verifying a package when all arguments are set to None. + url, package, server_key = None, None, None + results = verify_package(url, package, server_key) + self.assertFalse(results) + + # test verifying a package + 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. + package = 'distutils2/' + server_key = get_server_key(url, use_cache=False, ca_certs=CA_CERTS, + validate_ssl_cert=False) + results = verify_package(url, package, server_key) + self.assertTrue(results) + + +def test_suite(): + return unittest.makeSuite(PackageVerificationTestCase) + + +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 Mon Feb 28 22:27:27 2011 -0500 @@ -97,8 +97,87 @@ 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. + +A human-readable list of mirrors is available at http://pypi.python.org/mirrors. +This page also explains how to register a new mirror. + +.. _pypi-mirrors: + +Mirror listings +=============== + +The mirror list is provided as a list of host names of the form:: + + X.pypi.python.org + +The values of X are the sequence a,b,c,...,aa,ab,... a.pypi.python.org is the +master server; the mirrors start with b. A CNAME record last.pypi.python.org +points to the last host name. + +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 +=================== + +For each package, a mirrored signature is provided at /serversig/. +This is the DSA signature of the parallel URL /simple/, in DER form, +using SHA-1 with DSA:: + + http:://b.pypi.python.org/serversig/Django/ + http:://b.pypi.python.org/simple/Django/ + +The central index provides a DSA key at the URL /serverkey, in the PEM format +as generated by "openssl dsa -pubout":: + + https://pypi.python.org/serverkey + +About once a year, the serverkey will be replaced with a new one. Mirrors will +have to re-fetch all /serversig pages. Clients using mirrors need to find a trusted +copy of the new server key. One way to obtain one is to download it from +https://pypi.python.org/serverkey. To detect man-in-the-middle attacks, clients +need to verify the SSL server certificate, which will be signed by the CACert +authority. + +Clients using a mirror need to perform the following steps to verify a package: + +1. download the /simple page and compute its SHA-1 hash +2. compute the DSA signature of that hash +3. download the corresponding /serversig and compare it (byte-for-byte) + with the value computed in step 2. +4. compute and verify (against the /simple page) the MD-5 hashes of all files + they download from the mirror. + +You can achieve the first 3 steps like this:: + + >>> from distutils2.index.mirrors import get_server_key, verify_package + >>> mirror_url = 'http://b.pypi.python.org' + >>> serverkey_url = 'https://pypi.python.org' + >>> package_name = 'Django' + >>> serverkey = get_server_key(serverkey_url, use_cache=False, ca_certs='/tmp/CA_CERTS') + >>> package = 'Django' + >>> verify_package(mirror_url, package_name, serverkey) + True + +Verification is not needed when downloading from central index, and should be +avoided to reduce the computation overhead.