1 """SAML 2.0 Utilities module for M2Crypto SSL functionality
2
3 NERC DataGrid Project
4 """
5 __author__ = "P J Kershaw"
6 __date__ = "02/07/07"
7 __copyright__ = "(C) 2009 Science and Technology Facilities Council"
8 __license__ = "http://www.apache.org/licenses/LICENSE-2.0"
9 __contact__ = "Philip.Kershaw@stfc.ac.uk"
10 __revision__ = '$Id: m2crypto.py 8049 2012-03-28 15:57:38Z pjkersha $'
11 import logging
12 log = logging.getLogger(__name__)
13
14 from warnings import warn
15
16 import os
17 import re
18
19
20 from time import strptime
21 from datetime import datetime
22
23 import M2Crypto
24 from M2Crypto import SSL, X509
25 from M2Crypto.httpslib import HTTPSConnection as _HTTPSConnection
29 """Exception handling for NDG X.500 DN class."""
30
33 "NDG X500 Distinguished name"
34
35
36
37
38 __shortNameLUT = {
39 'commonName': 'CN',
40 'organisationalUnitName': 'OU',
41 'organisation': 'O',
42 'countryName': 'C',
43 'emailAddress': 'EMAILADDRESS',
44 'localityName': 'L',
45 'stateOrProvinceName': 'ST',
46 'streetAddress': 'STREET',
47 'domainComponent': 'DC',
48 'userid': 'UID'
49 }
50 SLASH_PARSER_RE_STR = '/(%s)=' % '|'.join(__shortNameLUT.keys() +
51 __shortNameLUT.values())
52 SLASH_PARSER_RE = re.compile(SLASH_PARSER_RE_STR)
53
54 COMMA_PARSER_RE_STR = '[,]?\s*(%s)=' % '|'.join(__shortNameLUT.keys() +
55 __shortNameLUT.values())
56 COMMA_PARSER_RE = re.compile(COMMA_PARSER_RE_STR)
57
58 - def __init__(self, dn=None, m2CryptoX509Name=None, separator=None):
59
60 """Create a new X500 Distinguished Name
61
62 @type m2CryptoX509Name: M2Crypto.X509.X509_Name
63 @param m2CryptoX509Name: initialise using using an
64 M2Crypto.X509.X509_Name
65 @type dn: basestring
66 @param dn: initialise using a distinguished name string
67 @type separator: basestring
68 @param: separator: separator used to delimit dn fields - usually '/'
69 or ','. If dn is input and separator is omitted the separator
70 character will be automatically parsed from the dn string.
71 """
72
73
74 self.__dat = {}.fromkeys(X500DN.__shortNameLUT.values(), '')
75
76 dict.__init__(self)
77
78 self.__separator = None
79
80
81 if separator is not None:
82 if not isinstance(separator, basestring):
83 raise X500DNError("dn Separator must be a valid string")
84
85
86 if len(separator.lstrip()) is not 1:
87 raise X500DNError("dn separator must be a single character")
88
89 self.__separator = separator
90
91 if m2CryptoX509Name is not None:
92
93 self.deserialise(str(m2CryptoX509Name))
94
95 elif dn is not None:
96
97
98 if self.__separator is None:
99 self.__separator = self.parseSeparator(dn)
100
101
102 self.deserialise(dn)
103
104 @classmethod
106 """Convenience method for parsing DN string into a new instance
107 """
108 return cls(dn=dn)
109
111 """Give representation based on underlying dict object"""
112 return repr(self.__dat)
113
115 """Behaviour for print and string statements - convert DN into
116 serialised format."""
117 return self.serialise()
118
120 """Return true if the all the fields of the two DNs are equal"""
121
122 if not isinstance(x500dn, X500DN):
123 return False
124
125 return self.__dat.items() == x500dn.items()
126
128 """Return true if the all the fields of the two DNs are equal"""
129
130 if not isinstance(x500dn, X500DN):
131 return False
132
133 return self.__dat.items() != x500dn.items()
134
136 """Prevent keys from being deleted."""
137 raise X500DNError('Keys cannot be deleted from the X500DN')
138
140
141
142 if self.__dat.has_key(key):
143
144
145 return self.__dat[key]
146
147 elif X500DN.__shortNameLUT.has_key(key):
148
149
150
151 shortName = X500DN.__shortNameLUT[key]
152 return self.__dat[shortName]
153
154 else:
155
156 raise KeyError('Key "' + key + '" not recognised for X500DN')
157
159
160
161 if self.__dat.has_key(key):
162
163
164 self.__dat[key] = item
165
166 elif X500DN.__shortNameLUT.has_key(key):
167
168
169
170 shortName = X500DN.__shortNameLUT[key]
171 self.__dat[shortName] = item
172
173 else:
174
175 raise KeyError('Key "' + key + '" not recognised for X500DN')
176
178 raise X500DNError("Data cannot be cleared from X500DN")
179
183
185 return self.__dat.keys()
186
188 return self.__dat.items()
189
191 return self.__dat.values()
192
195
196
199
200 - def get(self, *arg):
201 return self.__dat.get(*arg)
202
204 """Combine fields in Distinguished Name into a single string."""
205
206 if separator:
207 if not isinstance(separator, basestring):
208 raise X500DNError("Separator must be a valid string")
209
210 self.__separator = separator
211
212 else:
213
214 separator = '/'
215
216
217
218 if separator == '/':
219 sDN = separator
220 else:
221 sDN = ''
222
223 dnList = []
224 for (key, val) in self.__dat.items():
225 if val:
226 if isinstance(val, tuple):
227 dnList += [separator.join(["%s=%s" % (key, valSub) \
228 for valSub in val])]
229 else:
230 dnList += ["%s=%s" % (key, val)]
231
232 sDN += separator.join(dnList)
233
234 return sDN
235
236 serialize = serialise
237
239 """Break up a DN string into it's constituent fields and use to
240 update the object's dictionary"""
241
242 if separator:
243 if not isinstance(separator, basestring):
244 raise X500DNError("Separator must be a valid string")
245
246 self.__separator = separator
247
248
249
250 if self.__separator is None:
251 self.__separator = self.parseSeparator(dn)
252
253 if self.__separator == '/':
254 parserRe = self.__class__.SLASH_PARSER_RE
255
256 elif self.__separator == ',':
257 parserRe = self.__class__.COMMA_PARSER_RE
258 else:
259 raise X500DNError("DN field separator %r not recognised" %
260 self.__separator)
261
262 try:
263 dnFields = parserRe.split(dn)
264 if len(dnFields) < 2:
265 raise X500DNError("Error parsing DN string: \"%s\"" % dn)
266
267 items = zip(dnFields[1::2], dnFields[2::2])
268
269
270 self.__dat.fromkeys(self.__dat, '')
271
272
273
274 parsedDN = {}
275 for key, val in items:
276 key = key.strip()
277 if key in parsedDN:
278 if isinstance(parsedDN[key], tuple):
279 parsedDN[key] = tuple(list(parsedDN[key]) + [val])
280 else:
281 parsedDN[key] = (parsedDN[key], val)
282 else:
283 parsedDN[key] = val
284
285
286 for key, val in parsedDN.items():
287 if key not in self.__dat and key not in self.__shortNameLUT:
288 raise X500DNError('Invalid field "%s" in input DN string' %
289 key)
290
291 self.__dat[key] = val
292
293
294 except Exception, excep:
295 raise X500DNError("Error de-serialising DN \"%s\": %s" % \
296 (dn, str(excep)))
297
298 deserialize = deserialise
299
301 """Attempt to parse the separator character from a given input
302 DN string. If not found, return None
303
304 DNs don't use standard separators e.g.
305
306 /C=UK/O=eScience/OU=CLRC/L=DL/CN=AN Other
307 CN=SUM Oneelse,L=Didcot, O=RAL,OU=SSTD
308
309 This function isolates and identifies the character. - In the above,
310 '/' and ',' respectively"""
311
312
313
314
315
316
317
318
319
320
321
322 regExpr = '|'.join(['\W\s*'+i+'=' for i in self.__dat.keys()])
323 match = re.findall(regExpr, dn)
324
325
326
327
328 sepList = [i[0:1] for i in match]
329
330
331
332 if not [i for i in sepList if i != sepList[0]]:
333 return sepList[0]
334 else:
335 return None
336
337 @classmethod
339 """Convenience method to create an X500DN object from a DN string
340 @type dn: basestring
341 @param dn: Distinguished Name
342 """
343 return cls(dn=dn)
344
345 Deserialise = Deserialize = Parse
346
349 """Exception handling for NDG X.509 Certificate handling class."""
350
352 """Error reading in certificate from file"""
353
355 """Error parsing a certificate"""
356
358 """Call from X509Cert.isValidTime if certificates not before time is
359 BEFORE the current system time"""
360
362 """Call from X509Cert.isValidTime if certificate has expired"""
363
366 "NDG X509 Certificate Handling"
367
368 formatPEM = M2Crypto.X509.FORMAT_PEM
369 formatDER = M2Crypto.X509.FORMAT_DER
370
371 - def __init__(self, filePath=None, m2CryptoX509=None):
372
373
374 if filePath is not None:
375 if not isinstance(filePath, basestring):
376 raise X509CertError("Certificate File Path input must be a "
377 "valid string")
378
379 self.__filePath = filePath
380 self.__dn = None
381 self.__dtNotBefore = None
382 self.__dtNotAfter = None
383
384 if m2CryptoX509:
385 self.__setM2CryptoX509(m2CryptoX509)
386 else:
387 self.__m2CryptoX509 = None
388
389 - def read(self,
390 filePath=None,
391 format=None,
392 warningStackLevel=3,
393 **isValidTimeKw):
394 """Read a certificate from PEM encoded DER format file
395
396 @type filePath: basestring
397 @param filePath: file path of PEM format file to be read
398
399 @type format: int
400 @param format: format of input file - PEM is the default. Set to
401 X509Cert.formatDER for DER format
402
403 @type isValidTimeKw: dict
404 @param isValidTimeKw: keywords to isValidTime() call"""
405
406 if format is None:
407 format = X509Cert.formatPEM
408
409
410 if filePath is not None:
411 if not isinstance(filePath, basestring):
412 raise X509CertError("Certificate File Path input must be a "
413 "valid string")
414
415 self.__filePath = filePath
416
417 try:
418 self.__m2CryptoX509 = M2Crypto.X509.load_cert(self.__filePath,
419 format=format)
420 except Exception, e:
421 raise X509CertReadError("Error loading certificate \"%s\": %s" %
422 (self.__filePath, e))
423
424
425
426 self.__setM2CryptoX509()
427
428 self.isValidTime(warningStackLevel=warningStackLevel, **isValidTimeKw)
429
430 - def parse(self,
431 certTxt,
432 format=None,
433 warningStackLevel=3,
434 **isValidTimeKw):
435 """Read a certificate input as a string
436
437 @type certTxt: basestring
438 @param certTxt: PEM encoded certificate to parse
439
440 @type format: int
441 @param format: format of input file - PEM is the default. Set to
442 X509Cert.formatDER for DER format
443
444 @type isValidTimeKw: dict
445 @param isValidTimeKw: keywords to isValidTime() call"""
446
447 if format is None:
448 format = X509Cert.formatPEM
449
450 try:
451
452
453
454
455
456
457
458 self.__m2CryptoX509 = M2Crypto.X509.load_cert_string(str(certTxt),
459 format=format)
460 except Exception, e:
461 raise X509CertParseError("Error loading certificate: %s" % e)
462
463
464
465 self.__setM2CryptoX509()
466
467 self.isValidTime(warningStackLevel=warningStackLevel, **isValidTimeKw)
468
470 """Private method allows class members to be updated from the
471 current M2Crypto object. __m2CryptoX509 must have been set."""
472
473 if m2CryptoX509 is not None:
474 if not isinstance(m2CryptoX509, M2Crypto.X509.X509):
475 raise TypeError("Incorrect type for input M2Crypto.X509.X509 "
476 "object")
477
478 self.__m2CryptoX509 = m2CryptoX509
479
480
481 m2CryptoX509Name = self.__m2CryptoX509.get_subject()
482
483
484 self.__dn = X500DN(m2CryptoX509Name=m2CryptoX509Name)
485
486
487
488
489
490
491
492 try:
493 m2CryptoNotBefore = self.__m2CryptoX509.get_not_before()
494 self.__dtNotBefore=self.__m2CryptoUTC2datetime(m2CryptoNotBefore)
495
496 except Exception, e:
497 raise X509CertError("Not Before time: %s" % e)
498
499 try:
500 m2CryptoNotAfter = self.__m2CryptoX509.get_not_after()
501 self.__dtNotAfter = self.__m2CryptoUTC2datetime(m2CryptoNotAfter)
502
503 except Exception, e:
504 raise X509CertError("Not After time: %s" % e)
505
507 "Return M2Crypto X.509 cert object"
508 return self.__m2CryptoX509
509
510 m2CryptoX509 = property(fset=__setM2CryptoX509,
511 fget=__getM2CryptoX509,
512 doc="M2Crypto.X509.X509 type")
513
515 """Return certificate file content as a PEM format
516 string"""
517 return self.asPEM(**kw)
518
519 - def asPEM(self, filePath=None):
520 """Return certificate file content as a PEM format
521 string"""
522
523
524
525 if self.__m2CryptoX509 is None:
526 self.read(filePath)
527
528 return self.__m2CryptoX509.as_pem()
529
531 """Return certificate file content in DER format"""
532
533
534 assert(self.__m2CryptoX509)
535 return self.__m2CryptoX509.as_der()
536
537
539 """Get X500 Distinguished Name."""
540 return self.__dn
541
542 dn = property(fget=__getDN, doc="X.509 Distinguished Name")
543
545 """Get X.509 Certificate version"""
546 if self.__m2CryptoX509 is None:
547 return None
548
549 return self.__m2CryptoX509.get_version()
550
551 version = property(fget=__getVersion, doc="X.509 Certificate version")
552
554 """Get Serial Number"""
555 if self.__m2CryptoX509 is None:
556 return None
557
558 return self.__m2CryptoX509.get_serial_number()
559
560 serialNumber = property(fget=__getSerialNumber,
561 doc="X.509 Certificate Serial Number")
562
564 """Get not before validity time as datetime type"""
565 if self.__m2CryptoX509 is None:
566 return None
567
568 return self.__dtNotBefore
569
570 notBefore = property(fget=__getNotBefore,
571 doc="Not before validity time as datetime type")
572
574 """Get not after validity time as datetime type"""
575 if self.__m2CryptoX509 is None:
576 return None
577
578 return self.__dtNotAfter
579
580 notAfter = property(fget=__getNotAfter,
581 doc="Not after validity time as datetime type")
582
584 """Get public key
585
586 @return: RSA public key for certificate
587 @rtype: M2Crypto.RSA.RSA_pub"""
588 if self.__m2CryptoX509 is None:
589 return None
590
591 return self.__m2CryptoX509.get_pubkey()
592
593 pubKey = property(fget=__getPubKey, doc="Public Key")
594
596 """Get Certificate issuer"""
597 if self.__m2CryptoX509 is None:
598 return None
599
600
601 return X500DN(m2CryptoX509Name=self.__m2CryptoX509.get_issuer())
602
603 issuer = property(fget=__getIssuer, doc="Certificate Issuer")
604
606 """Get Certificate subject"""
607 if self.__m2CryptoX509 is None:
608 return None
609
610
611 return X500DN(m2CryptoX509Name=self.__m2CryptoX509.get_subject())
612
613 subject = property(fget=__getSubject, doc="Certificate subject")
614
615 - def isValidTime(self,
616 raiseExcep=False,
617 expiryWarning=True,
618 nDaysBeforeExpiryLimit=30,
619 warningStackLevel=2):
620 """Check Certificate for expiry
621
622 @type raiseExcep: bool
623 @param raiseExcep: set True to raise an exception if certificate is
624 invalid
625
626 @type expiryWarning: bool
627 @param expiryWarning: set to True to output a warning message if the
628 certificate is due to expire in less than nDaysBeforeExpiryLimit days.
629 Message is sent using warnings.warn and through logging.warning. No
630 message is set if the certificate has an otherwise invalid time
631
632 @type nDaysBeforeExpiryLimit: int
633 @param nDaysBeforeExpiryLimit: used in conjunction with the
634 expiryWarning flag. Set the number of days in advance of certificate
635 expiry from which to start outputing warnings
636
637 @type warningStackLevel: int
638 @param warningStackLevel: set where in the stack to flag the warning
639 from. Level 2 will flag it at the level of the caller of this
640 method. Level 3 would flag at the level of the caller of the caller
641 and so on.
642
643 @raise X509CertInvalidNotBeforeTime: current time is before the
644 certificate's notBefore time
645 @raise X509CertExpired: current time is after the certificate's
646 notAfter time"""
647
648 if not isinstance(self.__dtNotBefore, datetime):
649 raise X509CertError("Not Before datetime is not set")
650
651 if not isinstance(self.__dtNotAfter, datetime):
652 raise X509CertError("Not After datetime is not set")
653
654 dtNow = datetime.utcnow()
655 isValidTime = dtNow > self.__dtNotBefore and dtNow < self.__dtNotAfter
656
657
658 if self.__filePath:
659 fileInfo = ' "%s"' % self.__filePath
660 else:
661 fileInfo = ''
662
663
664
665
666 if isValidTime and expiryWarning:
667 dtTime2Expiry = self.__dtNotAfter - dtNow
668 if dtTime2Expiry.days < nDaysBeforeExpiryLimit:
669 msg = ('Certificate%s with DN "%s" will expire in %d days on: '
670 '%s' % (fileInfo,
671 self.dn,
672 dtTime2Expiry.days,
673 self.__dtNotAfter))
674 warn(msg, stacklevel=warningStackLevel)
675 log.warning(msg)
676
677
678 if dtNow < self.__dtNotBefore:
679 msg = ("Current time %s is before the certificate's Not Before "
680 'Time %s for certificate%s with DN "%s"' %
681 (dtNow, self.__dtNotBefore, fileInfo, self.dn))
682 log.error(msg)
683 if raiseExcep:
684 raise X509CertInvalidNotBeforeTime(msg)
685
686 elif dtNow > self.__dtNotAfter:
687 msg = ('Certificate%s with DN "%s" has expired: the time now is '
688 '%s and the certificate expiry is %s.' %(fileInfo,
689 self.dn,
690 dtNow,
691 self.__dtNotAfter))
692 log.error(msg)
693 if raiseExcep:
694 raise X509CertExpired(msg)
695
696
697 return isValidTime
698
700 """Convert M2Crypto UTC time string as returned by get_not_before/
701 get_not_after methods into datetime type"""
702
703 datetimeRE = "([a-zA-Z]{3} {1,2}\d{1,2} \d{2}:\d{2}:\d{2} \d{4}).*"
704 sM2CryptoUTC = None
705
706 try:
707
708 sM2CryptoUTC = str(m2CryptoUTC)
709
710
711 sTime = re.findall(datetimeRE, sM2CryptoUTC)[0]
712
713
714 lTime = strptime(sTime, "%b %d %H:%M:%S %Y")[0:6]
715
716 return datetime(lTime[0], lTime[1], lTime[2],
717 lTime[3], lTime[4], lTime[5])
718
719 except Exception:
720 msg = "Error parsing M2Crypto UTC"
721 if sM2CryptoUTC is not None:
722 msg += ": " + sM2CryptoUTC
723
724 raise X509CertError(msg)
725
726 - def verify(self, pubKey, **kw):
727 """Verify a certificate against the public key of the
728 issuer
729
730 @param pubKey: public key of cert that issued self
731 @type pubKey: M2Crypto.RSA.RSA_pub
732 @param **kw: keywords to pass to M2Crypto.X509.X509 -
733 'pkey'
734 @type: dict
735 @return: True if verifies OK, False otherwise
736 @rtype: bool
737 """
738 return bool(self.__m2CryptoX509.verify(pubKey, **kw))
739
740 @classmethod
741 - def Read(cls, filePath, warningStackLevel=4, **isValidTimeKw):
742 """Create a new X509 certificate read in from a file"""
743 x509Cert = cls(filePath=filePath)
744 x509Cert.read(warningStackLevel=warningStackLevel, **isValidTimeKw)
745
746 return x509Cert
747
748 @classmethod
749 - def Parse(cls, x509CertTxt, warningStackLevel=4, **isValidTimeKw):
750 """Create a new X509 certificate from string of file content"""
751 x509Cert = cls()
752 x509Cert.parse(x509CertTxt,
753 warningStackLevel=warningStackLevel,
754 **isValidTimeKw)
755
756 return x509Cert
757
758 @classmethod
760 """Convenience method to instantiate a new object from an M2Crypto
761 X.509 certificate object"""
762 x509Cert = cls(m2CryptoX509=m2CryptoX509)
763 return x509Cert
764
767 """Error from X509Stack type"""
768
771 """Expecting non-zero length X509Stack"""
772
775 """Raise from verifyCertChain if no certificate can be found to verify the
776 input"""
777
780 """Raise from verifyCertChain if cert. is self-signed and
781 rejectSelfSignedCert=True"""
782
785 """X.509 Certificate has an invalid signature"""
786
788 """Wrapper for M2Crypto X509_Stack"""
789
791 """Initialise from an M2Crypto stack object
792
793 @param m2X509Stack: M2Crypto X.509 stack object
794 @type m2X509Stack: M2Crypto.X509.X509_Stack"""
795
796 self.__m2X509Stack = m2X509Stack or M2Crypto.X509.X509_Stack()
797
799 """@return: length of stack
800 @rtype: int"""
801 return self.__m2X509Stack.__len__()
802
804 """Index stack as an array
805 @param idx: stack index
806 @type idx: int
807 @return: X.509 cert object
808 @rtype: ndg.security.common.X509.X509Cert"""
809
810 return X509Cert(m2CryptoX509=self.__m2X509Stack.__getitem__(idx))
811
813 """@return: stack iterator
814 @rtype: listiterator"""
815 return iter([X509Cert(m2CryptoX509=i) for i in self.__m2X509Stack])
816
817 - def push(self, x509Cert):
818 """Push an X509 certificate onto the stack.
819
820 @param x509Cert: X509 object.
821 @type x509Cert: M2Crypto.X509.X509,
822 ndg.security.common.X509.X509Cert or basestring
823 @return: The number of X509 objects currently on the stack.
824 @rtype: int"""
825 if isinstance(x509Cert, M2Crypto.X509.X509):
826 return self.__m2X509Stack.push(x509Cert)
827
828 elif isinstance(x509Cert, X509Cert):
829 return self.__m2X509Stack.push(x509Cert.m2CryptoX509)
830
831 elif isinstance(x509Cert, basestring):
832 return self.__m2X509Stack.push(
833 X509Cert.Parse(x509Cert).m2CryptoX509)
834 else:
835 raise X509StackError("Expecting M2Crypto.X509.X509, ndg.security."
836 "common.X509.X509Cert or string type")
837
839 """Pop a certificate from the stack.
840
841 @return: X509 object that was popped, or None if there is nothing
842 to pop.
843 @rtype: ndg.security.common.X509.X509Cert
844 """
845 return X509Cert(m2CryptoX509=self.__m2X509Stack.pop())
846
848 """Return the stack as a DER encoded string
849 @return: DER string
850 @rtype: string"""
851 return self.__m2X509Stack.as_der()
852
853 - def verifyCertChain(self,
854 x509Cert2Verify=None,
855 caX509Stack=None,
856 rejectSelfSignedCert=True):
857 """Treat stack as a list of certificates in a chain of
858 trust. Validate the signatures through to a single root issuer.
859
860 @param x509Cert2Verify: X.509 certificate to be verified default is
861 last in the stack
862 @type x509Cert2Verify: X509Cert
863
864 @param caX509Stack: X.509 stack containing CA certificates that are
865 trusted.
866 @type caX509Stack: X509Stack
867
868 @param rejectSelfSignedCert: Set to True (default) to raise an
869 SelfSignedCert exception if a certificate in self's stack is
870 self-signed.
871 @type rejectSelfSignedCert: bool"""
872
873 if caX509Stack is None:
874 caX509Stack = []
875
876 n2Validate = len(self)
877 if x509Cert2Verify:
878
879 n2Validate += 1
880 else:
881
882
883 if n2Validate == 0:
884 raise X509StackEmptyError("Empty stack and no x509Cert2Verify "
885 "set: no cert.s to verify")
886
887 x509Cert2Verify = self[-1]
888
889
890
891 nValidated = 0
892 issuerX509Cert = None
893 while nValidated < n2Validate:
894 issuerX509Cert = None
895 issuerDN = x509Cert2Verify.issuer
896
897
898 for x509Cert in self:
899 if x509Cert.dn == issuerDN:
900
901
902 issuerX509Cert = x509Cert
903 break
904
905 if issuerX509Cert:
906
907
908 if not x509Cert2Verify.verify(issuerX509Cert.pubKey):
909 X509CertInvalidSignature('Signature is invalid for cert. '
910 '"%s"' % x509Cert2Verify.dn)
911
912
913
914
915 x509Cert2Verify = issuerX509Cert
916 nValidated += 1
917 else:
918
919 break
920
921
922 if issuerX509Cert:
923
924 if (nValidated == 1 and rejectSelfSignedCert and
925 issuerX509Cert.dn == issuerX509Cert.issuer):
926
927
928
929 raise SelfSignedCert("Certificate is self signed: [DN=%s]" %
930 issuerX509Cert.dn)
931
932 if not caX509Stack:
933 caX509Stack = [issuerX509Cert]
934
935 elif not caX509Stack:
936 raise X509CertIssuerNotFound('No issuer certificate found for '
937 'certificate "%s"' %
938 x509Cert2Verify.dn)
939
940 for caCert in caX509Stack:
941 issuerDN = x509Cert2Verify.issuer
942 if caCert.dn == issuerDN:
943 issuerX509Cert = caCert
944 break
945
946 if issuerX509Cert:
947 if not x509Cert2Verify.verify(issuerX509Cert.pubKey):
948 X509CertInvalidSignature('Signature is invalid for cert. "%s"' %
949 x509Cert2Verify.dn)
950
951
952 return
953 else:
954 raise X509CertIssuerNotFound('No issuer cert. found for '
955 'certificate "%s"'%x509Cert2Verify.dn)
956
957
958
959 raise X509CertIssuerNotFound('Can\'t find issuer cert "%s" for '
960 'certificate "%s"' %
961 (x509Cert2Verify.issuer,
962 x509Cert2Verify.dn))
963
966 """Raise if verification against CA cert public key fails"""
967
970 """Raise if verification against a list acceptable DNs fails"""
971
972
973 -class HostCheck(SSL.Checker.Checker, object):
974 """Override SSL.Checker.Checker to enable alternate Common Name
975 setting match for peer cert"""
976
977 - def __init__(self,
978 peerCertDN=None,
979 peerCertCN=None,
980 acceptedDNs=None,
981 caCertList=None,
982 caCertFilePathList=None,
983 **kw):
984 """Override parent class __init__ to enable setting of myProxyServerDN
985 setting
986
987 @type peerCertDN: string/list
988 @param peerCertDN: Set the expected Distinguished Name of the
989 server to avoid errors matching hostnames. This is useful
990 where the hostname is not fully qualified.
991
992 *param acceptedDNs: a list of acceptable DNs. This enables validation
993 where the expected DN is where against a limited list of certs.
994
995 @type peerCertCN: string
996 @param peerCertCN: enable alternate Common Name to peer
997 hostname
998
999 @type caCertList: list type of M2Crypto.X509.X509 types
1000 @param caCertList: CA X.509 certificates - if set the peer cert's
1001 CA signature is verified against one of these. At least one must
1002 verify
1003
1004 @type caCertFilePathList: list string types
1005 @param caCertFilePathList: same as caCertList except input as list
1006 of CA cert file paths"""
1007
1008 if acceptedDNs is None:
1009 acceptedDNs = []
1010
1011 if caCertList is None:
1012 caCertList = []
1013
1014 if caCertFilePathList is None:
1015 caCertFilePathList = []
1016
1017 SSL.Checker.Checker.__init__(self, **kw)
1018
1019 self.peerCertDN = peerCertDN
1020 self.peerCertCN = peerCertCN
1021 self.acceptedDNs = acceptedDNs
1022
1023 if caCertList:
1024 self.caCertList = caCertList
1025 elif caCertFilePathList:
1026 self.caCertFilePathList = caCertFilePathList
1027 else:
1028
1029 self.__caCertStack = ()
1030
1031 - def __call__(self, peerCert, host=None):
1032 """Carry out checks on server ID
1033 @param peerCert: MyProxy server host certificate as M2Crypto.X509.X509
1034 instance
1035 @param host: name of host to check
1036 """
1037 if peerCert is None:
1038 raise SSL.Checker.NoCertificate('SSL Peer did not return '
1039 'certificate')
1040
1041 peerCertDN = '/'+peerCert.get_subject().as_text().replace(', ', '/')
1042 try:
1043 SSL.Checker.Checker.__call__(self, peerCert, host=self.peerCertCN)
1044
1045 except SSL.Checker.WrongHost, e:
1046
1047 if peerCertDN != self.peerCertDN:
1048 raise e
1049
1050
1051
1052 peerCertX500DN = X500DN(dn=peerCertDN)
1053
1054 if self.acceptedDNs:
1055 matchFound = False
1056 for dn in self.acceptedDNs:
1057 x500dn = X500DN(dn=dn)
1058 if x500dn == peerCertX500DN:
1059 matchFound = True
1060 break
1061
1062 if not matchFound:
1063 raise InvalidCertDN('Peer cert DN "%s" doesn\'t match '
1064 'verification list' % peerCertDN)
1065
1066 if len(self.__caCertStack) > 0:
1067 try:
1068 self.__caCertStack.verifyCertChain(
1069 x509Cert2Verify=X509Cert(m2CryptoX509=peerCert))
1070 except Exception, e:
1071 raise InvalidCertSignature("Peer certificate verification "
1072 "against CA certificate failed: %s"
1073 % e)
1074
1075
1076 return True
1077
1079 """Set list of CA certs - peer cert must validate against at least one
1080 of these"""
1081 self.__caCertStack = X509Stack()
1082 for caCert in caCertList:
1083 self.__caCertStack.push(caCert)
1084
1085 caCertList = property(fset=__setCACertList,
1086 doc="list of CA certificates - the peer certificate "
1087 "must validate against one")
1088
1090 '''Read CA certificates from file and add them to the X.509
1091 stack
1092
1093 @type caCertFilePathList: basestring, list or tuple
1094 @param caCertFilePathList: list of file paths for CA certificates to
1095 be used to verify certificate used to sign message. If a single
1096 string item is input then this is converted into a tuple
1097 '''
1098 if isinstance(caCertFilePathList, basestring):
1099 caCertFilePathList = (caCertFilePathList,)
1100
1101 elif not isinstance(caCertFilePathList, (list, tuple)):
1102 raise TypeError('Expecting a basestring, list or tuple type for '
1103 '"caCertFilePathList"')
1104
1105 self.__caCertStack = X509Stack()
1106
1107 for caCertFilePath in caCertFilePathList:
1108 self.__caCertStack.push(X509.load_cert(caCertFilePath))
1109
1110 caCertFilePathList = property(fset=__setCACertsFromFileList,
1111 doc="list of CA certificate file paths - "
1112 "peer certificate must validate against "
1113 "one")
1114
1117 """Modified version of M2Crypto equivalent to enable custom checks with
1118 the peer and timeout settings
1119
1120 @type defReadTimeout: M2Crypto.SSL.timeout
1121 @cvar defReadTimeout: default timeout for read operations
1122 @type defWriteTimeout: M2Crypto.SSL.timeout
1123 @cvar defWriteTimeout: default timeout for write operations"""
1124 defReadTimeout = SSL.timeout(sec=20.)
1125 defWriteTimeout = SSL.timeout(sec=20.)
1126
1128 '''Overload to enable setting of post connection check
1129 callback to SSL.Connection
1130
1131 type *args: tuple
1132 param *args: args which apply to M2Crypto.httpslib.HTTPSConnection
1133 type **kw: dict
1134 param **kw: additional keywords
1135 @type postConnectionCheck: SSL.Checker.Checker derivative
1136 @keyword postConnectionCheck: set class for checking peer
1137 @type readTimeout: M2Crypto.SSL.timeout
1138 @keyword readTimeout: readTimeout - set timeout for read
1139 @type writeTimeout: M2Crypto.SSL.timeout
1140 @keyword writeTimeout: similar to read timeout'''
1141
1142 self._postConnectionCheck = kw.pop('postConnectionCheck',
1143 SSL.Checker.Checker)
1144
1145 if 'readTimeout' in kw:
1146 if not isinstance(kw['readTimeout'], SSL.timeout):
1147 raise AttributeError("readTimeout must be of type "
1148 "M2Crypto.SSL.timeout")
1149 self.readTimeout = kw.pop('readTimeout')
1150 else:
1151 self.readTimeout = HTTPSConnection.defReadTimeout
1152
1153 if 'writeTimeout' in kw:
1154 if not isinstance(kw['writeTimeout'], SSL.timeout):
1155 raise AttributeError("writeTimeout must be of type "
1156 "M2Crypto.SSL.timeout")
1157 self.writeTimeout = kw.pop('writeTimeout')
1158 else:
1159 self.writeTimeout = HTTPSConnection.defWriteTimeout
1160
1161 self._clntCertFilePath = kw.pop('clntCertFilePath', None)
1162 self._clntPriKeyFilePath = kw.pop('clntPriKeyFilePath', None)
1163
1164 _HTTPSConnection.__init__(self, *args, **kw)
1165
1166
1167 if (self._clntCertFilePath is not None and
1168 self._clntPriKeyFilePath is not None):
1169 self.ssl_ctx.load_cert(self._clntCertFilePath,
1170 self._clntPriKeyFilePath)
1171
1172
1174 '''Overload M2Crypto.httpslib.HTTPSConnection to enable
1175 custom post connection check of peer certificate and socket timeout'''
1176
1177 self.sock = SSL.Connection(self.ssl_ctx)
1178 self.sock.set_post_connection_check_callback(self._postConnectionCheck)
1179
1180 self.sock.set_socket_read_timeout(self.readTimeout)
1181 self.sock.set_socket_write_timeout(self.writeTimeout)
1182
1183 self.sock.connect((self.host, self.port))
1184
1186 '''Overload to work around bug with unicode type URL'''
1187 url = str(url)
1188 _HTTPSConnection.putrequest(self, method, url, **kw)
1189
1190
1191 -class SSLContextProxy(object):
1192 """Holder for M2Crypto.SSL.Context parameters"""
1193 PRE_VERIFY_FAIL, PRE_VERIFY_OK = range(2)
1194
1195 SSL_CERT_FILEPATH_OPTNAME = "sslCertFilePath"
1196 SSL_PRIKEY_FILEPATH_OPTNAME = "sslPriKeyFilePath"
1197 SSL_PRIKEY_PWD_OPTNAME = "sslPriKeyPwd"
1198 SSL_CACERT_FILEPATH_OPTNAME = "sslCACertFilePath"
1199 SSL_CACERT_DIRPATH_OPTNAME = "sslCACertDir"
1200 SSL_VALID_DNS_OPTNAME = "sslValidDNs"
1201
1202 OPTNAMES = (
1203 SSL_CERT_FILEPATH_OPTNAME,
1204 SSL_PRIKEY_FILEPATH_OPTNAME,
1205 SSL_PRIKEY_PWD_OPTNAME,
1206 SSL_CACERT_FILEPATH_OPTNAME,
1207 SSL_CACERT_DIRPATH_OPTNAME,
1208 SSL_VALID_DNS_OPTNAME
1209 )
1210
1211 __slots__ = tuple(["__%s" % name for name in OPTNAMES])
1212 del name
1213
1214 VALID_DNS_PAT = re.compile(',\s*')
1215
1216 - def __init__(self):
1217 self.__sslCertFilePath = None
1218 self.__sslPriKeyFilePath = None
1219 self.__sslPriKeyPwd = None
1220 self.__sslCACertFilePath = None
1221 self.__sslCACertDir = None
1222 self.__sslValidDNs = []
1223
1224 - def createCtx(self, depth=9, **kw):
1225 """Create an M2Crypto SSL Context from this objects properties
1226 @type depth: int
1227 @param depth: max. depth of certificate to verify against
1228 @type kw: dict
1229 @param kw: M2Crypto.SSL.Context keyword arguments
1230 @rtype: M2Crypto.SSL.Context
1231 @return M2Crypto SSL context object
1232 """
1233 ctx = SSL.Context(**kw)
1234
1235
1236 if self.sslCertFilePath and self.sslPriKeyFilePath:
1237
1238 ctx.load_cert_chain(self.sslCertFilePath,
1239 self.__sslPriKeyFilePath,
1240 lambda *arg, **kw: self.sslPriKeyPwd)
1241 log.debug("Set client certificate and key in SSL Context")
1242 else:
1243 log.debug("No client certificate or key set in SSL Context")
1244
1245 if self.sslCACertFilePath or self.sslCACertDir:
1246
1247 ctx.load_verify_locations(self.sslCACertFilePath,
1248 self.sslCACertDir)
1249 mode = SSL.verify_peer
1250 else:
1251 mode = SSL.verify_none
1252 log.warning('No CA certificate files set: mode set to '
1253 '"verify_none"! No verification of the server '
1254 'certificate will be enforced')
1255
1256 if len(self.sslValidDNs) > 0:
1257
1258
1259 mode = SSL.verify_peer
1260 callback = self.createVerifySSLPeerCertCallback()
1261 log.debug('Set peer certificate Distinguished Name check set in '
1262 'SSL Context')
1263 else:
1264 callback = None
1265 log.warning('No peer certificate Distinguished Name check set in '
1266 'SSL Context')
1267
1268 ctx.set_verify(mode, 9, callback=callback)
1269
1270 return ctx
1271
1272 - def copy(self, sslCtxProxy):
1273 """Copy settings from another context object
1274 """
1275 if not isinstance(sslCtxProxy, SSLContextProxy):
1276 raise TypeError('Expecting %r for copy method input object; '
1277 'got %r' % (SSLContextProxy, type(sslCtxProxy)))
1278
1279 for name in SSLContextProxy.OPTNAMES:
1280 setattr(self, name, getattr(sslCtxProxy, name))
1281
1283 """Create a callback function to enable the DN of the peer in an SSL
1284 connection to be verified against a whitelist.
1285
1286 Nb. Making this function within the scope of a method of the class to
1287 enables to access instance variables
1288 """
1289
1290 def _verifySSLPeerCertCallback(preVerifyOK, x509StoreCtx):
1291 '''SSL verify callback function used to control the behaviour when
1292 the SSL_VERIFY_PEER flag is set. See:
1293
1294 http://www.openssl.org/docs/ssl/SSL_CTX_set_verify.html
1295
1296 This implementation applies verification in order to check the DN
1297 of the peer certificate against a whitelist
1298
1299 @type preVerifyOK: int
1300 @param preVerifyOK: If a verification error is found, this
1301 parameter will be set to 0
1302 @type x509StoreCtx: M2Crypto.X509.X509_Store_Context
1303 @param x509StoreCtx: locate the certificate to be verified and
1304 perform additional verification steps as needed
1305 @rtype: int
1306 @return: controls the strategy of the further verification process.
1307 - If verify_callback returns 0, the verification process is
1308 immediately stopped with "verification failed" state. If
1309 SSL_VERIFY_PEER is set, a verification failure alert is sent to the
1310 peer and the TLS/SSL handshake is terminated.
1311 - If verify_callback returns 1, the verification process is
1312 continued.
1313 If verify_callback always returns 1, the TLS/SSL handshake will not
1314 be terminated with respect to verification failures and the
1315 connection
1316 will be established. The calling process can however retrieve the
1317 error code of the last verification error using
1318 SSL_get_verify_result or by maintaining its own error storage
1319 managed by verify_callback.
1320 '''
1321 if preVerifyOK == 0:
1322
1323
1324 log.error("verifyCallback: pre-verify OK flagged an error "
1325 "with the peer certificate, returning error state "
1326 "to caller ...")
1327 return preVerifyOK
1328
1329 x509CertChain = x509StoreCtx.get1_chain()
1330 for cert in x509CertChain:
1331 x509Cert = X509Cert.fromM2Crypto(cert)
1332 if x509Cert.dn in self.sslValidDNs:
1333 return preVerifyOK
1334
1335 subject = cert.get_subject()
1336 dn = subject.as_text()
1337 log.debug("verifyCallback: dn = %r", dn)
1338
1339
1340 log.debug("No match for peer certificate %s in DN whitelist %r",
1341 x509Cert.dn, self.sslValidDNs)
1342 return SSLContextProxy.PRE_VERIFY_FAIL
1343
1344 return _verifySSLPeerCertCallback
1345
1347 return self.__sslCertFilePath
1348
1349 - def _setSSLCertFilePath(self, filePath):
1350 "Set X.509 cert/cert chian file path property method"
1351
1352 if isinstance(filePath, basestring):
1353 filePath = os.path.expandvars(filePath)
1354
1355 elif filePath is not None:
1356 raise TypeError("X.509 cert. file path must be a valid string")
1357
1358 self.__sslCertFilePath = filePath
1359
1360 sslCertFilePath = property(fset=_setSSLCertFilePath,
1361 fget=_getSSLCertFilePath,
1362 doc="File path to X.509 cert. / cert. chain")
1363
1365 """Get file path for list of CA cert or certs used to validate SSL
1366 connections
1367
1368 @rtype sslCACertFilePath: basestring
1369 @return sslCACertFilePathList: file path to file containing concatenated
1370 PEM encoded CA certificates."""
1371 return self.__sslCACertFilePath
1372
1373 - def _setSSLCACertFilePath(self, value):
1374 """Set CA cert file path
1375
1376 @type sslCACertFilePath: basestring, list, tuple or None
1377 @param sslCACertFilePath: file path to CA certificate file. If None
1378 then the input is quietly ignored."""
1379 if isinstance(value, basestring):
1380 self.__sslCACertFilePath = os.path.expandvars(value)
1381
1382 elif value is None:
1383 self.__sslCACertFilePath = value
1384
1385 else:
1386 raise TypeError("Input CA Certificate file path must be "
1387 "a valid string or None type: %r" % type(value))
1388
1389
1390 sslCACertFilePath = property(fget=_getSSLCACertFilePath,
1391 fset=_setSSLCACertFilePath,
1392 doc="Path to file containing concatenated PEM "
1393 "encoded CA Certificates - used for "
1394 "verification of peer certs in SSL "
1395 "connection")
1396
1397 - def _getSSLCACertDir(self):
1398 """Get file path for list of CA cert or certs used to validate SSL
1399 connections
1400
1401 @rtype sslCACertDir: basestring
1402 @return sslCACertDirList: directory containing PEM encoded CA
1403 certificates."""
1404 return self.__sslCACertDir
1405
1406 - def _setSSLCACertDir(self, value):
1407 """Set CA cert or certs to validate AC signatures, signatures
1408 of Attribute Authority SOAP responses and SSL connections where
1409 AA SOAP service is run over SSL.
1410
1411 @type sslCACertDir: basestring
1412 @param sslCACertDir: directory containing CA certificate files.
1413 """
1414 if isinstance(value, basestring):
1415 self.__sslCACertDir = os.path.expandvars(value)
1416 elif value is None:
1417 self.__sslCACertDir = value
1418 else:
1419 raise TypeError("Input CA Certificate directroy must be "
1420 "a valid string or None type: %r" % type(value))
1421
1422 sslCACertDir = property(fget=_getSSLCACertDir,
1423 fset=_setSSLCACertDir,
1424 doc="Path to directory containing PEM encoded CA "
1425 "Certificates used for verification of peer "
1426 "certs in SSL connection. Files in the "
1427 "directory must be named with the form "
1428 "<hash>.0 where <hash> can be obtained using "
1429 "openssl x509 -in cert -hash -noout or using "
1430 "the c_rehash OpenSSL script")
1431
1432 - def _getSslValidDNs(self):
1433 return self.__sslValidDNs
1434
1435 - def _setSslValidDNs(self, value):
1436 if isinstance(value, basestring):
1437 pat = SSLContextProxy.VALID_DNS_PAT
1438 self.__sslValidDNs = [X500DN.fromString(dn)
1439 for dn in pat.split(value)]
1440
1441 elif isinstance(value, (tuple, list)):
1442 self.__sslValidDNs = [X500DN.fromString(dn) for dn in value]
1443 else:
1444 raise TypeError('Expecting list/tuple or basestring type for "%s" '
1445 'attribute; got %r' %
1446 (SSLContextProxy.SSL_VALID_DNS_OPTNAME,
1447 type(value)))
1448
1449 sslValidDNs = property(_getSslValidDNs,
1450 _setSslValidDNs,
1451 doc="whitelist of acceptable certificate "
1452 "Distinguished Names for peer certificates in "
1453 "SSL requests")
1454
1457
1458 - def _setSSLPriKeyFilePath(self, filePath):
1459 "Set ssl private key file path property method"
1460
1461 if isinstance(filePath, basestring):
1462 filePath = os.path.expandvars(filePath)
1463
1464 elif filePath is not None:
1465 raise TypeError("Private key file path must be a valid "
1466 "string or None type")
1467
1468 self.__sslPriKeyFilePath = filePath
1469
1470 sslPriKeyFilePath = property(fget=_getSSLPriKeyFilePath,
1471 fset=_setSSLPriKeyFilePath,
1472 doc="File path to SSL private key")
1473
1474 - def _setSSLPriKeyPwd(self, sslPriKeyPwd):
1475 "Set method for ssl private key file password"
1476 if not isinstance(sslPriKeyPwd, (type(None), basestring)):
1477 raise TypeError("Signing private key password must be None "
1478 "or a valid string")
1479
1480
1481
1482 self.__sslPriKeyPwd = str(sslPriKeyPwd)
1483
1484 - def _getSSLPriKeyPwd(self):
1485 "Get property method for SSL private key"
1486 return self.__sslPriKeyPwd
1487
1488 sslPriKeyPwd = property(fset=_setSSLPriKeyPwd,
1489 fget=_getSSLPriKeyPwd,
1490 doc="Password protecting SSL private key file")
1491
1492 - def __getstate__(self):
1493 '''Enable pickling for use with beaker.session'''
1494 _dict = {}
1495 for attrName in SSLContextProxy.__slots__:
1496
1497
1498 if attrName.startswith('__'):
1499 attrName = "_SSLContextProxy" + attrName
1500
1501 _dict[attrName] = getattr(self, attrName)
1502
1503 return _dict
1504
1505 - def __setstate__(self, attrDict):
1506 '''Enable pickling for use with beaker.session'''
1507 for attr, val in attrDict.items():
1508 setattr(self, attr, val)
1509