From 8878d8705f0b0fcddb3979194340ca39df897580 Mon Sep 17 00:00:00 2001 From: StevenWdV Date: Sat, 20 Nov 2021 18:09:09 +0100 Subject: Add C bindings and a C# wrapper --- wrappers/csharp/.gitignore | 4 + wrappers/csharp/Discovery.cs | 135 +++++++++++++++++++++ wrappers/csharp/EduVpnCommon.csproj | 27 +++++ wrappers/csharp/EduVpnCommon.sln | 22 ++++ .../EduVpnCommonTests/EduVpnCommonTests.csproj | 19 +++ wrappers/csharp/EduVpnCommonTests/VerifyTests.cs | 72 +++++++++++ wrappers/csharp/Makefile | 4 + 7 files changed, 283 insertions(+) create mode 100644 wrappers/csharp/.gitignore create mode 100644 wrappers/csharp/Discovery.cs create mode 100644 wrappers/csharp/EduVpnCommon.csproj create mode 100644 wrappers/csharp/EduVpnCommon.sln create mode 100644 wrappers/csharp/EduVpnCommonTests/EduVpnCommonTests.csproj create mode 100644 wrappers/csharp/EduVpnCommonTests/VerifyTests.cs create mode 100644 wrappers/csharp/Makefile (limited to 'wrappers') 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 + { + /// + /// 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. + /// + /// .minisig signature file contents. + /// Signed .json file contents. + /// The file type to be verified, one of server_list.json or organization_list.json. + /// Minimum time for signature. Should be set to at least the time in a previously retrieved file. + /// If expectedFileName is not one of the allowed valued. + /// If signature verification fails. + public static void Verify( + ArraySegment signatureFileContent, + ArraySegment 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); + } + } + + /// Use for testing only, see Go documentation. + 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(ArraySegment segment) where T : struct => + new GoSliceHandle(segment.Array!, segment.Offset, segment.Count); + + public static GoSliceHandle FromString(string str) => + FromArray(new ArraySegment(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 + { + /// Signature is invalid (for the expected file type). + ErrInvalidSignature = VerifyReturnCode.ErrUnknownExpectedFileName + 1, + + /// Signature was created with an unknown key and has not been verified. + ErrInvalidSignatureUnknownKey, + + /// Signature has a timestamp lower than the specified minimum signing time. + 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 @@ + + + + netstandard2.0 + 8 + enable + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + 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 @@ + + + + net5.0 + false + + + + + + + + + + + + + + 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() + .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() + .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() + .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( + () => 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 -- cgit v1.2.3