openwisp/django-x509

Not conforming to RFC5280 - certificate use GENERALIZEDTIME for dates <2050

ezaquarii opened this issue · 2 comments

Generated certificate use GENERALIZEDTIME for dates <2050.

According to this RFC https://tools.ietf.org/html/rfc5280#section-4.1.2.5:

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.

Here is a test ca.crt generated with django-x509:

-----BEGIN CERTIFICATE-----
MIIDtjCCAp6gAwIBAgIQVeW93R49QC+y5UU79QyzizANBgkqhkiG9w0BAQsFADA4
MSIwIAYJKoZIhvcNAQkBFhNoZWxsb0BlemFxdWFyaWkuY29tMRIwEAYDVQQDDAlz
ZXJ2ZXItY2EwIhgPMjAxODEyMTIwMDAwMDBaGA8yMDE5MTIxMzAwMDAwMFowODEi
MCAGCSqGSIb3DQEJARYTaGVsbG9AZXphcXVhcmlpLmNvbTESMBAGA1UEAwwJc2Vy
dmVyLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArZ4waqi5X93l
5qQ6RTC0RrUnn768Sz4svOw8QL6JNNLvntia9bkA9JKofIYDoOxk5h7sY+Oy0HQe
cm4RCQgMwqC87PmVBH/ov1xBxMavqJxDaWfhWMp56lZZBcEHvRAICja6scC1i9ex
APEzwoJ+LUMvRwh629mYUVdOmNMeeZxc3pcYpcDFMDRYpn0gP98Cke+hH8Of7cG1
o9S+l+bQ4/l0zfUJybthY19VfyULLn7KVRm3bi+7w56oLNc14SQQssZoPteuRiHu
M6SiCAM4jzO7RYjwzvB0OZ2eYBQcuG9K9mnf3sxzmlNQpffo8qkmxP1XCrhhkTI0
kIGvGVLRewIDAQABo4G3MIG0MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/
BAQDAgEGMB0GA1UdDgQWBBSOTILApLPKh37AMXE+fRCLQVjUzzBvBgNVHSMEaDBm
gBSOTILApLPKh37AMXE+fRCLQVjUz6E8pDowODEiMCAGCSqGSIb3DQEJARYTaGVs
bG9AZXphcXVhcmlpLmNvbTESMBAGA1UEAwwJc2VydmVyLWNhghBV5b3dHj1AL7Ll
RTv1DLOLMA0GCSqGSIb3DQEBCwUAA4IBAQBhe9m/zQlA2B/mxXOZiiwRWzHNQxPh
TFl2r6sYZA3Vb5VHmp9pYuMKrATNQW1rNVgN8GVA+6qjhM5OqZ45/zh392KTV1DJ
kP12pNbbuqekHn6U8tf+SSPdqXbNy/FFTfq1D7zwmqXsyKzwCXYt+7DxuK+07GGK
Omj/ImiczOtm/DvVEVoFzV/cTEbqNUc0r3ssQN2okiOr+2EbS7Gc8tY+6385/hpK
0t3Kl80P+NERsNrjiyew+NoqGEYqMtWWHePxzB00Q0Zdid9WVY1BBbz/s61LsRb4
3ocaruGTLmiHzKo7XPYm2rriTEiRcL+jeFSReq4RU4qS1Bs9upYCIBim
-----END CERTIFICATE-----

Here is a matching test cert.crt:

