diff options
| author | StevenWdV <stevenwdv@gmail.com> | 2021-11-20 18:09:09 +0100 |
|---|---|---|
| committer | StevenWdV <stevenwdv@gmail.com> | 2021-11-22 12:37:33 +0100 |
| commit | 8878d8705f0b0fcddb3979194340ca39df897580 (patch) | |
| tree | 6c920d0b9d40584dfe6bf7e5b2e865acff72e72f /wrappers | |
| parent | b8d368b93479233a8ecbeba3daf4b10bee8f0a4a (diff) | |
Add C bindings and a C# wrapper
Diffstat (limited to 'wrappers')
| -rw-r--r-- | wrappers/csharp/.gitignore | 4 | ||||
| -rw-r--r-- | wrappers/csharp/Discovery.cs | 135 | ||||
| -rw-r--r-- | wrappers/csharp/EduVpnCommon.csproj | 27 | ||||
| -rw-r--r-- | wrappers/csharp/EduVpnCommon.sln | 22 | ||||
| -rw-r--r-- | wrappers/csharp/EduVpnCommonTests/EduVpnCommonTests.csproj | 19 | ||||
| -rw-r--r-- | wrappers/csharp/EduVpnCommonTests/VerifyTests.cs | 72 | ||||
| -rw-r--r-- | wrappers/csharp/Makefile | 4 |
7 files changed, 283 insertions, 0 deletions
diff --git a/wrappers/csharp/.gitignore b/wrappers/csharp/.gitignore new file mode 100644 index 0000000..115bb34 --- /dev/null +++ b/wrappers/csharp/.gitignore @@ -0,0 +1,4 @@ +bin/ +obj/ +/packages/ +.vs/ diff --git a/wrappers/csharp/Discovery.cs b/wrappers/csharp/Discovery.cs new file mode 100644 index 0000000..0d6fef1 --- /dev/null +++ b/wrappers/csharp/Discovery.cs @@ -0,0 +1,135 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +[assembly: InternalsVisibleTo("EduVpnCommonTests")] + +namespace EduVpnCommon +{ + public static class Discovery + { + /// <summary> + /// Verifies the signature on the JSON server_list.json/organization_list.json file. + /// If the function returns the signature is valid for the given file type. + /// </summary> + /// <param name="signatureFileContent">.minisig signature file contents.</param> + /// <param name="signedJson">Signed .json file contents.</param> + /// <param name="expectedFileName">The file type to be verified, one of <c>server_list.json</c> or <c>organization_list.json</c>.</param> + /// <param name="minSignTime">Minimum time for signature. Should be set to at least the time in a previously retrieved file.</param> + /// <exception cref="ArgumentException">If <c>expectedFileName</c> is not one of the allowed valued.</exception> + /// <exception cref="VerifyException">If signature verification fails.</exception> + public static void Verify( + ArraySegment<byte> signatureFileContent, + ArraySegment<byte> signedJson, + string expectedFileName, + DateTimeOffset minSignTime) + { + VerifyReturnCode result; + { + using var signatureHandle = GoSliceHandle.FromArray(signatureFileContent); + using var jsonHandle = GoSliceHandle.FromArray(signedJson); + using var expectedFileHandle = GoSliceHandle.FromString(expectedFileName); + + result = Verify(signatureHandle.Slice, jsonHandle.Slice, expectedFileHandle.Slice, + (ulong) minSignTime.ToUnixTimeSeconds()); + } + + if (result != VerifyReturnCode.Ok) + { + if (result == VerifyReturnCode.ErrUnknownExpectedFileName) + throw new ArgumentException("unknown name", nameof(expectedFileName)); + throw new VerifyException((VerifyErrorCode) result); + } + } + + /// <summary>Use for testing only, see Go documentation.</summary> + internal static void InsecureTestingSetExtraKey(string keyString) + { + using var keyHandle = GoSliceHandle.FromString(keyString); + InsecureTestingSetExtraKey(keyHandle.Slice); + } + + const string VerifyLibName = "eduvpn_verify"; + + [DllImport(VerifyLibName)] + static extern VerifyReturnCode Verify(GoSlice signatureFileContent, GoSlice signedJson, GoSlice expectedFileName, ulong minSignTime); + + [DllImport(VerifyLibName)] static extern void InsecureTestingSetExtraKey(GoSlice keyStr); + + class GoSliceHandle : IDisposable + { + GCHandle gcHandle_; + readonly GoSlice slice_; + + public GoSlice Slice => gcHandle_.IsAllocated + ? slice_ + : throw new InvalidOperationException("Handle was disposed"); + + GoSliceHandle(Array array, int offset, int count) + { + gcHandle_ = GCHandle.Alloc(array, GCHandleType.Pinned); + var elemSize = Marshal.SizeOf(array.GetType().GetElementType()!); + slice_ = new GoSlice(gcHandle_.AddrOfPinnedObject() + offset * elemSize, count * elemSize); + } + + public static GoSliceHandle FromArray<T>(ArraySegment<T> segment) where T : struct => + new GoSliceHandle(segment.Array!, segment.Offset, segment.Count); + + public static GoSliceHandle FromString(string str) => + FromArray(new ArraySegment<byte>(Encoding.UTF8.GetBytes(str))); + + public void Dispose() => gcHandle_.Free(); + } + + readonly struct GoSlice + { + readonly IntPtr data_; + readonly long len_, cap_; + + public GoSlice(IntPtr data, long len, long cap) + { + data_ = data; + len_ = len; + cap_ = cap; + } + + public GoSlice(IntPtr data, long len) : this(data, len, len) { } + } + } + + public class VerifyException : Exception + { + public VerifyErrorCode Code { get; } + + internal VerifyException(VerifyErrorCode code) : base(GetMessage(code)) => Code = code; + + static string GetMessage(VerifyErrorCode code) => code switch + { + VerifyErrorCode.ErrInvalidSignature => "invalid signature", + VerifyErrorCode.ErrInvalidSignatureUnknownKey => "invalid signature (unknown key)", + VerifyErrorCode.ErrTooOld => "replay of previous signature (rollback)", + _ => $"unknown verify error ({code})" + }; + } + + public enum VerifyErrorCode + { + /// <summary>Signature is invalid (for the expected file type).</summary> + ErrInvalidSignature = VerifyReturnCode.ErrUnknownExpectedFileName + 1, + + /// <summary>Signature was created with an unknown key and has not been verified.</summary> + ErrInvalidSignatureUnknownKey, + + /// <summary>Signature has a timestamp lower than the specified minimum signing time.</summary> + ErrTooOld + } + + enum VerifyReturnCode + { + Ok, + ErrUnknownExpectedFileName + + //... + } +}
\ No newline at end of file diff --git a/wrappers/csharp/EduVpnCommon.csproj b/wrappers/csharp/EduVpnCommon.csproj new file mode 100644 index 0000000..1bbbc23 --- /dev/null +++ b/wrappers/csharp/EduVpnCommon.csproj @@ -0,0 +1,27 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + <LangVersion>8</LangVersion> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <Compile Remove="EduVpnCommonTests/**" /> + </ItemGroup> + + <ItemGroup> + <EmbeddedResource Remove="EduVpnCommonTests/**" /> + </ItemGroup> + + <ItemGroup> + <None Remove="EduVpnCommonTests/**" /> + <None Condition="Exists('../../exports/eduvpn_verify.dll')" Include="../../exports/eduvpn_verify.dll"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Condition="Exists('../../exports/eduvpn_verify.so')" Include="../../exports/eduvpn_verify.so"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + </ItemGroup> + +</Project> diff --git a/wrappers/csharp/EduVpnCommon.sln b/wrappers/csharp/EduVpnCommon.sln new file mode 100644 index 0000000..0d59e2f --- /dev/null +++ b/wrappers/csharp/EduVpnCommon.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EduVpnCommon", "EduVpnCommon.csproj", "{D95F43A0-EF74-41FD-A526-8987151ABB30}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EduVpnCommonTests", "EduVpnCommonTests/EduVpnCommonTests.csproj", "{B7A75F1A-C83F-4FAB-AD16-E9DB7EF58EBC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D95F43A0-EF74-41FD-A526-8987151ABB30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D95F43A0-EF74-41FD-A526-8987151ABB30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D95F43A0-EF74-41FD-A526-8987151ABB30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D95F43A0-EF74-41FD-A526-8987151ABB30}.Release|Any CPU.Build.0 = Release|Any CPU + {B7A75F1A-C83F-4FAB-AD16-E9DB7EF58EBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7A75F1A-C83F-4FAB-AD16-E9DB7EF58EBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7A75F1A-C83F-4FAB-AD16-E9DB7EF58EBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7A75F1A-C83F-4FAB-AD16-E9DB7EF58EBC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/wrappers/csharp/EduVpnCommonTests/EduVpnCommonTests.csproj b/wrappers/csharp/EduVpnCommonTests/EduVpnCommonTests.csproj new file mode 100644 index 0000000..cf58249 --- /dev/null +++ b/wrappers/csharp/EduVpnCommonTests/EduVpnCommonTests.csproj @@ -0,0 +1,19 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <IsPackable>false</IsPackable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> + <PackageReference Include="NUnit" Version="3.13.1" /> + <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> + <PackageReference Include="coverlet.collector" Version="3.0.2" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../EduVpnCommon.csproj" /> + </ItemGroup> + +</Project> diff --git a/wrappers/csharp/EduVpnCommonTests/VerifyTests.cs b/wrappers/csharp/EduVpnCommonTests/VerifyTests.cs new file mode 100644 index 0000000..933f2bc --- /dev/null +++ b/wrappers/csharp/EduVpnCommonTests/VerifyTests.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using System.Linq; +using EduVpnCommon; +using NUnit.Framework; + +namespace EduVpnCommonTests +{ + [TestFixture(TestOf = typeof(Discovery)), Parallelizable] + public class VerifyTests + { + // Relative to e.g. EduVpnCommonTests/bin/Debug/net5.0 + readonly string testDataDir_ = $"{TestContext.CurrentContext.TestDirectory}/../../../../../../test_data"; + + [OneTimeSetUp] + public void OneTimeSetUp() => + Discovery.InsecureTestingSetExtraKey(File.ReadLines($"{testDataDir_}/dummy/public.key").Last()); + + [Test] + [TestCase("dummy/server_list.json.minisig", "dummy/server_list.json", "server_list.json")] + [TestCase("dummy/organization_list.json.minisig", "dummy/organization_list.json", "organization_list.json")] + public void TestValid(string sigFile, string jsonFile, string expectedFileName) => + Discovery.Verify( + File.ReadAllBytes($"{testDataDir_}/{sigFile}"), + File.ReadAllBytes($"{testDataDir_}/{jsonFile}"), + expectedFileName, + DateTimeOffset.UnixEpoch); + + [Test] + [TestCase("dummy/random.txt", "dummy/server_list.json", "server_list.json")] + public void TestInvalidSignature(string sigFile, string jsonFile, string expectedFileName) => + Assert.Throws(Is.TypeOf<VerifyException>() + .And.Property(nameof(VerifyException.Code)).EqualTo(VerifyErrorCode.ErrInvalidSignature), + () => Discovery.Verify( + File.ReadAllBytes($"{testDataDir_}/{sigFile}"), + File.ReadAllBytes($"{testDataDir_}/{jsonFile}"), + expectedFileName, + DateTimeOffset.UnixEpoch)); + + [Test] + [TestCase("dummy/server_list.json.wrong_key.minisig", "dummy/server_list.json", "server_list.json")] + public void TestWrongKey(string sigFile, string jsonFile, string expectedFileName) => + Assert.Throws(Is.TypeOf<VerifyException>() + .And.Property(nameof(VerifyException.Code)).EqualTo(VerifyErrorCode.ErrInvalidSignatureUnknownKey), + () => Discovery.Verify( + File.ReadAllBytes($"{testDataDir_}/{sigFile}"), + File.ReadAllBytes($"{testDataDir_}/{jsonFile}"), + expectedFileName, + DateTimeOffset.UnixEpoch)); + + [Test] + [TestCase("dummy/server_list.json.minisig", "dummy/server_list.json", "server_list.json")] + public void TestOldSignature(string sigFile, string jsonFile, string expectedFileName) => + Assert.Throws(Is.TypeOf<VerifyException>() + .And.Property(nameof(VerifyException.Code)).EqualTo(VerifyErrorCode.ErrTooOld), + () => Discovery.Verify( + File.ReadAllBytes($"{testDataDir_}/{sigFile}"), + File.ReadAllBytes($"{testDataDir_}/{jsonFile}"), + expectedFileName, + DateTimeOffset.MaxValue)); + + [Test] + [TestCase("dummy/other_list.json.minisig", "dummy/other_list.json", "other_list.json")] + public void TestUnknownExpectedFile(string sigFile, string jsonFile, string expectedFileName) => + Assert.Throws<ArgumentException>( + () => Discovery.Verify( + File.ReadAllBytes($"{testDataDir_}/{sigFile}"), + File.ReadAllBytes($"{testDataDir_}/{jsonFile}"), + expectedFileName, + DateTimeOffset.UnixEpoch)); + } +}
\ No newline at end of file diff --git a/wrappers/csharp/Makefile b/wrappers/csharp/Makefile new file mode 100644 index 0000000..293a7a2 --- /dev/null +++ b/wrappers/csharp/Makefile @@ -0,0 +1,4 @@ +.PHONY: test + +test: + dotnet test |
