Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
mildsunrise opened this issue Nov 22, 2024 · 5 comments
Open

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

mildsunrise opened this issue Nov 22, 2024 · 5 comments

Comments

@mildsunrise
Copy link

mildsunrise commented Nov 22, 2024

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
Copy link
Member

alex commented Nov 22, 2024

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

@mildsunrise
Copy link
Author

mildsunrise commented Nov 22, 2024

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
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:

  • verifying and signing X.509 objects (like certificates) that are broken, have "exotic" encodings and so on
  • working with non-X.509 objects that still use the X.509 signature machinery

...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
Copy link
Member

alex commented Nov 24, 2024

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
Copy link

obfusk commented Nov 24, 2024

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
Copy link
Author

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

Sure! Off the top of my head:

  • IKEv2 supports X.509 (referred here as PKIX) signatures via RFC7427:

    The Internet Key Exchange Version 2 (IKEv2) protocol has limited support for the Elliptic Curve Digital Signature Algorithm (ECDSA). [...] This document generalizes IKEv2 signature support to allow any signature method supported by PKIX and also adds signature hash algorithm negotiation. This is a generic mechanism and is not limited to ECDSA; it can also be used with other signature algorithms.

  • SPKAC, a simpler version of the PKCS#10 CSR that (like the PKCS#10 CSR) uses X.509 to self-sign the public key. It almost made its way into HTML5's <keygen> element. (this was the use case that prompted me to suggest the feature)

  • as @obfusk points above, PKCS#7 Cryptographic Message Syntax (CMS), a generic encrypted/signed container for all kinds of content. Forms the basis for S/MIME.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants