We are making use of letsencrypt staging certificates for internal dev use and it looks like after the maintenance performed on Feb 18th (today) the issuer has changed from "Fake LE Intermediate X1" to "(STAGING) Artificial Apricot R3" and the staging X1 certificates available on Staging Environment - Let's Encrypt - Free SSL/TLS Certificates are no longer valid.
You can fetch the new staging root from http://stg-dst3.i.lencr.org/ (note! do not add to trust stores). The intermediates are served via ACME. Thanks for the reminder about the documentation page. We'll get that updated.
@jsha For testing purpose we use the staging cert for ssl-verify some of the URLs . All our testcases are failing as the staging root CA is an expired cert. Please confirm if this is is bug in letsencrypt or intentional ?r
equests.exceptions.SSLError (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1091)')))al
$ openssl x509 -inform der -in le-staging-root-durian.der -text -noout
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
ba:8c:66:24:4d:69:11:78:10:66:52:f6:ea:e5:8f:ed
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, O = (STAGING) Internet Security Research Group, CN = (STAGING) Doctored Durian Root CA X3
Validity
Not Before: Sep 30 21:12:19 2000 GMT
Not After : Jan 30 14:01:15 2021 GMT
Subject: C = US, O = (STAGING) Internet Security Research Group, CN = (STAGING) Doctored Durian Root CA X3
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:a9:46:63:a1:16:e3:81:77:9c:3d:6c:57:90:60:
...
Show as PEM:
$ openssl x509 -inform der -in le-staging-root-durian.der -out le-staging-root-durian.pem
FWIW, if anyone needs to do this without OpenSSL, PEM is just DER encoded into base64 with text-wrapping and a header/footer.
In Python the conversions between the two formats are:
import textwrap
import base64
def convert_der_to_pem(der_data=None, header_object="CERTIFICATE"):
"""
:param der_data: string of DER formatted certificate.
:param header_object: string. see function for valid options
"""
_valid_header_objects = (
"CERTIFICATE",
"RSA PRIVATE KEY",
"PRIVATE KEY",
"CERTIFICATE REQUEST",
)
if header_object not in _valid_header_objects:
raise ValueError("invalid header object")
as_pem = """-----BEGIN {0}-----\n{1}\n-----END {2}-----\n""".format(
header_object,
"\n".join(textwrap.wrap(base64.b64encode(der_data).decode("utf8"), 64)),
header_object,
)
return as_pem
def convert_pem_to_der(pem_data=None):
lines = [l.strip() for l in pem_data.strip().split("\n")]
# remove the BEGIN header
if (
("BEGIN CERTIFICATE" in lines[0])
or ("BEGIN RSA PRIVATE KEY" in lines[0])
or ("BEGIN PRIVATE KEY" in lines[0])
or ("BEGIN CERTIFICATE REQUEST" in lines[0])
):
lines = lines[1:]
# remove the END header
if (
("END CERTIFICATE" in lines[-1])
or ("END RSA PRIVATE KEY" in lines[-1])
or ("END PRIVATE KEY" in lines[-1])
or ("END CERTIFICATE REQUEST" in lines[-1])
):
lines = lines[:-1]
lines = "".join(lines)
result = base64.b64decode(lines)
return result
To supplement @jvanasco's excellent contribution, I've written a small bit of PHP that transforms an ECDSA CSR in PEM format to the Base64 DER format expected by the ACME protocol as the payload of order finalization. You can copy it from here then try it out at: