d33397a13d6584ce0c84f60202eddebf2d178d75
[sfa.git] / archive / changes / Checker.py
1 """
2 M2Crypto.SSL.Checker
3
4 Copyright (c) 2004-2005 Open Source Applications Foundation.
5 All rights reserved.
6 """
7
8 from M2Crypto import util, EVP
9 import re
10
11 class SSLVerificationError(Exception):
12     pass
13
14 class NoCertificate(SSLVerificationError):
15     pass
16
17 class WrongCertificate(SSLVerificationError):
18     pass
19
20 class WrongHost(SSLVerificationError):
21     def __init__(self, expectedHost, actualHost, fieldName='commonName'):
22         """
23         This exception will be raised if the certificate returned by the
24         peer was issued for a different host than we tried to connect to.
25         This could be due to a server misconfiguration or an active attack.
26         
27         @param expectedHost: The name of the host we expected to find in the
28                              certificate.
29         @param actualHost:   The name of the host we actually found in the
30                              certificate.
31         @param fieldName:    The field name where we noticed the error. This
32                              should be either 'commonName' or 'subjectAltName'.
33         """
34         if fieldName not in ('commonName', 'subjectAltName'):
35             raise ValueError('Unknown fieldName, should be either commonName or subjectAltName')
36         
37         SSLVerificationError.__init__(self)
38         self.expectedHost = expectedHost
39         self.actualHost = actualHost
40         self.fieldName = fieldName
41         
42     def __str__(self):
43         s = 'Peer certificate %s does not match host, expected %s, got %s' \
44                % (self.fieldName, self.expectedHost, self.actualHost)
45         if isinstance(s, unicode):
46             s = s.encode('utf8')
47         return s
48
49
50 class Checker:
51     def __init__(self, host=None, peerCertHash=None, peerCertDigest='sha1'):
52         self.host = host
53         self.fingerprint = peerCertHash
54         self.digest = peerCertDigest
55         self.numericIpMatch = re.compile('^[0-9]+(\.[0-9]+)*$')
56
57     def __call__(self, peerCert, host=None):
58         if peerCert is None:
59             raise NoCertificate('peer did not return certificate')
60
61         if host is not None:
62             self.host = host
63         
64         if self.fingerprint:
65             if self.digest not in ('sha1', 'md5'):
66                 raise ValueError('unsupported digest "%s"' %(self.digest))
67
68             if (self.digest == 'sha1' and len(self.fingerprint) != 40) or \
69                (self.digest == 'md5' and len(self.fingerprint) != 32):
70                 raise WrongCertificate('peer certificate fingerprint length does not match')
71             
72             der = peerCert.as_der()
73             md = EVP.MessageDigest(self.digest)
74             md.update(der)
75             digest = md.final()
76             if util.octx_to_num(digest) != int(self.fingerprint, 16):
77                 raise WrongCertificate('peer certificate fingerprint does not match')
78
79         if self.host:
80             hostValidationPassed = False
81
82             # XXX subjectAltName might contain multiple fields
83             # subjectAltName=DNS:somehost
84             try:
85                 subjectAltName = peerCert.get_ext('subjectAltName').get_value()
86                 if not self._match(self.host, subjectAltName, True):
87                     raise WrongHost(expectedHost=self.host, 
88                                     actualHost=subjectAltName,
89                                     fieldName='subjectAltName')
90                 hostValidationPassed = True
91             except LookupError:
92                 pass
93
94             # commonName=somehost
95
96
97 ##-----by Soner, comment outed
98
99 ##            if not hostValidationPassed:
100 ##                try:
101 ##                    commonName = peerCert.get_subject().CN
102 ##                    if not self._match(self.host, commonName):
103 ##                        raise WrongHost(expectedHost=self.host,
104 ##                                        actualHost=commonName,
105 ##                                        fieldName='commonName')
106 ##                except AttributeError:
107 ##                    raise WrongCertificate('no commonName in peer certificate')
108
109 ##-----/by Soner
110
111         return True
112
113     def _match(self, host, certHost, subjectAltName=False):
114         """
115         >>> check = Checker()
116         >>> check._match(host='my.example.com', certHost='DNS:my.example.com', subjectAltName=True)
117         True
118         >>> check._match(host='my.example.com', certHost='DNS:*.example.com', subjectAltName=True)
119         True
120         >>> check._match(host='my.example.com', certHost='DNS:m*.example.com', subjectAltName=True)
121         True
122         >>> check._match(host='my.example.com', certHost='DNS:m*ample.com', subjectAltName=True)
123         False
124         >>> check._match(host='my.example.com', certHost='my.example.com')
125         True
126         >>> check._match(host='my.example.com', certHost='*.example.com')
127         True
128         >>> check._match(host='my.example.com', certHost='m*.example.com')
129         True
130         >>> check._match(host='my.example.com', certHost='m*.EXAMPLE.com')
131         True
132         >>> check._match(host='my.example.com', certHost='m*ample.com')
133         False
134         >>> check._match(host='my.example.com', certHost='*.*.com')
135         False
136         >>> check._match(host='1.2.3.4', certHost='1.2.3.4')
137         True
138         >>> check._match(host='1.2.3.4', certHost='*.2.3.4')
139         False
140         >>> check._match(host='1234', certHost='1234')
141         True
142         """
143         # XXX See RFC 2818 and 3280 for matching rules, this is not
144         # XXX yet complete.
145
146         host = host.lower()
147         certHost = certHost.lower()
148
149         if subjectAltName:
150             if certHost[:4] != 'dns:':
151                 return False
152             certHost = certHost[4:]
153         
154         if host == certHost:
155             return True
156
157         if certHost.count('*') > 1:
158             # Not sure about this, but being conservative
159             return False
160
161         if self.numericIpMatch.match(host) or \
162                self.numericIpMatch.match(certHost.replace('*', '')):
163             # Not sure if * allowed in numeric IP, but think not.
164             return False
165
166         if certHost.find('\\') > -1:
167             # Not sure about this, maybe some encoding might have these.
168             # But being conservative for now, because regex below relies
169             # on this.
170             return False
171
172         # Massage certHost so that it can be used in regex
173         certHost = certHost.replace('.', '\.')
174         certHost = certHost.replace('*', '[^\.]*')
175         if re.compile('^%s$' %(certHost)).match(host):
176             return True
177
178         return False
179
180
181 if __name__ == '__main__':
182     import doctest
183     doctest.testmod()