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

Skip to content

ML-DSA+COSE #115158

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
Open

ML-DSA+COSE #115158

wants to merge 8 commits into from

Conversation

krwq
Copy link
Member

@krwq krwq commented Apr 29, 2025

There are some things left here:

  • MLDsaAsymmetricAlgorithmWrapper should be reconsidered - this likely needs ML-DSA APIs and API review to finalize how APIs should look like but it's currently usable because internal AsymmetricAlgorithm implementation is relaxed and leaks through CoseSigner - tests should require minimal changes to support changes:
    // TODO: There seem to be couple of options here:
    //       - we leave this as is - I think tests might be abusing intended use of this class but this might be ok to not churn code for now
    //       - we make MLDsa implement AsymmetricAlgorithm and remove this wrapper
    //         - AsymmetricAlgorithm needs some re-design: HashAlgorithm should not be part of AA, some/all members should maybe throw and we only use it as a base class - some still make sense: KeySize i.e.
    //         - perhaps all AA API's should be replaced with IDisposable but that seems too relaxed, perhaps AsymmetricAlgorithm2
    //       - we consider making this class public inside COSE, perhaps under a different name (CoseSigningAlgorithm)
    //         and add a public constructor that takes an MLDsaAlgorithm
    //       - if we make this instance tied to specific instance of CoseMessage then:
    //         - it should probably be a weak ref
    //         - we need to make couple of new members taking MLDsa - basically all Verify embedded/non-embedded - see CoseTestSign1/CoseTestMultiSign.GetImplementations()
    //       - tests can just branch on key's IDisposable member if needed so we can cheaply change this
    //       - we can consider changing COSE to allow CoseSigner

Copy link

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

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.


