1
$\begingroup$

Given a private key using algorithm RSA-PSS with SHA256 is it possible to use this key to sign a JWT using algorith RS256 (RSA with SHA256) instead of PS256 (RSA-PSS with SHA256)?

The German government seems to be of the opinion it is possible and wants us to do this for one of their new tax reporting related APIs.

So far I've tried to generate a JWT using:

  • (java) auth0/java-jwt
  • (nodejs) jsonwebtoken

Neither library would allow this.

I also pushed ChatGTP 3.5 about this topic and it claims it should somehow be possible:

RSA-PSS (Probabilistic Signature Scheme) and RSA (used in RS256) are two different signature schemes. While both are based on RSA, they have different padding schemes and formats for generating signatures. RS256 specifically uses the PKCS#1 v1.5 padding scheme with RSA, whereas RSA-PSS uses a different padding scheme.

In most cases, if a certificate is generated using the RSA-PSS algorithm, it may not be directly compatible with the RS256 algorithm, as they use different padding schemes and signature formats.

However, some cryptographic libraries and frameworks might provide ways to convert between different signature schemes or to extract the RSA key pair from the X.509 certificate for use in other algorithms. If such conversion or extraction methods are available and the resulting RSA key pair is compatible with RS256 (using the appropriate padding scheme), then you could potentially use the RSA-PSS certificate to sign JWT tokens with RS256.

So is it technically possible? Any recommendations for tools that support this? Thanks.

Command used to generate the key pair (details replaced with variables):

openssl req -newkey rsa-pss -new -nodes -x509 -days 3650 \
   -subj "/C=$C/ST=$ST/L=$L/O=$O/OU=$OU/CN=$CN/emailAddress=$email" \
   -pkeyopt rsa_keygen_bits:4096 \
   -pkeyopt rsa_pss_keygen_md:sha256       \
   -pkeyopt rsa_pss_keygen_mgf1_md:sha256  \
   -pkeyopt rsa_pss_keygen_saltlen:32 \
   -sigopt rsa_pss_saltlen:32 \
   -sigopt rsa_padding_mode:pss \
   -sigopt rsa_mgf1_md:sha256 \
   -keyout privateKey.pem \
   -out cert.pem

openssl x509 -pubkey -noout -in cert.pem > publicKey.pem

Edit:

Based on the advise I got here (thanks again) I went with the following:

# create private key in RSA format
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:4096

# create self-signed certificate with RSA-PSS signing algorithm from this private key
openssl req -x509 -new -key private_key.pem -out certificate.pem -days 3650 -sha256 \
    -sigopt rsa_padding_mode:pss \
    -sigopt rsa_pss_saltlen:32

# optionally create public key in RSA format from certificate
openssl x509 -pubkey -noout -in certificate.pem > public_key.pem

# or from private key
openssl rsa -pubout -in private_key.pem -out public_key.pem

The certificate then has the following settings:

$ openssl x509 -in certificate.pem -text | grep Algorithm
Public Key Algorithm: rsaEncryption
Signature Algorithm: rsassaPss

It can now be used to verify RS256 JWTs that have been signed using the private key.

$\endgroup$

2 Answers 2

1
$\begingroup$

In the general case, any RSA key can be used with any padding scheme, or none at all (although not using any padding is usually insecure). A private key isn't intrinsically tied to RSA-PSS or RSA with PKCS#1 padding. It is often recommended to use one or the other (generally PSS, because it's harder to misuse), but in the real world keys designed for TLS are often used with both.

However, in your case you're using private keys in one of the many X.509 formats, and those typically are attached to a particular usage. With the command you've given, the private key has PSS restrictions applied to it, and so it can only be used with RSA-PSS. However, you can use a regular key that's generated with openssl req -newkey rsa for both RSA-PSS and RSA with PKCS#1 padding, and that's typically what's done for most web servers.

Here's an example of signing both ways with such a key:

$ openssl dgst -sha256 -binary /dev/null | openssl pkeyutl -inkey privateKey.pem -sign -pkeyopt rsa_padding_mode:pss -hexdump -pkeyopt digest:sha256
$ openssl pkeyutl -inkey privateKey.pem -in /dev/null -sign -pkeyopt rsa_padding_mode:pkcs1 -hexdump

If you've already generated the key, then you'll need to do some sort of manual extraction to turn the key into an appropriate format. I don't believe OpenSSL provides an easy way to do the conversion, but it might and I'm just not aware of it.

$\endgroup$
3
  • $\begingroup$ thanks for the very helpful response. maybe a quick follow up if I may: The explicit requirement is to share an X.509 certificate using RSA-PSS with them and then sign JWT using RSA (no PSS). Would it also be possible to create such a certificate from an RSA (no PSS) private key? It doesn't look like it from my short research. Thanks! $\endgroup$
    – Basti
    Commented Mar 5 at 10:44
  • 1
    $\begingroup$ You can create a new RSA public key with openssl genrsa that isn't PSS specific and then create or have issued a certificate that's RSA-PSS only, sure. Just pass the existing private key to openssl req. Then you can sign with whatever format you want and they can have an RSA-PSS certificate. However, that seems like an odd requirement and an inappropriate use of the certificate, so I'd counsel the German government against that approach. $\endgroup$
    – bk2204
    Commented Mar 5 at 22:23
  • $\begingroup$ Thanks so much. I'll get my hands dirty with the new knowledge. I'm afraid the German government is immune to good advice. We'll have to suffer through their strange requirements. $\endgroup$
    – Basti
    Commented Mar 6 at 6:19
1
$\begingroup$

Here is an example in Python using your command to create private and public key and the library PyJWT and Cryptography (with OpenSSL 3.2.0 installed):

import jwt
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key

with open("privateKey.pem", "rb") as file:
    private_key = load_pem_private_key(file.read(), password=None)
with open("publicKey.pem", "rb") as file:
    public_key = load_pem_public_key(file.read())

payload = {"foo": "bar"}

encoded = jwt.encode(payload=payload, key=private_key, algorithm="RS256")
print("JWT: " + encoded)
decoded = jwt.api_jwt.decode_complete(jwt=encoded, key=public_key, algorithms=["RS256"])
print("Header:  " + str(decoded["header"]))    # {'alg': 'RS256', 'typ': 'JWT'}
print("Content: " + str(decoded["payload"]))   # {'foo': 'bar'}

As far as I understand the OpenSSL man page, RSASSA-PSS "restrictions" are just additional parameters to be used with a RSA key pair (for signature or encryption/decryption), thus predefining the parameters used in signature and encryption/decryption scenarios. Furthermore, in X.509 it is common to inform your peer of the cryptographic parameters you expect, especially when using parameterized primitives. (For example when using PBKDF2, you need to know the hash, salt, iteration count and expected key length. It would be great to somehow transfer this information in an authenticated manner, if you see the parallels.)

Nevertheless it could be arguable whether it is really necessary to prefer usage of RS256 instead of PS256 to sign the JWT, but it clearly IS possible using the same key material.

-- EDIT

I was also able to produce and verify a RS256 JWT with Java and Auth0's JWT library you mentioned above. As the security system knows more about the key material than it would like to admit, you need to help a bit by adding a converting layer, e.g. by using the following class:

public class IgnoreRsaSsaPss {
    public static RSAPrivateKey stripPssParameters(RSAPrivateKey privateKey) {
        return new RSAPrivateKey() {
            @Override
            public BigInteger getPrivateExponent() {
                return privateKey.getPrivateExponent();
            }

            @Override
            public String getAlgorithm() {
                return privateKey.getAlgorithm();
            }

            @Override
            public String getFormat() {
                return privateKey.getFormat();
            }

            @Override
            public byte[] getEncoded() {
                return privateKey.getEncoded();
            }

            @Override
            public BigInteger getModulus() {
                return privateKey.getModulus();
            }
        };
    }
}

Like this, you strip any unnecessary information. It doesn't even matter that getAlgorithm() returns RSASSA-PSS, interestingly.

-- End EDIT

$\endgroup$
1
  • $\begingroup$ Thanks for that. I just went ahead and generated a new RSA private key and out of that a X.509 cert with signature algorithm RSA-PSS. That worked for me in the end. For those who can't just start over, the java code is certainly helpful. Also good to know that is is technically possible, after all. ;-) $\endgroup$
    – Basti
    Commented Mar 12 at 13:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.