Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Signed CMS for SLH-DSA #115310

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

PranavSenthilnathan
Copy link
Member

@PranavSenthilnathan PranavSenthilnathan commented May 5, 2025

This PR forks Sign and Verify into a Pure and Hash mode with the Hash mode being the same as the existing implementation. Additionally it adds support for SHAKE128 and SHAKE256 as digest algorithms in CMS.

Some design decisions:

  • The SLH-DSA CMS spec says that the content-type attribute MUST be present for for SLH-DSA. However for counter-signing , RFC 5652 says that counter-signers must not have a content-type attribute. This PR follows RFC 5652.
  • When SubjectIdentifierType.NoSignature is specified, we will check whether the signing algorithm is SLH-DSA and throw if it is. Otherwise we will just use the hash as the signature as before. If Windows has a different behavior for NetFx, we will just adopt it since we don't have an opinion here.
  • Same policy for CheckHash.
  • It's up to the user to specify and/or check the digest algorithm. We do not place any restrictions on hash strength during signing or verification.
  • NetFx public API stays the same and new APIs are added for .NET and netstandard2.1. OpenSsl cannot be supported downlevel but Windows will likely be. This depends on the cert accessors in Microsoft.Bcl.Cryptography which in turn depends on the Windows PQC APIs, so it is not implemented yet.

CMS for SLH-DSA successfully roundtrips with openssl (message created with SignedCms can be verified with OpenSsl and vice versa).

@ghost
Copy link

ghost commented May 5, 2025

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

1 similar comment
@ghost
Copy link

ghost commented May 5, 2025

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-security, @bartonjs, @vcsjones
See info in area-owners.md if you want to be subscribed.

@vcsjones
Copy link
Member

vcsjones commented May 5, 2025

Are we going to support NET Framework?

SignedCms and all of its associated types type-forward to the .NET Framework implementation. We can't enable it on .NET Framework through System.Security.Cryptography.Pkcs. Whatever work that needs to be done (if any) will need to be done in .NET Framework itself.

[assembly: TypeForwardedTo(typeof(System.Security.Cryptography.Pkcs.SignedCms))]

@vcsjones
Copy link
Member

vcsjones commented May 5, 2025

I am curious if we need to implement any of this down level, since .NET Framework will have its own implementation.

I... think... it probably could make sense to implement all of the new public API surface as #if NET10_0_OR_GREATER and don't implement it down level at all.

  1. >= net462 TFMs are going to get type-forwarded.
  2. <= net9.0 TFMs won't have the new public API. That's okay, they should just move to .NET 10. .NET (Core) users should expect to require updating to get new capabilities.
  3. >= netstandard2.0 this is where... maybe... there is value in doing the OOB. If someone is on netstandard and on Windows, but not .NET Framework and not .NET Core, then down level support might be justified. The one that has come up in the past is UWP.

@PranavSenthilnathan
Copy link
Member Author

  1. <= net9.0 TFMs won't have the new public API. That's okay, they should just move to .NET 10. .NET (Core) users should expect to require updating to get new capabilities.

For Unix, downlevel won't work because we don't have the key accessors on certs for openssl in Microsoft.Bcl.Cryptography. However, I think we can still support Windows downlevel (net8.0 and net9.0) since the accessors will be available for them.

  1. >= netstandard2.0 this is where... maybe... there is value in doing the OOB. If someone is on netstandard and on Windows, but not .NET Framework and not .NET Core, then down level support might be justified. The one that has come up in the past is UWP.

After talking to @bartonjs about this: netstandard2.0 should have the same API as NetFx so that means no public API changes. I think netstandard2.1 is more at our discretion and I just added the public API for now.

@PranavSenthilnathan PranavSenthilnathan changed the title Initial draft of signed CMS for SLH-DSA Signed CMS for SLH-DSA May 7, 2025
@PranavSenthilnathan PranavSenthilnathan marked this pull request as ready for review May 7, 2025 21:23
/// <exception cref="CryptographicException">
/// The public key was invalid, or otherwise could not be imported.
/// </exception>
[ExperimentalAttribute("SYSLIB5006")]
Copy link
Member