public static IEnumerable<CoseTestSign1> GetImplementations()
{
yield return new(true, "Sign/VerifyEmbedded(byte[])",
Copy link
Member Author

Choose a reason for hiding this comment

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

note for reviewers: this and equivalent in CoseTestMultiSign is a core of testing all overloads


if (msg.ProtectedHeaders.TryGetValue(CoseHeaderLabel.KeyIdentifier, out var keyIdentifier))
{
Assert.Equal(32 /* can't correlate that with anything on the draft example, it doesn't seem to match kid */, keyIdentifier.GetValueAsBytes().Length);
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm still confused where is this value coming from - it supposedly should be somewhere in the draft

Comment on lines +28 to +30
Dispose(true);
_disposed = true;
GC.SuppressFinalize(this);
Copy link
Member

Choose a reason for hiding this comment

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

Since the type hierarchy is internal and we know we won't use finalizers, we can use DisposeCore instead of Dispose(bool), if preferred.

Copy link
Member Author

Choose a reason for hiding this comment

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

I left as is, I like using single pattern everywhere

@krwq krwq marked this pull request as ready for review April 30, 2025 14:23
if (PlatformDetection.IsAndroid)
{
// Android supports PSS at the algorithms layer, but does not support it
// being used in cert chains.
Copy link
Member

Choose a reason for hiding this comment

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

If the support check is about cert chains, then the class or method should say that. Since the class seems like the wrong answer here, the method (and property) should.

@@ -10,16 +10,17 @@
<EnableDefaultPackageReadmeFile>false</EnableDefaultPackageReadmeFile>
</PropertyGroup>

<PropertyGroup>
<!-- PQC is new in .NET 10, so we need to build some pieces of it for .NET 9, .NET Standard, and .NET Framework -->
<BuildPqc Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))">true</BuildPqc>
Copy link
Member

Choose a reason for hiding this comment

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

Microsoft.Bcl.Cryptography exposes the types to netstandard 2.0... why wouldn't we expose the new members here to netstandard 2.0? (Unlike SignedCms, COSE doesn't have to worry about netfx compat/limitations)

Copy link
Member

Choose a reason for hiding this comment

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

As far as I can see BuildPqc is only used to decide if Microsoft.Bcl.Cryptography needs to be a referenced.

Since it is used just in this case I would get rid of BuildPqc and put the condition directly on the ItemGroup.

<Compile Include="$(CommonPath)System\Experimentals.cs" Link="Common\Experimentals.cs" />
<Compile Include="$(CommonPath)System\HashCodeRandomization.cs" Link="Common\System\HashCodeRandomization.cs" />
<Compile Include="$(CommonPath)System\Memory\PointerMemoryManager.cs" Link="Common\System\Memory\PointerMemoryManager.cs" />
<Compile Include="$(CommonPath)System\Security\Cryptography\IncrementalHash.netfx.cs" Link="Common\System\Security\Cryptography\IncrementalHash.cs" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'" />
Copy link
Member

Choose a reason for hiding this comment

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

I feel like we usually put conditions on ItemGroup, not individual items..

Comment on lines 18 to +21
<ItemGroup>
<Compile Include="$(CommonPath)System\HashCodeRandomization.cs"
Link="Common\System\HashCodeRandomization.cs" />
<Compile Include="$(CommonPath)System\Memory\PointerMemoryManager.cs"
Link="Common\System\Memory\PointerMemoryManager.cs" />
<Compile Include="$(CommonPath)System\Security\Cryptography\IncrementalHash.netfx.cs"
Link="Common\System\Security\Cryptography\IncrementalHash.cs"
Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'" />
<Compile Include="$(LibrariesProjectRoot)System.Formats.Cbor\src\System\Formats\Cbor\CborInitialByte.cs"
Link="System\Formats\Cbor\CborInitialByte.cs" />
<Compile Include="$(CommonPath)System\Experimentals.cs" Link="Common\Experimentals.cs" />
<Compile Include="$(CommonPath)System\HashCodeRandomization.cs" Link="Common\System\HashCodeRandomization.cs" />
<Compile Include="$(CommonPath)System\Memory\PointerMemoryManager.cs" Link="Common\System\Memory\PointerMemoryManager.cs" />
Copy link
Member

Choose a reason for hiding this comment

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

Why are you restyling this? Is it making it consistent with something else? (AFAIK, the newline style is more prevalent)

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't recall explicitly changing it so presumably VS did that on my behalf and I didn't notice - will fix


action("RSA-PKCS1", CoseTestKeyType.RSAPkcs1, HashAlgorithmName.SHA256);

if (PlatformSupport.IsRsaPssSupported)
Copy link
Member

Choose a reason for hiding this comment

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

The helper described that it was answering if PSS works in certs, but this is using it as whether PSS works at all. So it'll be wrong on Android.

Comment on lines +59 to +60
// we use 3 suffix because if PSS is not supported then ML-DSA will not be as well
action("RSA-PKCS1-3", CoseTestKeyType.RSAPkcs1, HashAlgorithmName.SHA256);
Copy link
Member

Choose a reason for hiding this comment

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

If PSS doesn't work then why do anything at all? I'd expect "if PSS works, try PSS. If it doesn't, either check we get the right kind of exception, or ignore it"

Comment on lines +71 to +73
// we currently need 5 keys for the tests
action("ECDsa-2", CoseTestKeyType.ECDsa, HashAlgorithmName.SHA256);
action("RSA-PKCS1-2", CoseTestKeyType.RSAPkcs1, HashAlgorithmName.SHA256);
Copy link
Member

Choose a reason for hiding this comment

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

If ML-DSA is supported, we have 6. Else we have 5. Seems like just supporting unbalanced is the way to go.

Comment on lines +20 to +22
private CoseTestMultiSign(bool isEmbedded, string label,
Func<CoseTestKey, byte[], byte[]> signFirstImpl,
Action<CoseMultiSignMessage, CoseTestKey, byte[]> addSignatureImpl,
Copy link
Member

Choose a reason for hiding this comment

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

Chop consistently

Suggested change
private CoseTestMultiSign(bool isEmbedded, string label,
Func<CoseTestKey, byte[], byte[]> signFirstImpl,
Action<CoseMultiSignMessage, CoseTestKey, byte[]> addSignatureImpl,
private CoseTestMultiSign(
bool isEmbedded,
string label,
Func<CoseTestKey, byte[], byte[]> signFirstImpl,
Action<CoseMultiSignMessage, CoseTestKey, byte[]> addSignatureImpl,

};
MLDsa mldsaKey = MLDsa.GenerateKey(algorithm);
CoseSigner mldsaSigner = new(mldsaKey, protectedHeaders: new CoseHeaderMap { [CoseHeaderLabel.KeyIdentifier] = CoseHeaderValue.FromBytes(Encoding.UTF8.GetBytes(keyId)) });
return (mldsaKey, mldsaSigner.Key, mldsaSigner);
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, this feels dirty.

I think we want to just say "we added a new feature and that if you use it, .Key will be null" (so change the nullability of CoseSigner.Key)... that's the approach that is currently being tried out for CmsSigner. And then we just set a field/property for the ML-DSA (et al) key that we can carry through to signing.

That means that we then want something like CoseVerifier. Or maybe we should make a CoseKey class, if we can serialize it to COSE_Key (https://github.com/dotnet/runtime/blob/26c9fb8e747ec936ff93b27e8c4de7e6bc43ec70/src/libraries/System.Formats.Cbor/tests/CoseKeyHelpers.cs) that sounds like goodness. Then we overload Sign and Verify once (per current method) with CoseKey, and just need to make sure CoseKey has ctors/factories for the supported algorithms.

public sealed class CoseKey
{
    private CoseKey();

    public static CoseKey FromKey(RSA key, RSASignaturePadding, HashAlgorithmName);
    public static CoseKey FromKey(ECDsa key, HashAlgorithmName);
    public static CoseKey FromKey(MLDsa key);

    public int AlgorithmId { get; }
    public int KeyType { get; }

    public byte[] ExportCoseKey();
    public static CoseKey ImportCoseKey(ReadOnlySpan<byte> source);
}

(for example)

And that CoseSigner should really have been CoseKey plus protected and unprotected headers.

Verify only needs the CoseKey, not the full signer. And, voila, we've broken up with AsymmetricAlgorithm.

--

Import/export to COSE_Key format can, of course, be put off until someone asks for it. But keeping it in mind helps name the type. Similarly, we could make the KTY and Alg values internal unless we have a reason to expose them. I'm just thinking forward.

@@ -129,5 +159,6 @@ private static bool CheckIfVbsAvailable()

private static bool? s_isVbsAvailable;
internal static bool IsVbsAvailable => s_isVbsAvailable ??= CheckIfVbsAvailable();
internal static bool IsRsaPssSupported { get; } = CheckIfRsaPssSupported();
Copy link
Member

Choose a reason for hiding this comment

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

This is going to eagerly check if RSA-PSS is supported in the static constructor which affects test discovery times. We should do this lazily like we do for IsVbsAvailable.


internal PureDataToBeSignedBuilder()
{
_stream = new();
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't use new() here.

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