1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """
18 Contains classes for working with x.509 certificates.
19 The backing implementation is M2Crypto.X509 which has insufficient
20 support for custom v3 extensions. It is not intended to be a
21 replacement of full wrapper but instead and extension.
22 """
23
24 import os
25 import re
26 from datetime import datetime as dt
27 from datetime import tzinfo, timedelta
28
29 from M2Crypto import X509
33 """
34 Represents and x.509 certificate.
35 @ivar x509: The M2Crypto.X509 backing object.
36 @type x509: L{X509}
37 @ivar __ext: A dictionary of extensions L{OID}:value
38 @type __ext: L{Extensions}
39 """
40
42 """
43 @param content: The (optional) PEM encoded content.
44 @type content: str
45 """
46 self.update(content)
47
49 if content:
50 x509 = X509.load_cert_string(content)
51 else:
52 x509 = X509.X509()
53 self.__ext = Extensions(x509)
54 self.x509 = x509
55
57 """
58 Get the serial number
59 @return: The x.509 serial number
60 @rtype: str
61 """
62 return self.x509.get_serial_number()
63
65 """
66 Get the certificate subject.
67 note: Missing NID mapping for UID added to patch openssl.
68 @return: A dictionary of subject fields.
69 @rtype: dict
70 """
71 d = {}
72 subject = self.x509.get_subject()
73 subject.nid['UID'] = 458
74 for key, nid in subject.nid.items():
75 entry = subject.get_entries_by_nid(nid)
76 if len(entry):
77 asn1 = entry[0].get_data()
78 d[key] = str(asn1)
79 continue
80 return d
81
83 """
84 Get the I{valid} date range.
85 @return: The valid date range.
86 @rtype: L{DateRange}
87 """
88 return DateRange(self.x509)
89
91 """
92 Get whether the certificate is valid based on date.
93 @return: True if valid.
94 @rtype: boolean
95 """
96 return self.validRange().hasNow()
97
99 """
100 Get whether the certificate contains bogus
101 data or is otherwise unsuitable. The certificate
102 may be valid but still be considered bogus.
103 @return: List of reasons if bogus
104 @rtype: list
105 """
106 return []
107
109 """
110 Get custom extensions.
111 @return: An extensions object.
112 @rtype: L{Extensions}
113 """
114 return self.__ext
115
116 - def read(self, path):
117 """
118 Read a certificate file.
119 @param path: The path to a .pem file.
120 @type path: str
121 @return: A certificate
122 @rtype: L{Certificate}
123 """
124 f = open(path)
125 content = f.read()
126 try:
127 self.update(content)
128 finally:
129 f.close()
130 self.path = path
131
133 """
134 Write the certificate.
135 @param path: The path to the .pem file.
136 @type path: str
137 @return: self
138 """
139 f = open(path, 'w')
140 f.write(self.toPEM())
141 self.path = path
142 f.close()
143 return self
144
146 """
147 Delete the file associated with this certificate.
148 """
149 if hasattr(self, 'path'):
150 os.unlink(self.path)
151 else:
152 raise Exception, 'no path, not deleted'
153
155 """
156 Get PEM representation of the certificate.
157 @return: A PEM string
158 @rtype: str
159 """
160 return self.x509.as_pem()
161
163 return self.x509.as_text()
164
166 sn = self.serialNumber()
167 path = None
168 if hasattr(self, 'path'):
169 path = self.path
170 return '[sn: %d, path: "%s"]' % (sn, path)
171
173 range = self.validRange()
174 exp1 = range.end()
175 range = other.validRange()
176 exp2 = range.end()
177 if exp1 < exp2:
178 return -1
179 if exp1 > exp2:
180 return 1
181 return 0
182
183
184 -class Key(object):
185 """
186 The (private|public) key.
187 @ivar content: The PEM encoded key.
188 @type content: str
189 """
190
191 @classmethod
192 - def read(cls, path):
193 """
194 Read the key.
195 @param path: The path to the .pem file.
196 @type path: str
197 """
198 f = open(path)
199 content = f.read()
200 f.close()
201 key = Key(content)
202 key.path = path
203 return key
204
206 """
207 @param content: The PEM encoded key.
208 @type content: str
209 """
210 self.content = content
211
213 """
214 Write the key.
215 @param path: The path to the .pem file.
216 @type path: str
217 @return: self
218 """
219 f = open(path, 'w')
220 f.write(self.content)
221 self.path = path
222 f.close()
223 return self
224
226 """
227 Delete the file associated with this key.
228 """
229 if hasattr(self, 'path'):
230 os.unlink(self.path)
231 else:
232 raise Exception, 'no path, not deleted'
233
236
239 """
240 Date range object converts between ASN1 and python
241 datetime objects.
242 @ivar x509: A certificate.
243 @type x509: X509
244 """
245
246 ASN1_FORMAT = '%b %d %H:%M:%S %Y %Z'
247
249 """
250 @param x509: A certificate.
251 @type x509: X509
252 """
253 self.range = \
254 (str(x509.get_not_before()),
255 str(x509.get_not_after()))
256
258 """
259 Get range beginning.
260 @return: The beginning date in UTC.
261 @rtype: L{datetime.datetime}
262 """
263 return self.__parse(self.range[0])
264
266 """
267 Get range end.
268 @return: The end date in UTC.
269 @rtype: L{datetime.datetime}
270 """
271 return self.__parse(self.range[1])
272
274 """
275 Get whether the certificate is valid based on date.
276 @return: True if valid.
277 @rtype: boolean
278 """
279 gmt = dt.utcnow()
280 gmt = gmt.replace(tzinfo=GMT())
281 return ( gmt >= self.begin() and gmt <= self.end() )
282
284 try:
285 d = dt.strptime(asn1, self.ASN1_FORMAT)
286 except:
287 d = dt(year=2000, month=1, day=1)
288 return d.replace(tzinfo=GMT())
289
291 return '\n\t%s\n\t%s' % self.range
292
293
294 -class GMT(tzinfo):
295 """GMT"""
296
298 return timedelta(seconds=0)
299
302
305
308 """
309 Represents x.509 (v3) I{custom} extensions.
310 """
311
312 pattern = re.compile('([0-9]+\.)+[0-9]+:')
313
315 """
316 @param x509: A certificate object.
317 @type x509: L{X509}
318 """
319 if isinstance(x509, dict):
320 self.update(x509)
321 else:
322 self.__parse(x509)
323
325 """
326 Left trim I{n} parts.
327 @param n: The number of parts to trim.
328 @type n: int
329 @return: The trimmed OID
330 @rtype: L{Extensions}
331 """
332 d = {}
333 for oid,v in self.items():
334 d[oid.ltrim(n)] = v
335 return Extensions(d)
336
337 - def get(self, oid, default=None):
338 """
339 Get the value of an extension by I{oid}.
340 Note: The I{oid} may contain (*) wildcards.
341 @param oid: An OID that may contain (*) wildcards.
342 @type oid: str|L{OID}
343 @return: The value of the first extension matched.
344 @rtype: str
345 """
346 ext = self.find(oid, 1)
347 if ext:
348 return ext[0][1]
349 else:
350 return default
351
352 - def find(self, oid, limit=0):
353 """
354 Find all extensions matching the I{oid}.
355 Note: The I{oid} may contain (*) wildcards.
356 @param oid: An OID that may contain (*) wildcards.
357 @type oid: str|L{OID}
358 @param limit: Limit the number returned, 0=unlimited
359 @type limit: int
360 @return: A list of matching items.
361 @rtype: (OID, value)
362 @see: OID.match()
363 """
364 ext = []
365 if isinstance(oid, str):
366 oid = OID(oid)
367 keyset = sorted(self.keys())
368 for k in keyset:
369 v = self[k]
370 if k.match(oid):
371 ext.append((k, v))
372 if limit and len(ext) == limit:
373 break
374 return ext
375
377 """
378 Find a subtree by matching the oid.
379 @param root: An OID that may contain (*) wildcards.
380 @type root: str|L{OID}
381 @return: A subtree.
382 @rtype: L{Extensions}
383 """
384 d = {}
385 if isinstance(root, str):
386 root = OID(root)
387 if root[-1]:
388 root = root.append('')
389 ln = len(root)-1
390 for oid,v in self.find(root):
391 trimmed = oid.ltrim(ln)
392 d[trimmed] = v
393 return Extensions(d)
394
396
397 text = x509.as_text()
398 start = text.find('extensions:')
399 end = text.rfind('Signature Algorithm:')
400 text = text[start:end]
401 text = text.replace('.\n', '..')
402 return [s.strip() for s in text.split('\n')]
403
405
406 oid = None
407 for entry in self.__ext(x509):
408 if oid is not None:
409 self[oid] = entry[2:]
410 oid = None
411 continue
412 m = self.pattern.match(entry)
413 if m is None:
414 continue
415 oid = OID(entry[:-1])
416
418 s = []
419 for item in self.items():
420 s.append('%s = "%s"' % item)
421 return '\n'.join(s)
422
423
424 -class OID(object):
425 """
426 The Object Identifier object.
427 @ivar part: The oid parts.
428 @type part: [str,]
429 @cvar WILDCARD: The wildcard character.
430 @type WILDCARD: str
431 """
432
433 WILDCARD = '*'
434
435 @classmethod
436 - def join(cls, *oid):
438
439 @classmethod
441 """
442 Split an OID string.
443 @param s: An OID string Eg: (1.2.3)
444 @type s: str
445 @return: A list of OID parts.
446 @rtype: [str,]
447 """
448 return s.split('.')
449
451 """
452 @param oid: The OID value.
453 @type oid: str|[str,]
454 """
455 if isinstance(oid, str):
456 self.part = self.split(oid)
457 else:
458 self.part = oid
459
461 """
462 Get the parent OID.
463 @return: The parent OID.
464 @rtype: L{OID}
465 """
466 p = self.part[:-1]
467 if p:
468 return OID(p)
469
471 """
472 Left trim I{n} parts.
473 @param n: The number of parts to trim.
474 @type n: int
475 @return: The trimmed OID
476 @rtype: L{OID}
477 """
478 return OID(self.part[n:])
479
481 """
482 Right trim I{n} parts.
483 @param n: The number of parts to trim.
484 @type n: int
485 @return: The trimmed OID
486 @rtype: L{OID}
487 """
488 return OID(self.part[:-n])
489
491 """
492 Append the specified OID fragment.
493 @param oid: An OID fragment.
494 @type oid: str|L{OID}
495 @return: The concatenated OID.
496 @rtype: L{OID}
497 """
498 if isinstance(oid, str):
499 oid = OID(oid)
500 part = self.part + oid.part
501 return OID(part)
502
504 """
505 Match the specified OID considering wildcards.
506 Patterns:
507 - 1.4.5.6.74 (not wildcarded)
508 - .5.6.74 (match on only last 4)
509 - 5.6.74. (match only first 4)
510 - 1.4.*.6.* (wildcard pattern)
511 @param oid: An OID string or object.
512 @type oid: str|L{OID}
513 @return: True if matched
514 """
515 i = 0
516 if isinstance(oid, str):
517 oid = OID(oid)
518 try:
519 if not oid[0]:
520 oid = OID(oid[1:])
521 parts = self.part[-len(oid):]
522 elif not oid[-1]:
523 oid = OID(oid[:-1])
524 parts = self.part[:len(oid)]
525 else:
526 parts = self.part
527 if len(parts) != len(oid):
528 raise Exception()
529 for x in parts:
530 if ( x == oid[i] or oid[i] == self.WILDCARD ):
531 i += 1
532 else:
533 raise Exception()
534 except:
535 return False
536 return True
537
539 return len(self.part)
540
542 return self.part[index]
543
546
548 return hash(str(self))
549
551 return ( str(self) == str(other) )
552
554 return '.'.join(self.part)
555
558 """
559 Represents a Red Hat certificate.
560 @cvar REDHAT: The Red Hat base OID.
561 @type REDHAT: str
562 """
563
564 REDHAT = '1.3.6.1.4.1.2312.9'
565
571
573 """
574 Get the extension subtree for the B{redhat} namespace.
575 @return: The extensions with the RH namespace trimmed.
576 @rtype: L{Extension}
577 """
578 try:
579 return self.__redhat
580 except:
581 return self.extensions()
582
591
594 """
595 Represents a Red Hat product/entitlement certificate.
596 It is OID schema aware and provides methods to
597 get product information.
598 """
599
601 """
602 Get the product defined in the certificate.
603 @return: A product object.
604 @rtype: L{Product}
605 """
606 rhns = self.redhat()
607 products = rhns.find('1.*.1', 1)
608 if products:
609 p = products[0]
610 oid = p[0]
611 root = oid.rtrim(1)
612 hash = oid[1]
613 ext = rhns.branch(root)
614 return Product(hash, ext)
615
617 """
618 Get a list products defined in the certificate.
619 @return: A list of product objects.
620 @rtype: [L{Product},..]
621 """
622 lst = []
623 rhns = self.redhat()
624 for p in rhns.find('1.*.1'):
625 oid = p[0]
626 root = oid.rtrim(1)
627 hash = oid[1]
628 ext = rhns.branch(root)
629 lst.append(Product(hash, ext))
630 return lst
631
637
651
654 """
655 Represents an entitlement certificate.
656 """
657
659 """
660 Get the B{order} object defined in the certificate.
661 @return: An order object.
662 @rtype: L{Order}
663 """
664 rhns = self.redhat()
665 order = rhns.find('4.1', 1)
666 if order:
667 p = order[0]
668 oid = p[0]
669 root = oid.rtrim(1)
670 ext = rhns.branch(root)
671 return Order(ext)
672
674 """
675 Get the B{all} entitlements defined in the certificate.
676 @return: A list of entitlement object.
677 @rtype: [L{Entitlement},..]
678 """
679 return self.getContentEntitlements() \
680 + self.getRoleEntitlements()
681
683 """
684 Get the B{content} entitlements defined in the certificate.
685 @return: A list of entitlement object.
686 @rtype: [L{Content},..]
687 """
688 lst = []
689 rhns = self.redhat()
690 entitlements = rhns.find('2.*.1.1')
691 for ent in entitlements:
692 oid = ent[0]
693 root = oid.rtrim(1)
694 ext = rhns.branch(root)
695 lst.append(Content(ext))
696 return lst
697
699 """
700 Get the B{role} entitlements defined in the certificate.
701 @return: A list of entitlement object.
702 @rtype: [L{Role},..]
703 """
704 lst = []
705 rhns = self.redhat()
706 entitlements = rhns.find('3.*.1')
707 for ent in entitlements:
708 oid = ent[0]
709 root = oid.rtrim(1)
710 ext = rhns.branch(root)
711 lst.append(Role(ext))
712 return lst
713
719
728
731
734
736 return self.ext.get('1')
737
739 return self.ext.get('2')
740
742 return self.ext.get('3')
743
745 return self.ext.get('4')
746
748 return self.ext.get('5')
749
751 return self.ext.get('6')
752
754 return self.ext.get('7')
755
757 return self.ext.get('8')
758
760 return self.ext.get('9')
761
763 return self.ext.get('10')
764
766 return self.ext.get('11')
767
769 return self.ext.get('12')
770
788
791
793 self.hash = hash
794 self.ext = ext
795
798
800 return self.ext.get('1')
801
803 return self.ext.get('2')
804
806 return self.ext.get('3')
807
809 return self.ext.get('4')
810
813
824
827
833
834
835 -class Content(Entitlement):
836
838 return self.ext.get('1')
839
840 - def getLabel(self):
841 return self.ext.get('2')
842
843 - def getQuantity(self):
844 return self.ext.get('3')
845
846 - def getFlexQuantity(self):
847 return self.ext.get('4')
848
849 - def getVendor(self):
850 return self.ext.get('5')
851
853 return self.ext.get('6')
854
856 return self.ext.get('7')
857
858 - def getEnabled(self):
859 return self.ext.get('8')
860
861 - def __eq__(self, rhs):
862 return ( self.getLabel() == rhs.getLabel() )
863
865 s = []
866 s.append('Entitlement (content) {')
867 s.append('\tName ........ = %s' % self.getName())
868 s.append('\tLabel ....... = %s' % self.getLabel())
869 s.append('\tQuantity .... = %s' % self.getQuantity())
870 s.append('\tFlex Quantity = %s' % self.getFlexQuantity())
871 s.append('\tVendor ...... = %s' % self.getVendor())
872 s.append('\tURL ......... = %s' % self.getUrl())
873 s.append('\tGPG Key ..... = %s' % self.getGpg())
874 s.append('\tEnabled ..... = %s' % self.getEnabled())
875 s.append('}')
876 return '\n'.join(s)
877
878 - def __repr__(self):
880
881
882 -class Role(Entitlement):
883
885 return self.ext.get('1')
886
888 return self.ext.get('2')
889
892
900
903
906
907 KEY_PATTERN = re.compile(
908 '-----BEGIN.+KEY-----\n.+\n-----END.+KEY-----',
909 re.DOTALL)
910 CERT_PATTERN = re.compile(
911 '-----BEGIN CERTIFICATE-----\n.+\n-----END CERTIFICATE-----',
912 re.DOTALL)
913
914 @classmethod
916 m = cls.KEY_PATTERN.search(pem)
917 if m is None:
918 raise Exception, 'Key not found.'
919 key = m.group(0)
920 m = cls.CERT_PATTERN.search(pem)
921 if m is None:
922 raise Exception, 'Certificate not found.'
923 cert = m.group(0)
924 return (key, cert)
925
926 - def __init__(self, key=None, cert=None):
927 self.key = key
928 self.cert = cert
929
930 - def read(self, path):
931 f = open(path)
932 try:
933 b = self.split(f.read())
934 self.key = b[0]
935 self.cert = b[1]
936 self.path = path
937 finally:
938 f.close()
939 return self
940
942 f = open(path, 'w')
943 try:
944 f.write(str(self))
945 self.path = path
946 finally:
947 f.close()
948 return self
949
956
957
958 import sys
959 if __name__ == '__main__':
960 for path in sys.argv[1:]:
961 print path
962 pc = EntitlementCertificate()
963 pc.read(path)
964 print pc
965