bcgit / bc-csharp

BouncyCastle.NET Cryptography Library (Mirror)
https://www.bouncycastle.org/csharp
MIT License
1.64k stars 549 forks source link

Adding a member to an interface breaks compatibility between netstandard2.0 and net6.0 #447

Open StipoR opened 1 year ago

StipoR commented 1 year ago

Description

When using a project or a package that targets netstandard2.0, references BouncyCastle.Cryptography package, and implements some interfaces from BouncyCastle.Cryptography, such as ISigner, in a project that targets net6.0, System.TypeLoadException is thrown with the message: Method does not have an implementation.

Reproduction Steps

Visual Studio solution that reproduces the issue: BouncyCastleCryptographyInterfaceIssue.zip

ClassLibrary1.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="BouncyCastle.Cryptography" Version="2.1.1" />
  </ItemGroup>

</Project>

ClassLibrary1.MySigner.cs:

using Org.BouncyCastle.Crypto;

namespace ClassLibrary1
{
    public class MySigner : ISigner
    {
        public string AlgorithmName => "MyAlgorithmName";

        public void BlockUpdate(byte[] input, int inOff, int inLen)
        {
        }

        public byte[] GenerateSignature()
        {
            return new byte[0];
        }

        public int GetMaxSignatureSize()
        {
            return 0;
        }

        public void Init(bool forSigning, ICipherParameters parameters)
        {
        }

        public void Reset()
        {
        }

        public void Update(byte input)
        {
        }

        public bool VerifySignature(byte[] signature)
        {
            return false;
        }
    }
}

ConsoleApp1.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\ClassLibrary1\ClassLibrary1.csproj" />
  </ItemGroup>

</Project>

ConsoleApp1.Program.cs:

using System;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main()
        {
            var mySignerAlgorithmName = GetMySignerAlgorithmName();

            Console.WriteLine(mySignerAlgorithmName);
        }

        private static string GetMySignerAlgorithmName()
        {
            return new ClassLibrary1.MySigner().AlgorithmName;
        }
    }
}

Expected behavior

MyAlgorithmName is output to the Console.

Actual behavior

An exception is thrown:

System.TypeLoadException
  HResult=0x80131522
  Message=Method 'BlockUpdate' in type 'ClassLibrary1.MySigner' from assembly 'ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation.
  Source=ConsoleApp1
  StackTrace:
   at ConsoleApp1.Program.GetMySignerAlgorithmName() in C:\Users\Stipo\Documents\Visual Studio 2022\Projects\BouncyCastleCryptographyInterfaceIssue\ConsoleApp1\Program.cs:line 17
   at ConsoleApp1.Program.Main() in C:\Users\Stipo\Documents\Visual Studio 2022\Projects\BouncyCastleCryptographyInterfaceIssue\ConsoleApp1\Program.cs:line 9

Regression?

Probably yes, after the member Org.BouncyCastle.Crypto.ISigner.BlockUpdate(ReadOnlySpan input) was added and after other conditional compilation members were added to other interfaces.

Known Workarounds

The Change rules for compatibility state:

DISALLOWED: Adding a member to an interface If you provide an implementation, adding a new member to an existing interface won't necessarily result in compile failures in downstream assemblies. However, not all languages support default interface members (DIMs). Also, in some scenarios, the runtime can't decide which default interface member to invoke. For these reasons, adding a member to an existing interface is considered a breaking change.

The solution is to either remove the problematic interface members (conditionally added ones, such as Org.BouncyCastle.Crypto.ISigner.BlockUpdate(ReadOnlySpan input)) (this would be a breaking change of BouncyCastle.Cryptography API) or provide a default implementation for them as explained in the Tutorial: Update interfaces with default interface methods. For example, the fix for the Org.BouncyCastle.Crypto.ISigner.BlockUpdate(ReadOnlySpan input) would be:

#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
        /// <summary>Update the signer with a span of bytes.</summary>
        /// <param name="input">the span containing the data.</param>
        public void BlockUpdate(ReadOnlySpan<byte> input) => this.BlockUpdate(input.ToArray(), 0, input.Length);
#endif
Gymbotan commented 1 year ago

I have the very similar problem, but with interface IDigest. I created 6 libraries as separated projects (Standard 2.0, Standard 2.1, .Net Core 3.1, .Net Core 5.0, .Net Core 6.0, .Net Core 7.0) with the same implementation of IDigest interface. Then I created console app .Net Core 7.0 and added project reference to each of them (one link at a time). Console app worked only with 6.0 and 7.0 libraries. With others it throwed exception IDigest exception

Then I created console app .Net Core 5.0 and it worked well at least with Standard 2.0 and .Net Core 5.0 libraries.

When I used Portable.BouncyCastle 1.9.0 I didn't have such a problem.

fdub commented 10 months ago

This is also the case for a program targeting net6.0 that references a project targeting netstandard2.1 that in turn references BouncyCastle.Cryptography, although the preprocessor directive states that with NETSTANDARD2_1_OR_GREATER the void BlockUpdate(ReadOnlySpan<byte> input) overload would be included. The reason is that the BouncyCastle.Cryptography package only includes DLLs for net6.0 and netstandard2.0 but not netstandard2.1. Therefore the netstandard2.1 project uses the BouncyCastle netstandard2.0 library without the overload.