From e544c6fa9e15e7277da79e2464243e90b2706b8c Mon Sep 17 00:00:00 2001 From: StevenWdV Date: Mon, 24 Jan 2022 14:59:25 +0100 Subject: Cleanup Added variables to Makefiles to specify custom exports/ directory; Split exception classes in Java & C#; Added more comments; Renamed library and Go package; Removed real (pure) tests; Added generate_lib.ps1 to generate import .lib for Windows (Swift); Moved built Go libraries to exports/lib/; Switch to hopefully faster Swift GitHub Action. --- wrappers/csharp/Discovery.cs | 86 +++++++++++++++--------- wrappers/csharp/EduVpnCommon.csproj | 57 ++++++++-------- wrappers/csharp/EduVpnCommon.props | 7 ++ wrappers/csharp/EduVpnCommonTests/VerifyTests.cs | 27 ++++---- wrappers/csharp/Makefile | 10 ++- wrappers/csharp/README.md | 15 +++-- 6 files changed, 121 insertions(+), 81 deletions(-) create mode 100644 wrappers/csharp/EduVpnCommon.props (limited to 'wrappers/csharp') diff --git a/wrappers/csharp/Discovery.cs b/wrappers/csharp/Discovery.cs index 183d94f..da4a6ea 100644 --- a/wrappers/csharp/Discovery.cs +++ b/wrappers/csharp/Discovery.cs @@ -1,8 +1,10 @@ using System; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +// Make InsecureTestingSetExtraKey visible to tests [assembly: InternalsVisibleTo("EduVpnCommonTests")] namespace EduVpnCommon @@ -11,12 +13,12 @@ namespace EduVpnCommon { /// /// 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. + /// 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. + /// Minimum time for signature. Should be set to at least the time of the previous signature. /// If expectedFileName is not one of the allowed values. /// If signature verification fails. public static void Verify( @@ -35,11 +37,20 @@ namespace EduVpnCommon (ulong) minSignTime.ToUnixTimeSeconds()); } - if (result != VerifyReturnCode.Ok) + switch (result) { - if (result == VerifyReturnCode.ErrUnknownExpectedFileName) - throw new ArgumentException("unknown name", nameof(expectedFileName)); - throw new VerifyException((VerifyErrorCode) result); + case VerifyReturnCode.Ok: + return; + case VerifyReturnCode.ErrUnknownExpectedFileName: + throw new ArgumentException("unknown expected file name", nameof(expectedFileName)); + case VerifyReturnCode.ErrInvalidSignature: + throw new InvalidSignatureException(); + case VerifyReturnCode.ErrInvalidSignatureUnknownKey: + throw new InvalidSignatureUnknownKeyException(); + case VerifyReturnCode.ErrTooOld: + throw new SignatureTooOldException(); + default: + throw new UnknownVerifyException((sbyte) result); } } @@ -50,13 +61,17 @@ namespace EduVpnCommon InsecureTestingSetExtraKey(keyHandle.Slice); } - const string VerifyLibName = "eduvpn_verify"; + const string LibName = "eduvpn_common"; - [DllImport(VerifyLibName)] + [DllImport(LibName)] static extern VerifyReturnCode Verify(GoSlice signatureFileContent, GoSlice signedJson, GoSlice expectedFileName, ulong minSignTime); - [DllImport(VerifyLibName)] static extern void InsecureTestingSetExtraKey(GoSlice keyStr); + [DllImport(LibName)] static extern void InsecureTestingSetExtraKey(GoSlice keyStr); + /// + /// Safe auto-disposing Go slice handle. + /// Non-copying alternative to `Marshal.AllocHGlobal` etc. + /// class GoSliceHandle : IDisposable { GCHandle gcHandle_; @@ -68,6 +83,7 @@ namespace EduVpnCommon GoSliceHandle(Array array, int offset, int count) { + Debug.Assert(offset <= array.Length && /*prevent overflow:*/ count <= array.Length && offset <= array.Length - count); gcHandle_ = GCHandle.Alloc(array, GCHandleType.Pinned); var elemSize = Marshal.SizeOf(array.GetType().GetElementType()!); slice_ = new GoSlice(gcHandle_.AddrOfPinnedObject() + offset * elemSize, count * elemSize); @@ -76,12 +92,14 @@ namespace EduVpnCommon public static GoSliceHandle FromArray(ArraySegment segment) where T : struct => new GoSliceHandle(segment.Array!, segment.Offset, segment.Count); + /// From string as UTF-8. public static GoSliceHandle FromString(string str) => FromArray(new ArraySegment(Encoding.UTF8.GetBytes(str))); public void Dispose() => gcHandle_.Free(); } + // C-compatible structure readonly struct GoSlice { readonly IntPtr data_; @@ -98,38 +116,42 @@ namespace EduVpnCommon } } - public class VerifyException : Exception + /// Verification failed, do not trust the file. + public abstract class VerifyException : Exception { - public VerifyErrorCode Code { get; } - - internal VerifyException(VerifyErrorCode code) : base(GetMessage(code)) => Code = code; + protected VerifyException(string message) : base(message) { } + } - 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})" - }; + /// Signature is invalid (for the expected file type). + public sealed class InvalidSignatureException : VerifyException + { + public InvalidSignatureException() : base("invalid signature") { } } - public enum VerifyErrorCode + /// Signature was created with an unknown key and has not been verified. + public sealed class InvalidSignatureUnknownKeyException : VerifyException { - /// Signature is invalid (for the expected file type). - ErrInvalidSignature = VerifyReturnCode.ErrUnknownExpectedFileName + 1, + public InvalidSignatureUnknownKeyException() : base("invalid signature (unknown key)") { } + } - /// Signature was created with an unknown key and has not been verified. - ErrInvalidSignatureUnknownKey, + /// Signature timestamp smaller than specified minimum signing time (rollback). + public sealed class SignatureTooOldException : VerifyException + { + public SignatureTooOldException() : base("replay of previous signature (rollback)") { } + } - /// Signature has a timestamp lower than the specified minimum signing time. - ErrTooOld + /// Other unknown error. + public sealed class UnknownVerifyException : VerifyException + { + public UnknownVerifyException(sbyte code) : base($"unknown verify error ({code})") => Debug.Assert(code != 0); } - enum VerifyReturnCode + enum VerifyReturnCode : sbyte { Ok, - ErrUnknownExpectedFileName - - //... + ErrUnknownExpectedFileName, + ErrInvalidSignature, + ErrInvalidSignatureUnknownKey, + ErrTooOld } -} \ No newline at end of file +} diff --git a/wrappers/csharp/EduVpnCommon.csproj b/wrappers/csharp/EduVpnCommon.csproj index 4643da2..587601c 100644 --- a/wrappers/csharp/EduVpnCommon.csproj +++ b/wrappers/csharp/EduVpnCommon.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -13,58 +13,59 @@ - - - + + + + + + - + Condition="!(Exists('$(EXPORTS_LIB_PATH)/windows/amd64/$(LIB_NAME).dll') + Or Exists('$(EXPORTS_LIB_PATH)/windows/386/$(LIB_NAME).dll') + Or Exists('$(EXPORTS_LIB_PATH)/windows/arm/$(LIB_NAME).dll') + Or Exists('$(EXPORTS_LIB_PATH)/windows/arm64/$(LIB_NAME).dll') + Or Exists('$(EXPORTS_LIB_PATH)/linux/amd64/lib$(LIB_NAME).so') + Or Exists('$(EXPORTS_LIB_PATH)/linux/arm/lib$(LIB_NAME).so') + Or Exists('$(EXPORTS_LIB_PATH)/linux/arm64/lib$(LIB_NAME).so'))"> + - - - + Always - + Always - + Always - + Always - + Always - + Always - + Always diff --git a/wrappers/csharp/EduVpnCommon.props b/wrappers/csharp/EduVpnCommon.props new file mode 100644 index 0000000..97c2289 --- /dev/null +++ b/wrappers/csharp/EduVpnCommon.props @@ -0,0 +1,7 @@ + + + + $(SolutionDir)../../exports/lib + eduvpn_common + + diff --git a/wrappers/csharp/EduVpnCommonTests/VerifyTests.cs b/wrappers/csharp/EduVpnCommonTests/VerifyTests.cs index 41faf87..2d4a565 100644 --- a/wrappers/csharp/EduVpnCommonTests/VerifyTests.cs +++ b/wrappers/csharp/EduVpnCommonTests/VerifyTests.cs @@ -14,11 +14,11 @@ namespace EduVpnCommonTests [OneTimeSetUp] public void OneTimeSetUp() => - Discovery.InsecureTestingSetExtraKey(File.ReadLines($"{testDataDir_}/dummy/public.key").Last()); + Discovery.InsecureTestingSetExtraKey(File.ReadLines($"{testDataDir_}/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")] + [TestCase("server_list.json.minisig", "server_list.json", "server_list.json")] + [TestCase("organization_list.json.minisig", "organization_list.json", "organization_list.json")] public void TestValid(string sigFile, string jsonFile, string expectedFileName) => Discovery.Verify( File.ReadAllBytes($"{testDataDir_}/{sigFile}"), @@ -27,7 +27,7 @@ namespace EduVpnCommonTests DateTimeOffset.UnixEpoch); [Test] - [TestCase("dummy/server_list.json.minisig", "dummy/server_list.json", "server_list.json")] + [TestCase("server_list.json.minisig", "server_list.json", "server_list.json")] public void TestValidSegment(string sigFile, string jsonFile, string expectedFileName) { var bytes = new byte[] { 1, 2, 3 }.Concat(File.ReadAllBytes($"{testDataDir_}/{jsonFile}")) @@ -40,10 +40,9 @@ namespace EduVpnCommonTests } [Test] - [TestCase("dummy/random.txt", "dummy/server_list.json", "server_list.json")] + [TestCase("random.txt", "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), + Assert.Throws( () => Discovery.Verify( File.ReadAllBytes($"{testDataDir_}/{sigFile}"), File.ReadAllBytes($"{testDataDir_}/{jsonFile}"), @@ -51,10 +50,9 @@ namespace EduVpnCommonTests DateTimeOffset.UnixEpoch)); [Test] - [TestCase("dummy/server_list.json.wrong_key.minisig", "dummy/server_list.json", "server_list.json")] + [TestCase("server_list.json.wrong_key.minisig", "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), + Assert.Throws( () => Discovery.Verify( File.ReadAllBytes($"{testDataDir_}/{sigFile}"), File.ReadAllBytes($"{testDataDir_}/{jsonFile}"), @@ -62,10 +60,9 @@ namespace EduVpnCommonTests DateTimeOffset.UnixEpoch)); [Test] - [TestCase("dummy/server_list.json.minisig", "dummy/server_list.json", "server_list.json")] + [TestCase("server_list.json.minisig", "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), + Assert.Throws( () => Discovery.Verify( File.ReadAllBytes($"{testDataDir_}/{sigFile}"), File.ReadAllBytes($"{testDataDir_}/{jsonFile}"), @@ -73,7 +70,7 @@ namespace EduVpnCommonTests DateTimeOffset.MaxValue)); [Test] - [TestCase("dummy/other_list.json.minisig", "dummy/other_list.json", "other_list.json")] + [TestCase("other_list.json.minisig", "other_list.json", "other_list.json")] public void TestUnknownExpectedFile(string sigFile, string jsonFile, string expectedFileName) => Assert.Throws( () => Discovery.Verify( @@ -82,4 +79,4 @@ namespace EduVpnCommonTests expectedFileName, DateTimeOffset.UnixEpoch)); } -} \ No newline at end of file +} diff --git a/wrappers/csharp/Makefile b/wrappers/csharp/Makefile index 1761edc..29f9682 100644 --- a/wrappers/csharp/Makefile +++ b/wrappers/csharp/Makefile @@ -1,5 +1,9 @@ .PHONY: build pack test clean +EXPORTS_PATH ?= ../../exports +# Export, see EduVpnCommon.props +export EXPORTS_LIB_PATH ?= $(EXPORTS_PATH)/lib + build: dotnet publish EduVpnCommon.csproj --configuration Release @@ -7,7 +11,11 @@ pack: dotnet pack EduVpnCommon.csproj --configuration Release test: - $(MAKE) -C ../../exports +ifneq ($(EXPORTS_PATH),) +ifneq ($(wildcard $(EXPORTS_PATH)/Makefile),) + $(MAKE) -C "$(EXPORTS_PATH)" +endif +endif dotnet test clean: diff --git a/wrappers/csharp/README.md b/wrappers/csharp/README.md index d4d1d97..32d2330 100644 --- a/wrappers/csharp/README.md +++ b/wrappers/csharp/README.md @@ -3,33 +3,38 @@ ## Requirements You will need to install the [.NET SDK](https://dotnet.microsoft.com/download), which includes the `dotnet` tool. The -wrapper targets .NET Standard 2.0, so which means that at least .NET Core 2.0 is required (.NET 5+ is also fine). For -the tests, .NET 5 or newer is required. +wrapper targets .NET Standard 2.0, which means that at least .NET Core 2.0 is required (.NET 5+ is also fine). For the +tests, .NET 5 is required. ## Build & test First build the shared Go library. Next: -Build `EduVpnCommon`: +Build `EduVpnCommon` (Release): ```shell make ``` -Build as nupkg, including eduvpn_verify library: +Build as nupkg, including shared Go library for all platforms built in `exports/lib/`: ```shell make pack ``` +If you do not build this as part of the full repository, specify `EXPORTS_PATH="" EXPORTS_LIB_PATH="path/to/lib-folder"` +when calling make. + The wrapper targets .NET Standard 2.0, which means that it can be referenced by projects using either .NET Framework 4.6.1+, .NET Core 2.0+, or .NET 5+. Currently, directly referencing the project may not work (with `System.BadImageFormatException`) if you have multiple -dynamic libraries compiled in the `exports` folder. If you instead add the `.nupkg`, e.g. with one of the +dynamic libraries compiled in the `exports/lib/` folder. If you instead add the `.nupkg`, e.g. with one of the methods [here](https://stackoverflow.com/q/43400069) or [here](https://stackoverflow.com/q/10240029), it automatically copies the correct library. +This also means that tests may fail if you have multiple dynamic libraries compiled! + Test: ```shell -- cgit v1.2.3