PeculiarVentures / x509

@peculiar/x509 is an easy to use TypeScript/Javascript library based on @peculiar/asn1-schema that makes generating X.509 Certificates and Certificate Requests as well as validating certificate chains easy
https://peculiarventures.github.io/x509/
MIT License
78 stars 10 forks source link

Incorrect time format for expiry dates after 2050? #73

Open achingbrain opened 3 months ago

achingbrain commented 3 months ago

I'm creating certificates with expiry dates in the far future.

import * as x509 from '@peculiar/x509'

const selfCert = await x509.X509CertificateGenerator.createSelfSigned({
  notBefore: new Date(now - CERT_VALIDITY_PERIOD_FROM),
  notAfter: new Date(now + CERT_VALIDITY_PERIOD_TO),
  //... other params
})

Trying to use them over TLS results in errors on the remote like:

failed to negotiate security protocol: tls: failed to parse certificate from server: x509: malformed GeneralizedTime

From what I understand X.509 has two time representations, UTCTime if the date is 2049 or earlier, and GeneralizedTime if the date is 2050 or later.

If I make the expiry date before 2049 everything works as expected.

Could this module be representing everything as UTCTime?

microshine commented 3 months ago

In accordance with RFC5280 (https://datatracker.ietf.org/doc/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. Conforming applications MUST be able to process validity dates that are encoded in either UTCTime or GeneralizedTime.

The current version of the module does not support the capability to specify the desired time type for notBefore and notAfter. There is a need to extend the module to support this functionality.

The implementation class that determines the choice between UTCTime or GeneralizedTime can be found here: https://github.com/PeculiarVentures/asn1-schema/blob/7721a9f59546e70ef24b86e54dd26105954e50eb/packages/x509/src/time.ts#L22.

microshine commented 3 months ago

@achingbrain I utilized one of the tests to generate an X509 certificate with GeneralizedTime and attempted to read this certificate using OpenSSL.

openssl x509 -in cert.pem -text -noout

OpenSSL successfully read the certificate without any issues.

SEQUENCE (3 elem)
  SEQUENCE (8 elem)
    [0] (1 elem)
      INTEGER 2
    INTEGER 1
    SEQUENCE (2 elem)
      OBJECT IDENTIFIER 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1)
      NULL
    SEQUENCE (2 elem)
      SET (1 elem)
        SEQUENCE (2 elem)
          OBJECT IDENTIFIER 2.5.4.3 commonName (X.520 DN component)
          PrintableString Test
      SET (1 elem)
        SEQUENCE (2 elem)
          OBJECT IDENTIFIER 2.5.4.10 organizationName (X.520 DN component)
          UTF8String Дом
    SEQUENCE (2 elem)
      UTCTime 2019-12-31 23:00:00 UTC
      GeneralizedTime 2059-12-31 23:00:00 UTC
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1 (0x1)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=Test, O=Дом
        Validity
            Not Before: Jan  1 00:00:00 2020 GMT
            Not After : Jan  1 00:00:00 2060 GMT

I also tried to read the generated certificate using Go, and it worked as well.

package main

import (
    "crypto/x509"
    "encoding/pem"
    "fmt"
    "io/ioutil"
    "log"
)

func main() {
    // Read the PEM certificate file
    data, err := ioutil.ReadFile("cert.pem")
    if err != nil {
        log.Fatalf("Failed to read the certificate file: %v", err)
    }

    // Parse the PEM certificate
    block, _ := pem.Decode(data)
    if block == nil || block.Type != "CERTIFICATE" {
        log.Fatalf("Failed to decode PEM block containing the certificate")
    }

    cert, err := x509.ParseCertificate(block.Bytes)
    if err != nil {
        log.Fatalf("Failed to parse certificate: %v", err)
    }

    // Print the NotBefore and NotAfter fields
    fmt.Printf("NotBefore: %v\n", cert.NotBefore)
    fmt.Printf("NotAfter: %v\n", cert.NotAfter)
}

Output

> go run cert.go
NotBefore: 2020-01-01 00:00:00 +0000 UTC
NotAfter: 2060-01-01 00:00:00 +0000 UTC
rmhrisk commented 2 months ago

I'm not too fond of the idea of exposing ASN.1 encoding types in X509, I do not believe we do this elsewhere, and it looks like a footgun to do so. As per RFC5280 GeneralizedTime should work today and after 250 and it is my understanding that is what the library does today. I suspect the issue is in the validator.

achingbrain commented 2 months ago

I also tried to read the generated certificate using Go, and it worked as well.

I see an error with this certificate, generated with @peculiar/x509:

-----BEGIN CERTIFICATE-----
MIIDUTCCAvagAwIBAgIIEymTRmkwCVIwCgYIKoZIzj0EAwIwADAkFw0yNDA0MDIx
MDA0MTVaGBMyMTI0MDMwOTExMDQxNS43OTJaMAAwWTATBgcqhkjOPQIBBggqhkjO
PQMBBwNCAAQBhVGQr9OLUew+L6aeXUcGsdiqb0p9b1Mej7HvHLMbkOoegU4haxxm
hVTFMn99SIEl9btWhleRAdXAHy0O4mMDo4ICUjCCAk4wggJKBgorBgEEAYOiWgEB
AQH/BIICNzCCAjMEggErCAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC/2CYK+ek+bYeq9wS252Z2cAreiRcjdavTproCzsPJxDIIKyTcur6XRoQ7
gLeNeoNXtwSx5FOIRpKzhuH8C4rijm5z6NaSX3d0PAZs9sluWpqPnr6qrTdn2Nkk
B+dfhz7g4xcTg8JrtDLMvZ4EkUTkcQ/Wz5NnLQ9ycnL1+4s9g2P/f4D6lmj5pXpg
aUyDstupqLGiB3w0H4aJusXG0RracWz9jPp/WuwnWW4ElS1suRPxwdstvhynsvUv
Fuzs3M9nEzja7jWRlE08MR5ftrHKQrhzyOSefzzepllWXmbwe2hSNkZ6mG15sbsT
0mDw5ObkEpKRDl6MTpV3Hmq7LK0zAgMBAAEEggEAthDQwcsekgECSWXy5DzAb/MR
Hq7feHS1rDTSved2KZ15qNiWPfWYDn6P9g1hGEJ7qUIpsoqL2vTczJ/BtAzds7PR
YJgWWI7UJOB2MIIRAOMVsE6ieAZl0dqI5v7y60QBNv3g1K15LhFTZK7rAXZewaCm
IxiRv+3l1MT0ERQECV0Q9hlEbXRaMMtJBAEHjkSzbEClJOnXMJhCTe7Erzw8ZRAI
S3Hqa1VIa0dFk15RdYQ8vAHHoh0aOJTmubfdxgiZ3eEtZi0wuXyJ2OISwwJpkPsI
IdLg6cK9M5FzdODpjRIM2avJo+UdEQuIZulUAhDiskOp4crSgaG4SGAQBYBfDzAK
BggqhkjOPQQDAgNJADBGAiEAmki95oiOVvCXBR5FvxIgxUSySxGvGVhZ77kQkCHt
P9ICIQCiuWI1wv+Ak7hYXOckKDTvcfPYE2SauTTPlddulP6obQ==
-----END CERTIFICATE-----

OpenSSL can parse it:

% openssl x509 -in ./cert.pem -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1761912615501990288 (0x1873923054468990)
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: 
        Validity
            Not Before: Apr  2 10:02:08 2024 GMT
            Not After : Mar  9 11:02:08.386 2124 GMT
        Subject: 
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:09:b9:6f:b8:a4:8a:7c:4b:f7:b7:9d:ab:36:8b:
                    c8:0a:ee:72:37:59:db:f3:24:89:72:59:9d:e8:04:
                    99:07:21:e5:31:bc:16:84:b3:06:1c:5b:7e:da:7b:
                    44:24:2b:0f:76:9b:95:8d:af:4f:77:da:b6:a1:62:
                    ff:44:ec:72:53
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            1.3.6.1.4.1.53594.1.1: critical
..........0..   0..3...+.....0.."0
........z'[....%Y^..82..~...>.....b...a.G.......+...........[P.I..jXA......s....zEm%..[..N.*g.....E....!..5.T...t<7(.BD.!...{.......y...K..0.o4J....;...&*........j3..^..a.....p.....t.l......1...H..i.Qj.q..J..a.....w.A.Mc.=p..p..<o...Ea.\FV..m.............Y.`..Ci...-..\...or...{C...........5..K....;..$...6..P.).v8.m7....x$ld}qT.F........,...a...5..
........q.n..}.+~..\q........_....l.'.V.Y..f.]..h..K].).{.G..Q..q '.
    Signature Algorithm: ecdsa-with-SHA256
    Signature Value:
        30:46:02:21:00:88:e4:c8:0c:54:60:ad:19:68:05:00:d8:82:
        b9:9f:cd:95:0c:fe:fa:5b:21:7e:c2:12:3b:0f:5e:c9:8d:9d:
        e5:02:21:00:b5:dd:b0:b4:45:98:5e:4d:5a:8c:f5:86:59:2c:
        8c:8f:62:93:6a:37:f3:1c:cd:07:30:97:89:43:10:d2:5f:33

Go fails using the script from above:

% go run parse.go 
2024/04/02 12:09:15 Failed to parse certificate: asn1: time did not serialize back to the original value and may be invalid: given "21240309110208.386Z", but serialized as "21240309110208Z"
exit status 1

RFC 5280 says:

GeneralizedTime values MUST NOT include fractional seconds. https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.5.2

Maybe this has something to do with it?

Certainly setting the ms of the notAfter date to 0 before using X509CertificateGenerator.createSelfSigned to create the cert seems to solve the issue.