From 9fd122e61443f6697e6b6ebc173f97fa24211524 Mon Sep 17 00:00:00 2001 From: Sun Yimin Date: Mon, 25 Nov 2024 08:41:30 +0800 Subject: [PATCH] x509: generate serial number for nil template SerialNumber #279 --- smx509/x509.go | 32 ++++++++++++++++++++++++++++---- smx509/x509_test.go | 45 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/smx509/x509.go b/smx509/x509.go index 0e90837..4aaa820 100644 --- a/smx509/x509.go +++ b/smx509/x509.go @@ -25,6 +25,7 @@ import ( "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" + cryptorand "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/x509" @@ -1422,6 +1423,10 @@ var emptyASN1Subject = []byte{0x30, 0} // // If SubjectKeyId from template is empty and the template is a CA, SubjectKeyId // will be generated from the hash of the public key. +// +// If template.SerialNumber is nil, a serial number will be generated which +// conforms to RFC 5280, Section 4.1.2.2 using entropy from rand. +// func CreateCertificate(rand io.Reader, template, parent, pub, priv any) ([]byte, error) { realTemplate, err := toCertificate(template) if err != nil { @@ -1438,8 +1443,27 @@ func CreateCertificate(rand io.Reader, template, parent, pub, priv any) ([]byte, return nil, errors.New("x509: certificate private key does not implement crypto.Signer") } - if realTemplate.SerialNumber == nil { - return nil, errors.New("x509: no SerialNumber given") + serialNumber := realTemplate.SerialNumber + if serialNumber == nil { + // Generate a serial number following RFC 5280 Section 4.1.2.2 if one is not provided. + // Requirements: + // - serial number must be positive + // - at most 20 octets when encoded + maxSerial := big.NewInt(1).Lsh(big.NewInt(1), 20*8) + for { + var err error + serialNumber, err = cryptorand.Int(rand, maxSerial) + if err != nil { + return nil, err + } + // If the serial is exactly 20 octets, check if the high bit of the first byte is set. + // If so, generate a new serial, since it will be padded with a leading 0 byte during + // encoding so that the serial is not interpreted as a negative integer, making it + // 21 octets. + if serialBytes := serialNumber.Bytes(); len(serialBytes) > 0 && (len(serialBytes) < 20 || serialBytes[0]&0x80 == 0) { + break + } + } } // RFC 5280 Section 4.1.2.2: serial number must positive @@ -1447,7 +1471,7 @@ func CreateCertificate(rand io.Reader, template, parent, pub, priv any) ([]byte, // We _should_ also restrict serials to <= 20 octets, but it turns out a lot of people // get this wrong, in part because the encoding can itself alter the length of the // serial. For now we accept these non-conformant serials. - if realTemplate.SerialNumber.Sign() == -1 { + if serialNumber.Sign() == -1 { return nil, errors.New("x509: serial number must be positive") } @@ -1513,7 +1537,7 @@ func CreateCertificate(rand io.Reader, template, parent, pub, priv any) ([]byte, encodedPublicKey := asn1.BitString{BitLength: len(publicKeyBytes) * 8, Bytes: publicKeyBytes} c := tbsCertificate{ Version: 2, - SerialNumber: realTemplate.SerialNumber, + SerialNumber: serialNumber, SignatureAlgorithm: algorithmIdentifier, Issuer: asn1.RawValue{FullBytes: asn1Issuer}, Validity: validity{realTemplate.NotBefore.UTC(), realTemplate.NotAfter.UTC()}, diff --git a/smx509/x509_test.go b/smx509/x509_test.go index b22a8e3..444dde0 100644 --- a/smx509/x509_test.go +++ b/smx509/x509_test.go @@ -1626,16 +1626,18 @@ func TestInsecureAlgorithmErrorString(t *testing.T) { */ // These CSR was generated with OpenSSL: -// openssl req -out CSR.csr -new -sha256 -nodes -keyout privateKey.key -config openssl.cnf +// +// openssl req -out CSR.csr -new -sha256 -nodes -keyout privateKey.key -config openssl.cnf // // With openssl.cnf containing the following sections: -// [ v3_req ] -// basicConstraints = CA:FALSE -// keyUsage = nonRepudiation, digitalSignature, keyEncipherment -// subjectAltName = email:gopher@golang.org,DNS:test.example.com -// [ req_attributes ] -// challengePassword = ignored challenge -// unstructuredName = ignored unstructured name +// +// [ v3_req ] +// basicConstraints = CA:FALSE +// keyUsage = nonRepudiation, digitalSignature, keyEncipherment +// subjectAltName = email:gopher@golang.org,DNS:test.example.com +// [ req_attributes ] +// challengePassword = ignored challenge +// unstructuredName = ignored unstructured name var csrBase64Array = [...]string{ // Just [ v3_req ] "MIIDHDCCAgQCAQAwfjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLQ29tbW9uIE5hbWUxITAfBgkqhkiG9w0BCQEWEnRlc3RAZW1haWwuYWRkcmVzczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK1GY4YFx2ujlZEOJxQVYmsjUnLsd5nFVnNpLE4cV+77sgv9NPNlB8uhn3MXt5leD34rm/2BisCHOifPucYlSrszo2beuKhvwn4+2FxDmWtBEMu/QA16L5IvoOfYZm/gJTsPwKDqvaR0tTU67a9OtxwNTBMI56YKtmwd/o8d3hYv9cg+9ZGAZ/gKONcg/OWYx/XRh6bd0g8DMbCikpWgXKDsvvK1Nk+VtkDO1JxuBaj4Lz/p/MifTfnHoqHxWOWl4EaTs4Ychxsv34/rSj1KD1tJqorIv5Xv2aqv4sjxfbrYzX4kvS5SC1goIovLnhj5UjmQ3Qy8u65eow/LLWw+YFcCAwEAAaBZMFcGCSqGSIb3DQEJDjFKMEgwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwLgYDVR0RBCcwJYERZ29waGVyQGdvbGFuZy5vcmeCEHRlc3QuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAB6VPMRrchvNW61Tokyq3ZvO6/NoGIbuwUn54q6l5VZW0Ep5Nq8juhegSSnaJ0jrovmUgKDN9vEo2KxuAtwG6udS6Ami3zP+hRd4k9Q8djJPb78nrjzWiindLK5Fps9U5mMoi1ER8ViveyAOTfnZt/jsKUaRsscY2FzE9t9/o5moE6LTcHUS4Ap1eheR+J72WOnQYn3cifYaemsA9MJuLko+kQ6xseqttbh9zjqd9fiCSh/LNkzos9c+mg2yMADitaZinAh+HZi50ooEbjaT3erNq9O6RqwJlgD00g6MQdoz9bTAryCUhCQfkIaepmQ7BxS0pqWNW3MMwfDwx/Snz6g=", @@ -2126,6 +2128,33 @@ func TestAdditionFieldsInGeneralSubtree(t *testing.T) { } } +func TestEmptySerialNumber(t *testing.T) { + template := Certificate{ + DNSNames: []string{"example.com"}, + } + for i := 0; i < 100; i++ { + derBytes, err := CreateCertificate(rand.Reader, &template, &template, &testPrivateKey.PublicKey, testPrivateKey) + if err != nil { + t.Fatalf("failed to create certificate: %s", err) + } + cert, err := ParseCertificate(derBytes) + if err != nil { + t.Fatalf("failed to parse certificate: %s", err) + } + if sign := cert.SerialNumber.Sign(); sign != 1 { + t.Fatalf("generated a non positive serial, sign: %d", sign) + } + b, err := asn1.Marshal(cert.SerialNumber) + if err != nil { + t.Fatalf("failed to marshal generated serial number: %s", err) + } + // subtract 2 for tag and length + if l := len(b) - 2; l > 20 { + t.Fatalf("generated serial number larger than 20 octets when encoded: %d", l) + } + } +} + func TestEmptySubject(t *testing.T) { template := x509.Certificate{ SerialNumber: big.NewInt(1),