From bf89ba8bce204313dc6448146d4d7a299847b71e Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Fri, 29 Dec 2017 20:14:50 +0100 Subject: [PATCH] Update for TLS PEP Auto-detection of PEM / DER is rather complicated and may not always work reliable. All PEM parsers (that I know) ignore extra data before and after the BEGIN/END block. Some data may look like DER at the beginning and then turn out to be PEM later. Instead of guessing the format, how about a format argument with PEM as default? Most people will use PEM anyway. Related to that, OpenSSL has a file format "TRUSTED CERTIFICATE", which is a standard PEM + additional AUX data at the end of the certificate blob. With a format argument, we can easily support additional formats for load and dump. The certificate class can only load the first certificate from a PEM bundle. Except for trivial test cases, a chain and CA bundle are made up from multiple certs. IMO dedicated load methods for chain and batch of certs would be useful for multiple reasons. It's easier and more efficient to use existing load functions extract all certs from a PEM bundle. A chain bundle can contain private key, too. With a dedicated function we can avoid to load the private key into Python memory. And OpenSSL makes it a bit more awkward because it loads the EE cert differently than the rest of the chain, https://github.com/openssl/openssl/blob/master/ssl/ssl_rsa.c#L619 Signed-off-by: Christian Heimes --- pep-0543.rst | 149 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 128 insertions(+), 21 deletions(-) diff --git a/pep-0543.rst b/pep-0543.rst index 539863ac7f3..99ed2605e0f 100644 --- a/pep-0543.rst +++ b/pep-0543.rst @@ -9,7 +9,7 @@ Type: Standards Track Content-Type: text/x-rst Created: 17-Oct-2016 Python-Version: 3.7 -Post-History: 11-Jan-2017, 19-Jan-2017, 02-Feb-2017, 09-Feb-2017 +Post-History: 11-Jan-2017, 19-Jan-2017, 02-Feb-2017, 09-Feb-2017, 29-Dec-2017 Abstract @@ -1256,6 +1256,32 @@ The definitions of the errors are below:: attack and so can ignore this exception. """ +File format +~~~~~~~~~~~ + +Certificates and private keys can be encoded in multiple ways. The most +important file formats are DER and PEM. DER (*Distinguished Encoding +Rules*) describes ASN.1 data structure as bytes. The PEM format is +base64-encoded DER with a header, footer, and optional metadata. For +example a PEM-encoded encrypted private key in PKCS#8 format has additional +PKCS#5 metadata. A file in DER format can only contain one certificate or +key, while a PEM encoded file can contain multiple certs and a key together +with additional text. + +A PKCS#12 file is an archive format private keys, certificates, trust +anchors and other data with optional encryption and signature. The PKCS#12 +format is out of scope. + +:: + + class FileFormat(Enum): + DER = auto() + PEM = auto() + +Libraries may define their own file formats, too. For example OpenSSL has +trusted certificates, which are regular PEM-encoded certificates with an AUX +section appended. + Certificates ~~~~~~~~~~~~ @@ -1283,19 +1309,28 @@ an individual concrete implementation are also hashable. class Certificate(metaclass=ABCMeta): @abstractclassmethod - def from_buffer(cls, buffer: bytes): + def from_buffer( + cls, + buffer: bytes, + *, + password: Optional[Union[Callable[[], Union[bytes, bytearray]], bytes, bytearray]] = None, + format: FileFormat = FileFormat.PEM + ) -> Certificate: """ Creates a Certificate object from a byte buffer. This byte buffer - may be either PEM-encoded or DER-encoded. If the buffer is PEM - encoded it *must* begin with the standard PEM preamble (a series of - dashes followed by the ASCII bytes "BEGIN CERTIFICATE" and another - series of dashes). In the absence of that preamble, the - implementation may assume that the certificate is DER-encoded - instead. + may be either PEM-encoded or DER-encoded. The password argument + may be used in the future for additional file formats like + PKCS#12 files. """ @abstractclassmethod - def from_file(cls, path: Union[pathlib.Path, AnyStr]): + def from_file( + cls, + path: Union[pathlib.Path, AnyStr], + *, + password: Optional[Union[Callable[[], Union[bytes, bytearray]], bytes, bytearray]] = None, + format: FileFormat = FileFormat.PEM + ) -> Certificate: """ Creates a Certificate object from a file on disk. This method may be a convenience method that wraps ``open`` and ``from_buffer``, @@ -1304,6 +1339,75 @@ an individual concrete implementation are also hashable. code. """ + @abstractclassmethod + def chain_from_buffer( + cls, + buffer: bytes, + *, + password: Optional[Union[Callable[[], Union[bytes, bytearray]], bytes, bytearray]] = None, + format: FileFormat = FileFormat.PEM + ) -> Tuple[Certificate]: + """ + Creates a Certificate object from a byte buffer. This byte buffer + must be PEM-encoded. The password and format arguments are + currently not used. + """ + + @abstractclassmethod + def chain_from_file( + cls, + path: Union[pathlib.Path, AnyStr], + *, + password: Optional[Union[Callable[[], Union[bytes, bytearray]], bytes, bytearray]] = None, + format: FileFormat = FileFormat.PEM + ) -> Tuple[Certificate]: + """ + Creates a Certificate chain from a file on disk. This method may + be a convenience method that wraps ``open`` and + ``chain_from_buffer``, but some TLS implementations may be able + to provide more-secure or faster methods of loading certificates + that do not involve Python code. + + A certificate chain must start with the end-entity cert (server + or client cert) and may be followed by one or more intermediate + CA certs. The second cert must be the issuer of the first cert, + the third cert the issuer of the second cert, and so on. The + chain must not contain a trust anchor (root cert). + """ + + @abstractclassmethod + def bundle_from_buffer( + cls, + buffer: bytes, + *, + password: Optional[Union[Callable[[], Union[bytes, bytearray]], bytes, bytearray]] = None, + format: FileFormat = FileFormat.PEM + ) -> List[Certificate]: + """ + Creates a bundle of Certificate objects from a byte buffer. This + byte buffer must be PEM-encoded. The password and format + arguments are currently not used. + """ + + @abstractclassmethod + def bundle_from_file( + cls, + path: Union[pathlib.Path, AnyStr], + *, + password: Optional[Union[Callable[[], Union[bytes, bytearray]], bytes, bytearray]] = None, + format: FileFormat = FileFormat.PEM + ) -> List[Certificate]: + """ + Creates a Certificate bundle from a file on disk. This method may + be a convenience method that wraps ``open`` and + ``bundle_from_buffer``, but some TLS implementations may be able + to provide more-secure or faster methods of loading certificates + that do not involve Python code. + """ + + def dump(self, format: FileFormat = FileFormat.PEM) -> bytes: + """Dump certificate as PEM or DER-encoded bytes. + """ Private Keys ~~~~~~~~~~~~ @@ -1318,17 +1422,16 @@ This class has all the caveats of the ``Certificate`` class. class PrivateKey(metaclass=ABCMeta): @abstractclassmethod - def from_buffer(cls, - buffer: bytes, - password: Optional[Union[Callable[[], Union[bytes, bytearray]], bytes, bytearray]] = None): + def from_buffer( + cls, + buffer: bytes, + *, + password: Optional[Union[Callable[[], Union[bytes, bytearray]], bytes, bytearray]] = None, + format: FileFormat = FileFormat.PEM + ) -> PrivateKey: """ Creates a PrivateKey object from a byte buffer. This byte buffer - may be either PEM-encoded or DER-encoded. If the buffer is PEM - encoded it *must* begin with the standard PEM preamble (a series of - dashes followed by the ASCII bytes "BEGIN", the key type, and - another series of dashes). In the absence of that preamble, the - implementation may assume that the certificate is DER-encoded - instead. + may be either PEM-encoded or DER-encoded. The key may additionally be encrypted. If it is, the ``password`` argument can be used to decrypt the key. The ``password`` argument @@ -1342,9 +1445,13 @@ This class has all the caveats of the ``Certificate`` class. """ @abstractclassmethod - def from_file(cls, - path: Union[pathlib.Path, bytes, str], - password: Optional[Union[Callable[[], Union[bytes, bytearray]], bytes, bytearray]] = None): + def from_file( + cls, + path: Union[pathlib.Path, bytes, str], + *, + password: Optional[Union[Callable[[], Union[bytes, bytearray]], bytes, bytearray]] = None, + format: FileFormat = FileFormat.PEM + ) -> PrivateKey: """ Creates a PrivateKey object from a file on disk. This method may be a convenience method that wraps ``open`` and ``from_buffer``,