Repository: secdev/scapy

Issue:
Scapy does not support generalized time format in certificate while dissecting TLS packet.
Scapy does not support generalized time format in certificate while dissecting TLS packet. UTC time format is hardcoded:
https://github.com/secdev/scapy/blob/e64b2617c2ca4eced0d31cafb0dc05f4827aaefd/scapy/layers/tls/cert.py#L617

That's why each [TLS][TLS Handshake - Certificate] packet containig a date with year >= 2050 ends up as [TLS][Raw] packet.

According to RFC 5280 (Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile):
```
4.1.  Basic Certificate Fields

[...]

Validity ::= SEQUENCE {
        notBefore      Time,
        notAfter       Time }

Time ::= CHOICE {
        utcTime        UTCTime,
        generalTime    GeneralizedTime }

[...]

4.1.2.5.  Validity

[...]

   CAs conforming to this profile MUST always encode certificate
   validity dates through the year 2049 as UTCTime; certificate validity
   dates in 2050 or later MUST be encoded as GeneralizedTime.
   Conforming applications MUST be able to process validity dates that
   are encoded in either UTCTime or GeneralizedTime.
```

This workaround solves the problem, but I think that a better (object-oriented) solution is needed:
```
68a69,79
> def strptime_utc_generalized(string):
>     """Parse a UTC/Generalized time string to a time tuple."""
>     str_length = len(string)
>     if str_length == 12:
>         fmt = "%y%m%d%H%M%S"
>     elif str_length == 14:
>         fmt = "%Y%m%d%H%M%S"
>     else:
>         raise ValueError("Time data '{}' does not match any supported format.".format(string))
>     return time.strptime(string, fmt)
>
606c617
<             self.notBefore = time.strptime(notBefore, "%y%m%d%H%M%S")
---
>             self.notBefore = strptime_utc_generalized(notBefore)
616c627
<             self.notAfter = time.strptime(notAfter, "%y%m%d%H%M%S")
---
>             self.notAfter = strptime_utc_generalized(notAfter)
```
My proposition is to replace:
``` 
       notAfter = tbsCert.validity.not_after.val
        if notAfter[-1] == "Z":
            notAfter = notAfter[:-1]
        try:
            self.notAfter = time.strptime(notAfter, "%y%m%d%H%M%S")
        except:
            raise Exception(error_msg)
        self.notAfter_str_simple = time.strftime("%x", self.notAfter)
```
with something like this:
```
self.notAfter_str_simple = tbsCert.validity.not_after.to_simple_str()  # to_simple_str() to be implemented
```


Discussion:
Hi, nice catch !

Could you provide a tls packet (pcap or raw or hex) that replicates the issue ?
Thanks !
FTR on Ubuntu /etc/ssl/certs/Certum_Trusted_Network_CA_2.pem uses GENERALIZEDTIME.
Here is a script that strips data from a certificate:
```
from scapy.all import *

load_layer("tls")

cert = Cert("Certum_Trusted_Network_CA_2.pem")

x509_cert = cert.x509Cert
x509_cert.tbsCertificate.version = b""
x509_cert.tbsCertificate.serialNumber = 0
x509_cert.tbsCertificate.issuer = b""
x509_cert.tbsCertificate.subject = b""
x509_cert.tbsCertificate.subjectPublicKeyInfo.subjectPublicKey.modulus = 0
x509_cert.tbsCertificate.extensions = b""
x509_cert.signatureValue = b""

data = raw(x509_cert)
x = X509_Cert(data)
x.show()
```

Resolving patch:
diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py
--- a/scapy/asn1/asn1.py
+++ b/scapy/asn1/asn1.py
@@ -445,14 +445,14 @@ def __setattr__(self, name, value):
             pretty_time = None
             if isinstance(self, ASN1_GENERALIZED_TIME):
                 _len = 15
-                _format = "%Y%m%d%H%M%S"
+                self._format = "%Y%m%d%H%M%S"
             else:
                 _len = 13
-                _format = "%y%m%d%H%M%S"
+                self._format = "%y%m%d%H%M%S"
             _nam = self.tag._asn1_obj.__name__[4:].lower()
             if (isinstance(value, str) and
                     len(value) == _len and value[-1] == "Z"):
-                dt = datetime.strptime(value[:-1], _format)
+                dt = datetime.strptime(value[:-1], self._format)
                 pretty_time = dt.strftime("%b %d %H:%M:%S %Y GMT")
             else:
                 pretty_time = "%s [invalid %s]" % (value, _nam)
diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py
--- a/scapy/layers/tls/cert.py
+++ b/scapy/layers/tls/cert.py
@@ -611,7 +611,8 @@ def import_from_asn1pkt(self, cert):
         if notBefore[-1] == "Z":
             notBefore = notBefore[:-1]
         try:
-            self.notBefore = time.strptime(notBefore, "%y%m%d%H%M%S")
+            _format = tbsCert.validity.not_before._format
+            self.notBefore = time.strptime(notBefore, _format)
         except Exception:
             raise Exception(error_msg)
         self.notBefore_str_simple = time.strftime("%x", self.notBefore)
@@ -621,7 +622,8 @@ def import_from_asn1pkt(self, cert):
         if notAfter[-1] == "Z":
             notAfter = notAfter[:-1]
         try:
-            self.notAfter = time.strptime(notAfter, "%y%m%d%H%M%S")
+            _format = tbsCert.validity.not_after._format
+            self.notAfter = time.strptime(notAfter, _format)
         except Exception:
             raise Exception(error_msg)
         self.notAfter_str_simple = time.strftime("%x", self.notAfter)


Test patch:
diff --git a/test/cert.uts b/test/cert.uts
--- a/test/cert.uts
+++ b/test/cert.uts
@@ -634,3 +634,16 @@ except:
 
 repr_str = Chain([], c0).__repr__()
 assert repr_str == '__ /OU=Domain Control Validated/CN=*.tools.ietf.org [Not Self Signed]\n'
+
+= Test GeneralizedTime
+
+data = b"MHAwXAIBADANBgkqhkiG9w0BAQ0FADAAMCIYDzIwMTExMDA2MDgzOTU2WhgPMjA0NjEwMDYwODM5NTZaMAAwHDANBgkqhkiG9w0BAQEFAAMLADAIAgEAAgMBAAGjAjAAMA0GCSqGSIb3DQEBDQUAAwEA"
+import tempfile, os
+_, filename = tempfile.mkstemp()
+fd = open(filename, "wb")
+fd.write(b"-----BEGIN CERTIFICATE-----\n")
+fd.write(data)
+fd.write(b"-----END CERTIFICATE-----\n")
+fd.close()
+cert = Cert(filename)
+assert "2011" in cert.notBefore_str and "2046" in cert.notAfter_str