@vcsjones vcsjones May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use the const (and UrlFormat, as being introduced in #115412)

[Experimental(Experimentals.PostQuantumCryptographyDiagId)]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@barotnjs I don't know where we landed on this - but shouldn't the experimental be on the class? Like, if we ship the class it would, technically, be an unguarded breaking change for us to remove it, if we needed to.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we put it on the class, but then mark ML-DSA (or whatever) as "not Experimental" while one of the rest still is, we have to be diligent at moving the Experimentals down to each remaining member.

We could also put it on the class until there's one stable member so that we don't end up with "well, we deleted everything, because all of PQC got abandoned by the industry before the specs all went final-stable.... so now we have this empty static class"... but I don't really see that outcome, so I don't feel like the class-level one is warranted.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also put it on the class until there's one stable member so that we don't end up with "well, we deleted everything, because all of PQC got abandoned by the industry before the specs all went final-stable.

More like, "It turns out that extensions are not the right shape for [whatever reason]" but, okay.

/// <summary>
/// Helper methods to access keys on <see cref="X509Certificate2"/>.
/// </summary>
public static class X509CertificateKeyAccessors
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should EB-Never this for .NET 10+?

public static class X509CertificateKeyAccessorsTests
{
[ConditionalFact(typeof(SlhDsa), nameof(SlhDsa.IsSupported))]
public static void GetPublicKey()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are using a single class for all of the algorithm names, the tests here should have SLH-DSA somewhere in the test name (repeat feedback for all tests in fixture)

#else
// Unix: OpenSsl accessors not supported downlevel
// Windows: SlhDsa currently not supported so test won't execute
Assert.Throws<PlatformNotSupportedException>(() => X509CertificateKeyAccessors.GetSlhDsaPrivateKey(null));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this assert even be hit because it's a ConditionalFact?

  • Windows returns false right now....
  • OOB Linux should return false right now...

So I am failing to see when this #else will be hit.

Comment on lines +61 to +64
X509Certificate2 copied = X509CertificateKeyAccessors.CopyWithPrivateKey(cert, privateKey);
AssertExtensions.TrueExpression(copied.HasPrivateKey);

SlhDsa? copiedKey = copied.GetSlhDsaPrivateKey();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should dispose of the cert and the key.

@@ -177,6 +179,8 @@ public sealed override byte[] GetSubjectKeyIdentifier(X509Certificate2 certifica
return (T)(object)new RSACryptoServiceProvider(cspParams);
if (typeof(T) == typeof(DSA))
return (T)(object)new DSACryptoServiceProvider(cspParams);
if (typeof(T) == typeof(SlhDsa))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this if is ever going to be hit because this is the CAPI path and I would be... surprised... if and PQC types worked with CAPI. ECDsa doesn't, either, which is why ECDsa is not here.

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is adding a BOM. We try to keep BOMs out of this repo, if I remember correctly.

// RFC 8702 specifies SHAKE256 in CMS must use 512 bits of output.
private const int OutputSizeBytes = 512 / 8;

private readonly Shake256 _shake256;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

YESSS. Finally some use of SHAKE!

Note that we did not OOB any of the SHA-3 APIs. So, if you think this is going to be needed to make CMS work for .NET Standard or .NET 9, then the Shake API's won't be available.

// The spec (as of May 5, 2025) has strength requirements on the hash, but we will
// not enforce them here. If the callers wants to enforce them, they can do so by themselves.

SlhDsa? publicKey = certificate.GetSlhDsaPublicKey();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We didn't get set a good example in the other algorithms, but this should be disposed (The classic-crypto algs can be cleaned up in a separate PR. Maybe we should make an issue for that?)

public static class X509CertificateKeyAccessorsTests
{
[ConditionalFact(typeof(SlhDsa), nameof(SlhDsa.IsSupported))]
public static void GetPublicKey()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public static void GetPublicKey()
public static void GetSlhDsaPublicKey()

{
Assert.NotNull(certKey);
AssertExtensions.SequenceEqual(SlhDsaTestData.IetfSlhDsaSha2_128sPublicKeyValue, certKey.ExportSlhDsaPublicKey());
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shows that it exports the correct public key, but not that it's not a private key.

It should look for a CryptographicException from signing something. (Looking for an exception from exporting the private key isn't quite right, since that would also happen from a non-exportable private key... though X509Certificate2.CreateFromPem shouldn't make one of those).

#else
// Unix: OpenSsl accessors not supported downlevel
// Windows: SlhDsa currently not supported so test won't execute
Assert.Throws<PlatformNotSupportedException>(() => X509CertificateKeyAccessors.GetSlhDsaPrivateKey(null));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test is already conditional on SlhDsa.IsSupported. So why is it using #if, and why would it expect failure?

{
#if NET10_0_OR_GREATER
string certPem = PemEncoding.WriteString("CERTIFICATE", SlhDsaTestData.IetfSlhDsaSha2_128sCertificate);
using (X509Certificate2 cert = X509Certificate2.CreateFromPem(certPem))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test should also check that GetSlhDsaPublicKey is returning a public-only key when a private key is available.

}

[ConditionalFact(typeof(SlhDsa), nameof(SlhDsa.IsSupported))]
public static void GetPrivateKey()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public static void GetPrivateKey()
public static void GetSlhDsaPrivateKey()

/// <exception cref="CryptographicException">
/// The public key was invalid, or otherwise could not be imported.
/// </exception>
[ExperimentalAttribute("SYSLIB5006")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we put it on the class, but then mark ML-DSA (or whatever) as "not Experimental" while one of the rest still is, we have to be diligent at moving the Experimentals down to each remaining member.

We could also put it on the class until there's one stable member so that we don't end up with "well, we deleted everything, because all of PQC got abandoned by the industry before the specs all went final-stable.... so now we have this empty static class"... but I don't really see that outcome, so I don't feel like the class-level one is warranted.

signatureParameters = null;

// If there's no private key, fall back to the public key for a "no private key" exception.
SlhDsa? signingKey = key as SlhDsa ??
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise, more algorithms need to be disposed (and cleanup for the others can happen separately)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just have to be careful here to not dispose of the key if it was passed in externally.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// If there's no private key, fall back to the public key for a "no private key" exception.

Seems like a roundabout way of doing it... can we just throw here ourselves or do we prefer the exception coming from the cert?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this has a type forward to .NET Framework, we try to be exception compatible with .NET Framework, and this is what .NET Framework does.

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

Successfully merging this pull request may close these issues.

3 participants