-----BEGIN CERTIFICATE-----
MIIDwTCCAqmgAwIBAgIQU6Th6IdIQZuLsCIshr/qOzANBgkqhkiG9w0BAQsFADA4
MSIwIAYJKoZIhvcNAQkBFhNoZWxsb0BlemFxdWFyaWkuY29tMRIwEAYDVQQDDAlz
ZXJ2ZXItY2EwIhgPMjAxODEyMTIwMDAwMDBaGA8yMDE5MTIxMzAwMDAwMFowTzEi
MCAGCSqGSIb3DQEJARYTaGVsbG9AZXphcXVhcmlpLmNvbTEpMCcGA1UEAwwgNjE1
M2JiZWZjN2UzNDljYTkxYmVmMjQzZjJiMjhhYjkwggEiMA0GCSqGSIb3DQEBAQUA
A4IBDwAwggEKAoIBAQDqpd3attN8kLIE+dy1gf66pmzsKXhP2/ZQ+EGdgYDlXJ40
vsXJ1eqjlMR6acNeyb0LeOhILtho9Mx+jPKVYrjgsjjvD4OM7Yu4w/qRhzmj8gop
z69FrrmQkGt9ziXVcMjI17k5Lmy/l6FdIAIKaANZ5tGpOgZhe3kM7Lf+ldM+knJc
Pqi6RNrNilbjaxxC7DHlDwUcxEabJqRi0vP61WoEtx5DjoxQp2umu0iwNLx/kx/0
Ae4NV3RMXIxvkwVgOspNVNwkwzv/Z6mi2p3i0prSgtLYmRHHZHQ5BaNJYRwc9Z2S
u0xCOwJUIsvrYfmFJcsfcrQQ7y+N+Pzzxbg4eQPlAgMBAAGjgaswgagwCQYDVR0T
BAIwADALBgNVHQ8EBAMCBaAwHQYDVR0OBBYEFGDZUAM0v0j1yqGehbO8tJTtBcAD
MG8GA1UdIwRoMGaAFI5MgsCks8qHfsAxcT59EItBWNTPoTykOjA4MSIwIAYJKoZI
hvcNAQkBFhNoZWxsb0BlemFxdWFyaWkuY29tMRIwEAYDVQQDDAlzZXJ2ZXItY2GC
EFXlvd0ePUAvsuVFO/UMs4swDQYJKoZIhvcNAQELBQADggEBAIRM4FFfUYtSyu2x
pCMIt0oRcyoBNhB7E8qjSuR7wWOLfEKzp+5Arn8S3HLHNdYHVyYg4xC14KIwKGVL
ZuOZ9GeEp0yigXwpHuebxgwzl8O07MTQEPPcPQ4C0DRZYTqu2p0VwP2QWFOxo0cW
20GhwUDIcLFmSNBsP4XAK6HxnVBCpD6lpkrNqKxvgE9R3zozB3CzunnYanBMKPQS
5cyN0uJD6AcO1IeSJMNGcqbdY5hmd3OgwepZURAQnARe00NKqLqnFW4LxulVy6Kx
nfnHPzBqJV49b5l40vTVsXNuAmUL3LIJ7V+cP7xB/YspCSelOFivzdAscbP4PDQh
bkzmxkA=
-----END CERTIFICATE-----

Verification using LibreSSL fails:

$ openssl verify -CAfile ca.crt cert.crt
cert.crt: emailAddress = xxxx@xxxxxxx.com, CN = server-ca
error 13 at 1 depth lookup:format error in certificate's notBefore field
emailAddress = xxxx@xxxxxxx.com, CN = server-ca
error 13 at 1 depth lookup:format error in certificate's notBefore field
emailAddress = xxxx@xxxxxxx.com, CN = server-ca
error 13 at 1 depth lookup:format error in certificate's notBefore field

That is because LibreSSL has intentionaly stricter parser and
CA uses GENERALIZEDTIME:

$ openssl asn1parse -in ca.crt | grep TIME
  106:d=3  hl=2 l=  15 prim: GENERALIZEDTIME   :20181212000000Z
  123:d=3  hl=2 l=  15 prim: GENERALIZEDTIME   :20191213000000Z

Weird! I didn't notice that at all.
Thanks for reporting.
I guess changing these parts is necessary:

  • cert.set_notBefore(bytes_compat(self.validity_start.strftime(generalized_time)))
    cert.set_notAfter(bytes_compat(self.validity_end.strftime(generalized_time)))
  • self.validity_start = datetime.strptime(not_before,
    generalized_time)
    self.validity_start = timezone.make_aware(self.validity_start)
    not_after = cert.get_notAfter().decode('utf8')
    self.validity_end = datetime.strptime(not_after,
    generalized_time)

Although generalized_time is also used in the CRL, not sure if that's an issue as well:

@ezaquarii can you test the PR #77 when you have time?
I tested it and it looks good to me.