pyca / cryptography

cryptography is a package designed to expose cryptographic primitives and recipes to Python developers.
https://cryptography.io
Other
6.7k stars 1.54k forks source link

Feature request: expose the low-level X.509 signature layer #12018

Open mildsunrise opened 5 days ago

mildsunrise commented 5 days ago

There are several sign(self, privkey, algorithm, padding) methods, but they all work on high-level builders of different types. There doesn't seem to be a method that operates on an arbitrary to-be-signed byte string. A similar thing happens with verification.

The logic to encode, decode, create and verify X.509 signatures is neatly encapsulated in the x509::sign module, and exposing it to Python would be very helpful. Otherwise, as the current documentation states, creating or verifying X.509 signatures requires highly algorithm-specific code:

To validate the signature on a certificate you can do the following. Note: [...] this example will only work for RSA public keys with PKCS1v15 signatures, and so it can’t be used for general purpose signature verification.

alex commented 5 days ago

I'm not sure I understand what the actual feature request is here, can you try rephrasing it?

mildsunrise commented 5 days ago

Thanks for the quick response :)

I'm not sure I understand what the actual feature request is here, can you try rephrasing it?

Sure! When exposed to Python, the x509::sign API could look like this:

Example API 1 ```python def compute_signature_algorithm( private_key: CertificateIssuerPrivateKeyTypes, algorithm: _AllowedHashTypes, rsa_padding: padding.PSS | padding.PKCS1v15 | None, ) -> bytes: """ Serializes a hash algorithm and parameters into an X.509 signature algorithm, returning its DER representation. """ ... def sign_data( private_key: CertificateIssuerPrivateKeyTypes, algorithm: _AllowedHashTypes, rsa_padding: padding.PSS | padding.PKCS1v15 | None, data: bytes, ) -> bytes: """ Signs the specified data with the private key, returning the X.509 signature. """ ... def verify_data( issuer_public_key: CertificatePublicKeyTypes, signature_algorithm: bytes, signature: bytes, data: bytes, ): """ Verifies an X.509 signature against the public key and specified data. An `InvalidSignature` exception will be raised if the signature fails to verify. """ ... def identify_signature_algorithm( signature_algorithm: bytes, ) -> tuple[_AllowedHashTypes, padding.PSS | padding.PKCS1v15 | ec.ECDSA | None]: """ Deserializes the DER representation of an X.509 signature algorithm into the hash algorithm and its parameters. """ ... ```

I'm making a ~simple and direct translation here, but from what I see in the existing Python APIs maybe a class with methods would be preferred:

class SignatureAlgorithm:
    """
    An X.509 signature algorithm.
    """

    def __init__(
        self,
        oid: ObjectIdentifier,
        parameters: padding.PSS | padding.PKCS1v15 | ec.ECDSA | None,
    ):
        """
        Creates an signature algorithm object from its parsed components.
        This does not validate that `parameters` has the correct type for `oid`.
        """
        ...

    # PROPERTIES (these already exist in each X.509 object,
    # with a `signature_` prefix)

    @property
    def oid(self) -> ObjectIdentifier:
        """
        Returns the `ObjectIdentifier` for the algorithm to be used.
        This will be one of the OIDs from `SignatureAlgorithmOID`.
        """
        ...

    @property 
    def parameters(self) -> padding.PSS | padding.PKCS1v15 | ec.ECDSA | None:
        """
        Returns the parameters of the signature algorithm.
        For RSA signatures it will return either a `PKCS1v15` or `PSS` object.
        For ECDSA signatures it will return an `ECDSA` object.
        For EdDSA and DSA signatures it will return None.
        """
        ...

    @property 
    def hash_algorithm(self) -> HashAlgorithm | None:
        """
        Returns the `HashAlgorithm` to be used as part of the signature algorithm.
        Can be `None` if signature does not use separate hash (ED25519, ED448).
        """
        ...

    # EXPOSED API

    @static_method
    def create(
        private_key: CertificateIssuerPrivateKeyTypes,
        algorithm: _AllowedHashTypes | None,
        rsa_padding: padding.PSS | padding.PKCS1v15 | None = None,
    ) -> SignatureAlgorithm:
        """
        Prepares a X.509 signature algorithm to use with the specified key.
        This method fails if the passed `algorithm` and `rsa_padding` do
        not match the type of `private_key`.
        """
        ...

    @static_method
    def load(der: bytes) -> SignatureAlgorithm:
        """
        Loads an X.509 signature algorithm from its DER representation.
        """
        ...

    def bytes(self) -> bytes:
        """
        Serializes an X.509 signature algorithm into its DER representation.
        """
        ...

    def sign(
        self,
        private_key: CertificateIssuerPrivateKeyTypes,
        data: bytes,
    ) -> bytes:
        """
        Signs the specified data with the private key, returning the X.509 signature.
        This method fails if the passed key is not compatible with the signature algorithm.
        """
        ...

    def verify(
        self,
        issuer_public_key: CertificatePublicKeyTypes,
        signature: bytes,
        data: bytes,
    ):
        """
        Verifies an X.509 signature against the public key and specified data.
        This method fails if the passed key is not compatible with the signature algorithm.
        An `InvalidSignature` exception will be raised if the signature fails to verify.
        """
        ...

This would allow:

...without having to resort to algorithm-dependent operations. For example, the documentation I linked earlier could now tell users to do this:

x509.SignatureAlgorithm(
    cert_to_check.signature_algorithm_oid,
    cert_to_check.signature_algorithm_parameters,
).verify(
    issuer_public_key,
    cert_to_check.signature,
    cert_to_check.tbs_certificate_bytes,
)

(or x509.Certificate could directly expose a signature_algorithm property rather than exposing the 3 properties separately. and the same with the other objects)

alex commented 2 days ago

Are there other motivations for this besides the two you listed?

In general, working with broken X.509 structures isn't really something we endeavor to support.

For non-X.509 structures that use the same signatures structures, can you give us an example?

obfusk commented 2 days ago

For non-X.509 structures that use the same signatures structures, can you give us an example?

This would also reduce the need for custom code to create and verify APK signatures -- which use X.509 certificates but custom signature formats (though unlike later bespoke formats the legacy format is based on PKCS#7) -- in apksigtool and androguard.

mildsunrise commented 2 days ago

For non-X.509 structures that use the same signatures structures, can you give us an example?

Sure! Off the top of my head: