104080 custom rsapss salt length - #119255
Conversation
…ation/generation with RSA keys. dotnet#104080
…lt customizations
…pported there) Updated reference code
Cose does not support custom salt lengths with PSS.
There was a problem hiding this comment.
Pull Request Overview
This pull request adds support for custom PSS salt length for RSA-based signature operations. The implementation extends the RSASignaturePadding class with a new CreatePss(int saltLength) method and updates related cryptographic components to utilize custom salt lengths.
Key changes include:
- Extended
RSASignaturePaddingto support custom PSS salt lengths with new constants and factory method - Updated all RSA implementation backends (OpenSSL, CNG, BCrypt, etc.) to handle custom salt lengths
- Added comprehensive test coverage for various salt length scenarios
- Updated high-level APIs like
CmsSignerandCertificateRequestto support the new functionality
Reviewed Changes
Copilot reviewed 40 out of 41 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_rsa.h | Added pssSaltLength parameter to RSA sign/verify functions |
| src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_rsa.c | Implemented custom salt length support in OpenSSL backend |
| src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/RSASignaturePadding.cs | Added CreatePss method, PssSaltLength property, and related constants |
| src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/RSAPssX509SignatureGenerator.cs | Updated to use custom salt lengths from padding configuration |
| src/libraries/Common/src/System/Security/Cryptography/RsaPaddingProcessor.cs | Modified PSS encoding/verification to accept custom salt lengths |
| src/libraries/Common/src/System/Security/Cryptography/RsaPaddingProcessor.DigestInfo.cs | Added salt length calculation logic and moved digest info constants |
| src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/RSAPssX509SignatureGeneratorTests.cs | Added comprehensive tests for custom PSS salt lengths |
| src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/CmsSigner.cs | Updated validation to accept any PSS mode padding |
| src/libraries/System.Security.Cryptography.Cose/src/System/Security/Cryptography/Cose/CoseSigner.cs | Added validation to prevent custom salt lengths in COSE |
…tes/CertificateCreation/CertificateRequestChainTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
….Cryptography.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ngProcessor.DigestInfo.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 38 out of 39 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/RSAPssX509SignatureGeneratorTests.cs:1
- In RSASSA-PSS-params,
saltLengthis[2] INTEGER(implicitly-tagged), which is encoded as a context-specific primitive integer, not a constructed sequence. Creating the tag as constructed (..., true) and callingReadSequence(saltTag)is therefore incorrect and should fail to parse on correctly-encoded inputs. Use a primitive context-specific tag and read it as an integer with the expected tag (e.g., via anAsnReader.ReadInteger(expectedTag)overload) rather than reading a sequence.
| { | ||
| #if NET11_0_OR_GREATER | ||
| if (SignaturePadding.PssSaltLength != RSASignaturePadding.PssSaltLengthIsHashLength) | ||
| { | ||
| signatureParameters = GetSignaturePaddingForCustomPssSaltLength(publicKey, hashAlgorithmName); | ||
| } | ||
| #endif | ||
| if (hashAlgorithmName == HashAlgorithmName.SHA1) | ||
| { | ||
| signatureParameters = s_rsaPssSha1Parameters; | ||
| } | ||
| else if (hashAlgorithmName == HashAlgorithmName.SHA256) | ||
| { |
There was a problem hiding this comment.
This may be what is causing tests to fail.
| @@ -531,9 +546,9 @@ public override bool VerifyHash(ReadOnlySpan<byte> hash, ReadOnlySpan<byte> sign | |||
| Debug.Fail($"TryRsaVerificationPrimitive with a pre-allocated buffer"); | |||
| throw new CryptographicException(); | |||
| } | |||
There was a problem hiding this comment.
The prevailing style here is a blank line after } unless followed by }, not sure why this line got deleted.
| } | |
| } | |
| // 11. Set the "unused" bits in the leftmost byte of maskedDB to 0. | ||
| int unusedBits = 8 * emLen - emBits; | ||
|
|
||
| if (unusedBits != 0) |
There was a problem hiding this comment.
The prevailing style is a blank line (or {) before a control flow statement. Not sure why this line got deleted.
| if (unusedBits != 0) | |
| if (unusedBits != 0) |
| } | ||
|
|
||
| } |
|
|
||
| foreach (var saltLength in saltLengths) | ||
| { | ||
| var padding = saltLength is null ? RSASignaturePadding.Pss : RSASignaturePadding.CreatePss(saltLength.Value); |
There was a problem hiding this comment.
| var padding = saltLength is null ? RSASignaturePadding.Pss : RSASignaturePadding.CreatePss(saltLength.Value); | |
| RSASignaturePadding padding = saltLength is null ? | |
| RSASignaturePadding.Pss : | |
| RSASignaturePadding.CreatePss(saltLength.Value); |
|
|
||
| // Assert.NoThrow | ||
| CertificateRequest.LoadSigningRequest(pkcs10, hashAlgorithmName, out _); | ||
| CertificateRequest.LoadSigningRequest(pkcs10, hashAlgorithmName, out _, signerSignaturePadding: padding); |
There was a problem hiding this comment.
Is this edit necessary? The only thing the optional RSASignaturePadding should do is control how a future CreateSigningRequest, CreateSelfSigned, or Create works. It being null here should be allowed in the cases it was in the past...
| pssStructure.ReadEncodedValue(); // Ignore the hash algorithm OID | ||
| pssStructure.ReadEncodedValue(); // Ignore the mask generation function OID | ||
| ReadOnlyMemory<byte> hashAlgorithm = pssStructure.ReadEncodedValue(); // Ignore the hash algorithm OID | ||
| ReadOnlyMemory<byte> mgf = pssStructure.ReadEncodedValue(); // Ignore the mask generation function OID |
There was a problem hiding this comment.
I still don't see the mgf or hashAlgorithm values being verified in this routine?
| // Salt length is 234 | ||
| return "303606092a864886f70d01010a3029a009300706052b0e03021aa116301406092a864886f70d010108300706052b0e03021aa204020200ea".HexToByteArray(); | ||
| } | ||
| } |
| 0x11, 0xf2, 0x10, 0xbd, 0x19, 0x47, 0x75, 0x3c, 0x30, 0xc4, 0x0f, 0xd0, 0x7b, 0xc2, 0x6b, 0x4b | ||
| }; | ||
|
|
||
| RSASignaturePadding padding = RSASignaturePadding.CreatePss(validateWithCorrectSaltLength ? RSASignaturePadding.PssSaltLengthMax : 2); |
There was a problem hiding this comment.
I'd like to see two tests added to show that the sentinel values aren't special (aside from in COSE), something like
[Theory]
[InlineData("SHA256", 256 / 8)]
[InlineData("SHA384", 384 / 8)]
public void PssWithImplicitAndExplicitHashLength(string hashName, int hashLength)
{
byte[] data = TestData.HelloBytes;
HashAlgorithmName hashAlg = new HashAlgorithmName(hashName);
RSASignaturePadding implicitPadding = RSASignaturePadding.Pss;
RSASignaturePadding explicitPadding = RSASignaturePadding.CreatePSS(hashLength);
using (RSA rsa = RSAFactory.Create(TestData.RSA2048Params))
{
byte[] implicitSig = rsa.SignData(data, hashAlg, implicitPadding);
byte[] explicitSig = rsa.SignData(data, hashAlg, explicitPadding);
AssertExtensions.TrueExpression(rsa.VerifyData(data, implicitSig, hashAlg, explicitPadding));
AssertExtensions.TrueExpression(rsa.VerifyData(data, explicitSig, hashAlg, implicitPadding));
if (custom pss is supported)
{
RSASignaturePadding minusOne = RSASignaturePadding.CreatePSS(hashLength - 1);
RSASignaturePadding plusOne = RSASignaturePadding.CreatePSS(hashLength + 1);
AssertExtensions.FalseExpression(rsa.VerifyData(data, implicitSig, hashAlg, minusOne));
AssertExtensions.FalseExpression(rsa.VerifyData(data, implicitSig, hashAlg, plusOne));
}
}
}and a similar test for max-length padding, though it needs to vary on the keysize/max.
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Is there a test that I'm not immediately seeing that shows that calling Sign with a PSS padding sLen too large throws a reasonable exception? And one that shows that calling verify with that same sLen returns false instead of throw? (The desired PssWithMaxAndExplicitHashLength test would (ideally) have a redundant sLen > sLenMax test, but there should be a dedicated one.)
…thmImplementations/RSA/SignVerify.cs Co-authored-by: Jeremy Barton <jbarton@microsoft.com>
| #if NET11_0_OR_GREATER | ||
| if (SignaturePadding.PssSaltLength != RSASignaturePadding.PssSaltLengthIsHashLength) | ||
| { | ||
| signatureParameters = GetSignaturePaddingForCustomPssSaltLength(publicKey, hashAlgorithmName); | ||
| } | ||
| #endif |
| #if NET11_0_OR_GREATER | ||
| if (signaturePadding.Mode == RSASignaturePaddingMode.Pss) | ||
| { | ||
| if (signaturePadding.PssSaltLength != RSASignaturePadding.PssSaltLengthIsHashLength) | ||
| { | ||
| throw new ArgumentException(SR.CoseSignerPssSaltLengthMustBeHashLength, nameof(signaturePadding)); | ||
| } | ||
| } | ||
| #endif |
| #if NET11_0_OR_GREATER | ||
| Debug.Assert(signaturePadding.PssSaltLength == RSASignaturePadding.PssSaltLengthIsHashLength); | ||
| #endif |
| #if NET11_0_OR_GREATER | ||
| return RSASignaturePadding.CreatePss(SaltLength); | ||
| #else |
…/henning-krause/runtime into 104080-custom-rsapss-salt-length
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 38 out of 39 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
src/libraries/Common/src/System/Security/Cryptography/RsaPaddingProcessor.cs:300
- The size check
if (emLen < 2 + hLen + sLen)can overflow whensLenis a large user-supplied value (via CreatePss), which can bypass the guard and lead to large allocations/invalid slicing later. Use subtraction (or long/checked arithmetic) to make the guard overflow-safe.
// 3. if emLen < hLen + sLen + 2, encoding error.
//
// sLen = hLen in this implementation.
if (emLen < 2 + hLen + sLen)
src/libraries/Common/src/System/Security/Cryptography/RsaPaddingProcessor.cs:393
- This guard uses
hLen + sLen + 2, which can overflow for largesLenvalues and incorrectly skip returning false. That can result in negative indexes later in VerifyPss. Make the check overflow-safe (e.g., via subtraction) before proceeding.
// 3. If emLen < hLen + sLen + 2, output "inconsistent" and stop.
if (emLen < hLen + sLen + 2)
{
return false;
}
| { | ||
| Debug.Assert(padding == RSASignaturePadding.Pss); | ||
| // PSS salt length is validated in the RsaSignaturePaddingMode constructor. | ||
| Debug.Assert(padding.PssSaltLength >= RSASignaturePadding.PssSaltLengthMax); | ||
| } |
| Numerics.BigInteger actualSaltLength = saltEntry.ReadInteger(); | ||
| int expectedSaltLength = saltLengthToTest switch | ||
| { | ||
| RSASignaturePadding.PssSaltLengthIsHashLength => hashLength, | ||
| RSASignaturePadding.PssSaltLengthMax => maxSaltLength, | ||
| _ => saltLengthToTest, | ||
| }; | ||
| Assert.Equal(expectedSaltLength, actualSaltLength); |
| private static void ValidatePaddingParameters(HashAlgorithmName hashAlgorithm, RSASignaturePadding padding) | ||
| { | ||
| // Apple does not support custom salt length for the PSS padding | ||
| if (padding.Mode == RSASignaturePaddingMode.Pss && | ||
| (padding.PssSaltLength != RSASignaturePadding.PssSaltLengthIsHashLength && | ||
| padding.PssSaltLength != RsaPaddingProcessor.HashLength(hashAlgorithm))) | ||
| { | ||
| throw new CryptographicException(SR.Cryptography_CustomPssSaltLengthNotSupported); | ||
| } | ||
| } |
| using (RSA rsa = RSA.Create()) | ||
| { | ||
| var requestBuilder = new CertificateRequest("CN=Test", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.CreatePss(customSaltLength)); | ||
| X509Certificate2 cert = requestBuilder.CreateSelfSigned(DateTime.Now, DateTime.Now.AddYears(1)); | ||
|
|
||
| var reader = new AsnReader(cert.RawData, AsnEncodingRules.DER); | ||
| AsnReader sequence = reader.ReadSequence(); | ||
| ReadOnlyMemory<byte> tbsCertificate = sequence.ReadEncodedValue(); | ||
| ReadOnlyMemory<byte> signatureAlgorithm = sequence.ReadEncodedValue(); | ||
| byte[] signature = sequence.ReadBitString(out _); | ||
|
|
||
| int testSaltLength = customSaltLength switch | ||
| { | ||
| RSASignaturePadding.PssSaltLengthMax => 222, | ||
| RSASignaturePadding.PssSaltLengthIsHashLength => 32, | ||
| _ => customSaltLength | ||
| }; | ||
| Assert.True(rsa.VerifyData(tbsCertificate.Span, signature, HashAlgorithmName.SHA256, RSASignaturePadding.CreatePss(testSaltLength))); | ||
| } |
| if (hashAlgorithm == HashAlgorithmName.SHA1) | ||
| return "300D06092A864886F70D01010A3000".HexToByteArray(); | ||
|
|
||
| { | ||
| if (_signaturePadding.PssSaltLength == RSASignaturePadding.PssSaltLengthIsHashLength) | ||
| { | ||
| // sha1WithRSAEncryption with RSASSA-PSS parameters | ||
| return "300D06092A864886F70D01010A3000".HexToByteArray(); | ||
| } | ||
| else if (_signaturePadding.PssSaltLength == 0) | ||
| { | ||
| return "303506092a864886f70d01010a3028a009300706052b0e03021aa116301406092a864886f70d010108300706052b0e03021aa203020100".HexToByteArray(); | ||
| } | ||
| else if (_signaturePadding.PssSaltLength == 1) | ||
| { | ||
| return "303506092a864886f70d01010a3028a009300706052b0e03021aa116301406092a864886f70d010108300706052b0e03021aa203020101".HexToByteArray(); | ||
| } | ||
| else if (_signaturePadding.PssSaltLength == RSASignaturePadding.PssSaltLengthMax) | ||
| { | ||
| // Salt length is 234 | ||
| return "303606092a864886f70d01010a3029a009300706052b0e03021aa116301406092a864886f70d010108300706052b0e03021aa204020200ea".HexToByteArray(); | ||
| } |
|
@bartonjs All comments should now be fixed... Can you review my last changes? Thanks :-) |
| /// <summary> | ||
| /// Specifies the salt length to use for PSS padding. This property is only relevant when the <see cref="Mode"/> is <see cref="RSASignaturePaddingMode.Pss"/>. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// This value must either be a non-negative number or one of the special constants <see cref="PssSaltLengthIsHashLength"/> or <see cref="PssSaltLengthMax"/>. | ||
| /// </remarks> | ||
| public int PssSaltLength { get; } |
| { | ||
| Debug.Assert(padding == RSASignaturePadding.Pss); | ||
| // PSS salt length is validated in the RsaSignaturePaddingMode constructor. | ||
| Debug.Assert(padding.PssSaltLength >= RSASignaturePadding.PssSaltLengthMax); |
|
Workflow state for the Holistic Review Orchestrator. {
"version": 5,
"last_dispatched_commit": "d0d88a24fb7edde8e3b0826d606d91309e18ef35",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "17bbb207a46d1b2ae3741baa62cb88a98e2e9847",
"last_reviewed_commit": "d0d88a24fb7edde8e3b0826d606d91309e18ef35",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "17bbb207a46d1b2ae3741baa62cb88a98e2e9847",
"last_recorded_worker_run_id": "29684886999",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "d0d88a24fb7edde8e3b0826d606d91309e18ef35",
"review_id": 4730678743
}
]
} |
There was a problem hiding this comment.
Holistic Review
Motivation: Justified. Issue #104080 is api-approved and documents a real interoperability gap: .NET hard-codes the PSS salt length to the hash length, so it cannot produce or validate PSS signatures that use a different salt length (e.g. RSA_PSS_SALTLEN_MAX or an explicit value), which other stacks and standards require.
Approach: Sound and consistent with the codebase. RSASignaturePadding.CreatePss(int) plus the PssSaltLengthIsHashLength/PssSaltLengthMax sentinels are stored on the immutable padding object; every backend (OpenSSL, CNG, BCrypt, Android, Apple, plus the managed RsaPaddingProcessor) resolves the sentinels through a single new RsaPaddingProcessor.CalculatePssSaltLength helper, and platforms that cannot honor a custom length (Apple, COSE) throw rather than silently ignore it. This directly follows the implementation checklist bartonjs posted on the approved issue (encode/verify, certificate PssParams, PssParamsAsn rehydration, CMS/COSE, and tests).
Summary: CHANGES_REQUESTED). A crypto-domain maintainer should confirm the salt-length math and per-platform semantics before merge.
Detailed Findings
✅ API Approval — Matches approved shape
The new public surface in ref/System.Security.Cryptography.cs (const int PssSaltLengthIsHashLength = -1, const int PssSaltLengthMax = -2, int PssSaltLength { get; }, static RSASignaturePadding CreatePss(int saltLength)) is byte-for-byte identical to the shape approved by bartonjs (who applied the api-approved label on 2025-06-03) in the issue comment. Namespaces, type, member names, and signatures all match; no extra or missing public API.
✅ Cross-platform completeness — sentinels resolved centrally, unsupported paths throw
Sentinel values (-1/-2) are never leaked to native layers or the managed encoder: each caller resolves them via RsaPaddingProcessor.CalculatePssSaltLength(padding.PssSaltLength, KeySize, hashAlgorithm) before use (RSAOpenSsl, RSACng.SignVerify, RSABCrypt, RSAAndroid, RSAAppleCrypto). Backends that cannot honor a non-hLen salt reject it: Apple's ValidatePaddingParameters (called from both TrySignHash and VerifyHash) throws Cryptography_CustomPssSaltLengthNotSupported, and COSE's GetRSAAlgorithm throws because the spec mandates sLen==hLen. The EncodePss/VerifyPss emLen < 2 + hLen + sLen guard plus the PssSaltLengthTooLarge test protect against over-large explicit salts, and EncodePss moves the salt buffer to the heap when sLen > 128 to avoid an unbounded stackalloc.
✅ Test coverage — thorough and interop-anchored
Tests exercise explicit lengths, PssSaltLengthMax, and hLen equivalence, use externally-generated OpenSSL signatures as known-answer vectors, assert cross-verification between implicit Pss and explicit CreatePss(hLen), verify wrong salt lengths fail, and cover CMS/COSE/CertificateRequest. Coverage is gated by AreCustomSaltLengthsSupportedWithPss (excludes Apple), matching the platform capabilities.
💡 Stale comment in RsaPaddingProcessor.EncodePss
The comment // sLen = hLen in this implementation. (RsaPaddingProcessor.cs, in EncodePss near the emLen < 2 + hLen + sLen check) is now inaccurate since sLen comes from the caller-supplied saltLength. It sits on an unchanged line so I did not attach an inline comment; consider removing it to avoid confusion. Non-blocking.
Note
This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.
Generated by Holistic Review · 275.2 AIC · ⌖ 10.7 AIC · ⊞ 10K
Added support for custom PSS salt length for RSA based signature operations. Resolves #104080.
With this PR, I have extended the RsaSignaturePadding class with a custom salt length as discussed in #104080. I've also updated the CmsSigner and the CertificateRequest class to support this.
CoseSigner did not need functionality updates, because the spec does not support custom salt length. I have added checks to prevent this to be configured on the CoseSigner.
Tests where either added or updated to test the new functionality.
All RSA implementations which support this (MAC doesn't seem to support this) were updated.