diff options
| author | StevenWdV <stevenwdv@gmail.com> | 2022-01-24 14:59:25 +0100 |
|---|---|---|
| committer | StevenWdV <stevenwdv@gmail.com> | 2022-01-24 16:24:57 +0100 |
| commit | e544c6fa9e15e7277da79e2464243e90b2706b8c (patch) | |
| tree | de6613747e0e34a799089d4677f9833a85748712 /wrappers | |
| parent | aab2e4b966c82b67eb0e204060e5ea6cd4ea15cf (diff) | |
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.
Diffstat (limited to 'wrappers')
33 files changed, 421 insertions, 231 deletions
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 { /// <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. + /// 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> + /// <param name="minSignTime">Minimum time for signature. Should be set to at least the time of the previous signature.</param> /// <exception cref="ArgumentException">If <c>expectedFileName</c> is not one of the allowed values.</exception> /// <exception cref="VerifyException">If signature verification fails.</exception> 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); + /// <summary> + /// Safe auto-disposing Go slice handle. + /// Non-copying alternative to `Marshal.AllocHGlobal` etc. + /// </summary> 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<T>(ArraySegment<T> segment) where T : struct => new GoSliceHandle(segment.Array!, segment.Offset, segment.Count); + /// <summary>From string as UTF-8.</summary> public static GoSliceHandle FromString(string str) => FromArray(new ArraySegment<byte>(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 + /// <summary>Verification failed, do not trust the file.</summary> + 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})" - }; + /// <summary>Signature is invalid (for the expected file type).</summary> + public sealed class InvalidSignatureException : VerifyException + { + public InvalidSignatureException() : base("invalid signature") { } } - public enum VerifyErrorCode + /// <summary>Signature was created with an unknown key and has not been verified.</summary> + public sealed class InvalidSignatureUnknownKeyException : VerifyException { - /// <summary>Signature is invalid (for the expected file type).</summary> - ErrInvalidSignature = VerifyReturnCode.ErrUnknownExpectedFileName + 1, + public InvalidSignatureUnknownKeyException() : base("invalid signature (unknown key)") { } + } - /// <summary>Signature was created with an unknown key and has not been verified.</summary> - ErrInvalidSignatureUnknownKey, + /// <summary>Signature timestamp smaller than specified minimum signing time (rollback).</summary> + public sealed class SignatureTooOldException : VerifyException + { + public SignatureTooOldException() : base("replay of previous signature (rollback)") { } + } - /// <summary>Signature has a timestamp lower than the specified minimum signing time.</summary> - ErrTooOld + /// <summary>Other unknown error.</summary> + 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 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> @@ -13,58 +13,59 @@ <ItemGroup> <Compile Remove="EduVpnCommonTests/**" /> - </ItemGroup> - - <ItemGroup> <EmbeddedResource Remove="EduVpnCommonTests/**" /> + <None Remove="EduVpnCommonTests/**" /> </ItemGroup> + <!-- Include EXPORTS_LIB_PATH, LIB_NAME definitions --> + <ImportGroup Label="PropertySheets"> + <Import Project="EduVpnCommon.props" /> + </ImportGroup> + <Target Name="Build library for current OS" BeforeTargets="PrepareForBuild" - Condition="!(Exists('../../exports/windows/amd64/eduvpn_verify.dll') - Or Exists('../../exports/windows/386/eduvpn_verify.dll') - Or Exists('../../exports/windows/arm/eduvpn_verify.dll') - Or Exists('../../exports/windows/arm64/eduvpn_verify.dll') - Or Exists('../../exports/linux/amd64/libeduvpn_verify.so') - Or Exists('../../exports/linux/arm/libeduvpn_verify.so') - Or Exists('../../exports/linux/arm64/libeduvpn_verify.so'))"> - <Message Text="Shared eduvpn_verify library not found, you should build that one first" Importance="high" /> + 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'))"> + <Message Text="!! Shared $(LIB_NAME) library not found, you should build that one first" Importance="high" /> </Target> <ItemGroup> - <None Remove="EduVpnCommonTests/**" /> - <!-- See https://docs.microsoft.com/en-us/nuget/create-packages/supporting-multiple-target-frameworks#architecture-specific-folders and https://docs.microsoft.com/en-us/dotnet/core/rid-catalog --> - <None Condition="Exists('../../exports/windows/amd64/eduvpn_verify.dll')" - Include="../../exports/windows/amd64/eduvpn_verify.dll" Pack="true" PackagePath="runtimes/win-x64/native/"> + <None Condition="Exists('$(EXPORTS_LIB_PATH)/windows/amd64/$(LIB_NAME).dll')" + Include="$(EXPORTS_LIB_PATH)/windows/amd64/$(LIB_NAME).dll" Pack="true" PackagePath="runtimes/win-x64/native/"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> - <None Condition="Exists('../../exports/windows/386/eduvpn_verify.dll')" - Include="../../exports/windows/386/eduvpn_verify.dll" Pack="true" PackagePath="runtimes/win-x86/native/"> + <None Condition="Exists('$(EXPORTS_LIB_PATH)/windows/386/$(LIB_NAME).dll')" + Include="$(EXPORTS_LIB_PATH)/windows/386/$(LIB_NAME).dll" Pack="true" PackagePath="runtimes/win-x86/native/"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> - <None Condition="Exists('../../exports/windows/arm/eduvpn_verify.dll')" - Include="../../exports/windows/arm/eduvpn_verify.dll" Pack="true" PackagePath="runtimes/win-arm/native/"> + <None Condition="Exists('$(EXPORTS_LIB_PATH)/windows/arm/$(LIB_NAME).dll')" + Include="$(EXPORTS_LIB_PATH)/windows/arm/$(LIB_NAME).dll" Pack="true" PackagePath="runtimes/win-arm/native/"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> - <None Condition="Exists('../../exports/windows/arm64/eduvpn_verify.dll')" - Include="../../exports/windows/arm64/eduvpn_verify.dll" Pack="true" PackagePath="runtimes/win-arm64/native/"> + <None Condition="Exists('$(EXPORTS_LIB_PATH)/windows/arm64/$(LIB_NAME).dll')" + Include="$(EXPORTS_LIB_PATH)/windows/arm64/$(LIB_NAME).dll" Pack="true" PackagePath="runtimes/win-arm64/native/"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> - <None Condition="Exists('../../exports/linux/amd64/libeduvpn_verify.so')" - Include="../../exports/linux/amd64/libeduvpn_verify.so" Pack="true" PackagePath="runtimes/linux-x64/native/"> + <None Condition="Exists('$(EXPORTS_LIB_PATH)/linux/amd64/lib$(LIB_NAME).so')" + Include="$(EXPORTS_LIB_PATH)/linux/amd64/lib$(LIB_NAME).so" Pack="true" PackagePath="runtimes/linux-x64/native/"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> - <None Condition="Exists('../../exports/linux/arm/libeduvpn_verify.so')" - Include="../../exports/linux/arm/libeduvpn_verify.so" Pack="true" PackagePath="runtimes/linux-arm/native/"> + <None Condition="Exists('$(EXPORTS_LIB_PATH)/linux/arm/lib$(LIB_NAME).so')" + Include="$(EXPORTS_LIB_PATH)/linux/arm/lib$(LIB_NAME).so" Pack="true" PackagePath="runtimes/linux-arm/native/"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> - <None Condition="Exists('../../exports/linux/arm64/libeduvpn_verify.so')" - Include="../../exports/linux/arm64/libeduvpn_verify.so" Pack="true" PackagePath="runtimes/linux-arm64/native/"> + <None Condition="Exists('$(EXPORTS_LIB_PATH)/linux/arm64/lib$(LIB_NAME).so')" + Include="$(EXPORTS_LIB_PATH)/linux/arm64/lib$(LIB_NAME).so" Pack="true" PackagePath="runtimes/linux-arm64/native/"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> </ItemGroup> 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <EXPORTS_LIB_PATH Condition="'$(EXPORTS_LIB_PATH)' == ''">$(SolutionDir)../../exports/lib</EXPORTS_LIB_PATH> + <LIB_NAME Condition="'$(LIB_NAME)' == ''">eduvpn_common</LIB_NAME> + </PropertyGroup> +</Project> 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<VerifyException>() - .And.Property(nameof(VerifyException.Code)).EqualTo(VerifyErrorCode.ErrInvalidSignature), + Assert.Throws<InvalidSignatureException>( () => 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<VerifyException>() - .And.Property(nameof(VerifyException.Code)).EqualTo(VerifyErrorCode.ErrInvalidSignatureUnknownKey), + Assert.Throws<InvalidSignatureUnknownKeyException>( () => 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<VerifyException>() - .And.Property(nameof(VerifyException.Code)).EqualTo(VerifyErrorCode.ErrTooOld), + Assert.Throws<SignatureTooOldException>( () => 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<ArgumentException>( () => 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 diff --git a/wrappers/java/Makefile b/wrappers/java/Makefile index 4b7b602..a63aef7 100644 --- a/wrappers/java/Makefile +++ b/wrappers/java/Makefile @@ -1,14 +1,21 @@ .PHONY: build pack test clean +EXPORTS_PATH ?= ../../exports +EXPORTS_LIB_PATH ?= $(EXPORTS_PATH)/lib + build: - mvn --no-transfer-progress compile + mvn --no-transfer-progress compile -DEXPORTS_LIB_PATH="$(EXPORTS_LIB_PATH)" pack: - mvn --no-transfer-progress package + mvn --no-transfer-progress package -DEXPORTS_LIB_PATH="$(EXPORTS_LIB_PATH)" test: - $(MAKE) -C ../../exports - mvn --no-transfer-progress test +ifneq ($(EXPORTS_PATH),) +ifneq ($(wildcard $(EXPORTS_PATH)/Makefile),) + $(MAKE) -C "$(EXPORTS_PATH)" +endif +endif + mvn --no-transfer-progress test -DEXPORTS_LIB_PATH="$(EXPORTS_LIB_PATH)" clean: rm -rf target/ diff --git a/wrappers/java/README.md b/wrappers/java/README.md index 87cdd49..b72d90e 100644 --- a/wrappers/java/README.md +++ b/wrappers/java/README.md @@ -16,7 +16,7 @@ Build `EduVpnCommon`: make ``` -Build as JAR, including eduvpn_verify library: +Build as JAR, including shared Go library: ```shell make pack diff --git a/wrappers/java/pom.xml b/wrappers/java/pom.xml index bd9f721..520bd34 100644 --- a/wrappers/java/pom.xml +++ b/wrappers/java/pom.xml @@ -10,9 +10,11 @@ <name>eduVPN common library</name> <properties> - <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> - <maven.compiler.source>1.8</maven.compiler.source> - <maven.compiler.target>1.8</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <maven.compiler.source>1.8</maven.compiler.source> + <maven.compiler.target>1.8</maven.compiler.target> + + <EXPORTS_LIB_PATH>../../exports/lib</EXPORTS_LIB_PATH> </properties> <dependencies> @@ -40,21 +42,21 @@ <!-- See com.sun.jna.Platform#getNativeLibraryResourcePrefix --> <resource> - <directory>../../exports/linux/amd64</directory> + <directory>${EXPORTS_LIB_PATH}/linux/amd64</directory> <includes> <include>*.so</include> </includes> <targetPath>linux-x86-64</targetPath> </resource> <resource> - <directory>../../exports/linux/arm</directory> + <directory>${EXPORTS_LIB_PATH}/linux/arm</directory> <includes> <include>*.so</include> </includes> <targetPath>linux-arm</targetPath> </resource> <resource> - <directory>../../exports/linux/arm64</directory> + <directory>${EXPORTS_LIB_PATH}/linux/arm64</directory> <includes> <include>*.so</include> </includes> @@ -62,36 +64,38 @@ </resource> <resource> - <directory>../../exports/windows/amd64</directory> + <directory>${EXPORTS_LIB_PATH}/windows/amd64</directory> <includes> <include>*.dll</include> </includes> <targetPath>win32-x86-64</targetPath> </resource> <resource> - <directory>../../exports/windows/386</directory> + <directory>${EXPORTS_LIB_PATH}/windows/386</directory> <includes> <include>*.dll</include> </includes> <targetPath>win32-x86</targetPath> </resource> <resource> - <directory>../../exports/windows/arm</directory> + <directory>${EXPORTS_LIB_PATH}/windows/arm</directory> <includes> <include>*.dll</include> </includes> <targetPath>win32-arm</targetPath> </resource> <resource> - <directory>../../exports/windows/arm64</directory> + <directory>${EXPORTS_LIB_PATH}/windows/arm64</directory> <includes> <include>*.dll</include> </includes> <targetPath>win32-arm64</targetPath> </resource> </resources> + <plugins> <plugin> + <!-- Test adapter --> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> diff --git a/wrappers/java/src/main/java/nl/eduvpn/common/Discovery.java b/wrappers/java/src/main/java/nl/eduvpn/common/Discovery.java index 40d5300..69308c8 100644 --- a/wrappers/java/src/main/java/nl/eduvpn/common/Discovery.java +++ b/wrappers/java/src/main/java/nl/eduvpn/common/Discovery.java @@ -6,38 +6,48 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; public final class Discovery { - private static final NativeApi discovery = Native.load("eduvpn_verify", NativeApi.class); + private static final String LIB_NAME = "eduvpn_common"; + private static final NativeApi discovery = Native.load(LIB_NAME, NativeApi.class); /** * 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. * * @param signature .minisig signature file contents. * @param signedJson Signed .json file contents. * @param expectedFileName The file type to be verified, one of {@code "server_list.json"} or {@code "organization_list.json"}. - * @param minSignTime Minimum time for signature. Should be set to at least the time in a previously retrieved file. + * @param minSignTime Minimum time for signature. Should be set to at least the time of the previous signature. * @throws IllegalArgumentException If {@code expectedFileName} is not one of the allowed values or one of the parameters is empty. * @throws VerifyException If signature verification fails. */ public static void verify(byte[] signature, byte[] signedJson, String expectedFileName, Instant minSignTime) throws VerifyException { - long err = discovery.Verify(NativeApi.GoSlice.make(signature), NativeApi.GoSlice.make(signedJson), - NativeApi.GoSlice.make(expectedFileName.getBytes(StandardCharsets.UTF_8)), - minSignTime.getEpochSecond()); - if (err != 0) { - if (err == 1) throw new IllegalArgumentException("Unknown excpectedFileName"); - throw new VerifyException(err); + byte err = discovery.Verify(NativeApi.GoSlice.fromArray(signature), NativeApi.GoSlice.fromArray(signedJson), + NativeApi.GoSlice.fromString(expectedFileName), minSignTime.getEpochSecond()); + + switch (err) { + case 0: + return; + case 1: + throw new IllegalArgumentException("unknown expected file name"); + case 2: + throw new InvalidSignatureException(); + case 3: + throw new InvalidSignatureUnknownKeyException(); + case 4: + throw new SignatureTooOldException(); + default: + throw new UnknownVerifyException(err); } } - /** - * Use for testing only, see Go documentation. - */ - // package-private + /** Use for testing only, see Go documentation. */ + /*package-private*/ static void insecureTestingSetExtraKey(String keyString) { - discovery.InsecureTestingSetExtraKey(NativeApi.GoSlice.make(keyString.getBytes(StandardCharsets.UTF_8))); + discovery.InsecureTestingSetExtraKey(NativeApi.GoSlice.fromArray(keyString.getBytes(StandardCharsets.UTF_8))); } private interface NativeApi extends Library { + // C-compatible structure @Structure.FieldOrder({"data", "len", "cap"}) class GoSlice extends Structure implements Structure.ByValue { public Pointer data; @@ -49,14 +59,19 @@ public final class Discovery { this.cap = cap; } - public static GoSlice make(byte[] bytes) { + public static GoSlice fromArray(byte[] bytes) { Memory memory = new Memory(bytes.length); memory.write(0, bytes, 0, bytes.length); return new GoSlice(memory, bytes.length, bytes.length); } + + /** From string as UTF-8. */ + public static GoSlice fromString(String str) { + return fromArray(str.getBytes(StandardCharsets.UTF_8)); + } } - long Verify(GoSlice signatureFileContent, GoSlice signedJson, GoSlice expectedFileName, long minSignTime); + byte Verify(GoSlice signatureFileContent, GoSlice signedJson, GoSlice expectedFileName, long minSignTime); void InsecureTestingSetExtraKey(GoSlice keyString); } diff --git a/wrappers/java/src/main/java/nl/eduvpn/common/InvalidSignatureException.java b/wrappers/java/src/main/java/nl/eduvpn/common/InvalidSignatureException.java new file mode 100644 index 0000000..e531206 --- /dev/null +++ b/wrappers/java/src/main/java/nl/eduvpn/common/InvalidSignatureException.java @@ -0,0 +1,8 @@ +package nl.eduvpn.common; + +/** Signature is invalid (for the expected file type). */ +public final class InvalidSignatureException extends VerifyException { + public InvalidSignatureException() { + super("invalid signature"); + } +} diff --git a/wrappers/java/src/main/java/nl/eduvpn/common/InvalidSignatureUnknownKeyException.java b/wrappers/java/src/main/java/nl/eduvpn/common/InvalidSignatureUnknownKeyException.java new file mode 100644 index 0000000..8eaf661 --- /dev/null +++ b/wrappers/java/src/main/java/nl/eduvpn/common/InvalidSignatureUnknownKeyException.java @@ -0,0 +1,8 @@ +package nl.eduvpn.common; + +/** Signature was created with an unknown key and has not been verified. */ +public final class InvalidSignatureUnknownKeyException extends VerifyException { + public InvalidSignatureUnknownKeyException() { + super("invalid signature (unknown key)"); + } +} diff --git a/wrappers/java/src/main/java/nl/eduvpn/common/SignatureTooOldException.java b/wrappers/java/src/main/java/nl/eduvpn/common/SignatureTooOldException.java new file mode 100644 index 0000000..c40c718 --- /dev/null +++ b/wrappers/java/src/main/java/nl/eduvpn/common/SignatureTooOldException.java @@ -0,0 +1,8 @@ +package nl.eduvpn.common; + +/** Signature timestamp smaller than specified minimum signing time (rollback). */ +public final class SignatureTooOldException extends VerifyException { + public SignatureTooOldException() { + super("replay of previous signature (rollback)"); + } +} diff --git a/wrappers/java/src/main/java/nl/eduvpn/common/UnknownVerifyException.java b/wrappers/java/src/main/java/nl/eduvpn/common/UnknownVerifyException.java new file mode 100644 index 0000000..fa76a44 --- /dev/null +++ b/wrappers/java/src/main/java/nl/eduvpn/common/UnknownVerifyException.java @@ -0,0 +1,9 @@ +package nl.eduvpn.common; + +/** Other unknown error. */ +public final class UnknownVerifyException extends VerifyException { + public UnknownVerifyException(byte code) { + super(String.format("unknown verify error (%d)", code)); + assert code != 0; + } +} diff --git a/wrappers/java/src/main/java/nl/eduvpn/common/VerifyException.java b/wrappers/java/src/main/java/nl/eduvpn/common/VerifyException.java index 83dffb1..71ea290 100644 --- a/wrappers/java/src/main/java/nl/eduvpn/common/VerifyException.java +++ b/wrappers/java/src/main/java/nl/eduvpn/common/VerifyException.java @@ -1,9 +1,8 @@ package nl.eduvpn.common; -public class VerifyException extends Exception { - public final long code; //TODO not use plain long - - public VerifyException(long code) { - this.code = code; +/** Verification failed, do not trust the file. */ +public abstract class VerifyException extends Exception { + protected VerifyException(String message) { + super(message); } -}
\ No newline at end of file +} diff --git a/wrappers/java/src/test/java/nl/eduvpn/common/VerifyTests.java b/wrappers/java/src/test/java/nl/eduvpn/common/VerifyTests.java index b4767a5..a82c019 100644 --- a/wrappers/java/src/test/java/nl/eduvpn/common/VerifyTests.java +++ b/wrappers/java/src/test/java/nl/eduvpn/common/VerifyTests.java @@ -1,6 +1,5 @@ package nl.eduvpn.common; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -18,15 +17,15 @@ class VerifyTests { @SuppressWarnings("OptionalGetWithoutIsPresent") @BeforeAll static void oneTimeSetup() throws IOException { - Discovery.insecureTestingSetExtraKey(Files.lines(testDataDir.resolve("dummy/public.key")).reduce((a, b) -> b).get()); + Discovery.insecureTestingSetExtraKey(Files.lines(testDataDir.resolve("public.key")).reduce((a, b) -> b).get()); } @Test void testValid() { assertDoesNotThrow(() -> Discovery.verify( - Files.readAllBytes(testDataDir.resolve("dummy/server_list.json.minisig")), - Files.readAllBytes(testDataDir.resolve("dummy/server_list.json")), + Files.readAllBytes(testDataDir.resolve("server_list.json.minisig")), + Files.readAllBytes(testDataDir.resolve("server_list.json")), "server_list.json", Instant.EPOCH )); @@ -34,45 +33,45 @@ class VerifyTests { @Test void testInvalidSignature() { - Assertions.assertEquals(2, assertThrows(VerifyException.class, () -> + assertThrows(InvalidSignatureException.class, () -> Discovery.verify( - Files.readAllBytes(testDataDir.resolve("dummy/random.txt")), - Files.readAllBytes(testDataDir.resolve("dummy/server_list.json")), + Files.readAllBytes(testDataDir.resolve("random.txt")), + Files.readAllBytes(testDataDir.resolve("server_list.json")), "server_list.json", Instant.EPOCH - )).code); + )); } @Test void testWrongKey() { - assertEquals(3, assertThrows(VerifyException.class, () -> + assertThrows(InvalidSignatureUnknownKeyException.class, () -> Discovery.verify( - Files.readAllBytes(testDataDir.resolve("dummy/server_list.json.wrong_key.minisig")), - Files.readAllBytes(testDataDir.resolve("dummy/server_list.json")), + Files.readAllBytes(testDataDir.resolve("server_list.json.wrong_key.minisig")), + Files.readAllBytes(testDataDir.resolve("server_list.json")), "server_list.json", Instant.EPOCH - )).code); + )); } @Test void testOldSignature() { - assertEquals(4, assertThrows(VerifyException.class, () -> + assertThrows(SignatureTooOldException.class, () -> Discovery.verify( - Files.readAllBytes(testDataDir.resolve("dummy/server_list.json.minisig")), - Files.readAllBytes(testDataDir.resolve("dummy/server_list.json")), + Files.readAllBytes(testDataDir.resolve("server_list.json.minisig")), + Files.readAllBytes(testDataDir.resolve("server_list.json")), "server_list.json", Instant.MAX - )).code); + )); } @Test void testUnknownExpectedFile() { assertThrows(IllegalArgumentException.class, () -> Discovery.verify( - Files.readAllBytes(testDataDir.resolve("dummy/other_list.json.minisig")), - Files.readAllBytes(testDataDir.resolve("dummy/other_list.json")), + Files.readAllBytes(testDataDir.resolve("other_list.json.minisig")), + Files.readAllBytes(testDataDir.resolve("other_list.json")), "other_list.json", Instant.EPOCH )); } -}
\ No newline at end of file +} diff --git a/wrappers/php/.gitignore b/wrappers/php/.gitignore index 4a56e6c..bdaa6e3 100644 --- a/wrappers/php/.gitignore +++ b/wrappers/php/.gitignore @@ -1,4 +1,5 @@ /vendor/ +/lib/* composer.phar .phpunit* *.h diff --git a/wrappers/php/Makefile b/wrappers/php/Makefile index 06c49f1..e0262e9 100644 --- a/wrappers/php/Makefile +++ b/wrappers/php/Makefile @@ -1,25 +1,51 @@ .PHONY: install-header test install-dev-dependencies clean -ifneq (clean,$(MAKECMDGOALS)) -include ../../exports/platform.mk +EXPORTS_PATH ?= ../../exports +EXPORTS_LIB_PATH ?= $(EXPORTS_PATH)/lib +ifneq ($(MAKECMDGOALS),clean) +include $(EXPORTS_PATH)/platform.mk + +# Add phpunit to PATH export PATH := $(abspath vendor/bin):$(PATH) endif +ifeq ($(COPY_LIB),1) +COPY_LIB_DIR = lib +endif + +ifneq ($(COPY_LIB_DIR),) +COPY_LIB_DIR := $(COPY_LIB_DIR)/ +endif + +# Strip / replace elements confusing PHP's limited C parser: __SIZE_TYPE__, _Complex, extern "C" +# Also add FFI_LIB library name, see https://www.php.net/manual/en/ffi.load install-header: - $(MAKE) -C ../../exports +ifneq ($(EXPORTS_PATH),) +ifneq ($(wildcard $(EXPORTS_PATH)/Makefile),) + $(MAKE) -C "$(EXPORTS_PATH)" +endif +endif mkdir -p src/headers sed --null-data \ -e 's/DO NOT EDIT/Modified for PHP/' \ + \ -e 's/__SIZE_TYPE__/size_t/g' \ -e 's/[^\n]*_Complex[^\n]*//g' \ -e 's/#ifdef __cplusplus[^#]*#endif//g' \ - -e 's/^/#define FFI_LIB "$(LIB_PREFIX)eduvpn_verify$(LIB_SUFFIX)"\n\n/' \ - "../../exports/$(GOOS)/$(GOARCH)/eduvpn_verify.h" > src/headers/eduvpn_verify_php.h + \ + -e 's/^/#define FFI_LIB "$(subst /,\/,$(COPY_LIB_DIR))$(LIB_FILE)"\n\n/' \ + \ + "$(EXPORTS_LIB_PATH)/$(GOOS)/$(GOARCH)/$(LIB_NAME).h" > src/headers/$(LIB_NAME)_php.h +ifeq ($(COPY_LIB),1) + install "$(EXPORTS_LIB_PATH)/$(GOOS)/$(GOARCH)/$(LIB_FILE)" -Dt "$(COPY_LIB_DIR)" +endif test: install-header install-dev-dependencies phpunit +# Try: composer, composer.phar, ./composer.phar, ./composer +# check-platform-reqs is needed because of config.platform in composer.json, see https://getcomposer.org/doc/06-config.md#platform install-dev-dependencies: if command -v composer; then \ composer install && composer check-platform-reqs; \ @@ -31,4 +57,4 @@ install-dev-dependencies: fi clean: - rm -rf vendor/ .phpunit* src/headers/*.h + rm -rf vendor/ .phpunit* src/headers/*.h lib/* diff --git a/wrappers/php/README.md b/wrappers/php/README.md index 776c0ac..b5cafa2 100644 --- a/wrappers/php/README.md +++ b/wrappers/php/README.md @@ -27,3 +27,9 @@ Or for the specified platform: ```shell make install-header GOOS=windows GOARCH=amd64 ``` + +When using this library, you will need to make sure that the linker can find the shared Go library. Alternatively, +pass `COPY_LIB=1` to `make install-header` to copy the library over to this folder and load it via this relative path. + +If you do not build this as part of the full repository, specify `EXPORTS_PATH="path/to/exports-folder"` when calling +make. This folder must contain `platform.mk` and the `lib/` folder with built libraries and headers. diff --git a/wrappers/php/src/Discovery.php b/wrappers/php/src/Discovery.php index 3ae7010..322d621 100644 --- a/wrappers/php/src/Discovery.php +++ b/wrappers/php/src/Discovery.php @@ -11,13 +11,15 @@ final class Discovery { public function __construct() { } + const LIB_NAME = "eduvpn_common"; + private static ?FFI $ffi = null; private static function ffi(): FFI { if (!self::$ffi) { - if (!(self::$ffi = FFI::load(__DIR__ . '/headers/eduvpn_verify_php.h'))) - throw new Error('failed to load eduvpn_verify'); + if (!(self::$ffi = FFI::load(__DIR__ . '/headers/' . self::LIB_NAME . '_php.h'))) + throw new Error('failed to load ' . self::LIB_NAME); } return self::$ffi; } @@ -30,8 +32,8 @@ final class Discovery * @param string $signedJson Signed .json file contents. * @param string $expectedFileName The file type to be verified, one of "server_list.json" or * "organization_list.json". - * @param int $minSignTime Minimum time for signature. Should be set to at least the time in a previously - * retrieved file. + * @param int $minSignTime Minimum time for signature. Should be set to at least the time of the previous + * signature. * @return void * @throws InvalidArgumentException If expectedFileName is not one of the allowed values. * @throws VerifyException If signature verification fails. diff --git a/wrappers/php/src/SignatureTooOldException.php b/wrappers/php/src/SignatureTooOldException.php index bbae949..4b7e341 100644 --- a/wrappers/php/src/SignatureTooOldException.php +++ b/wrappers/php/src/SignatureTooOldException.php @@ -2,7 +2,7 @@ namespace EduVpn\Common; -/** Signature has a timestamp lower than the specified minimum signing time. */ +/** Signature timestamp smaller than specified minimum signing time (rollback). */ final class SignatureTooOldException extends VerifyException { public function __construct() diff --git a/wrappers/php/tests/DiscoveryTest.php b/wrappers/php/tests/DiscoveryTest.php index 25ac187..fdce505 100644 --- a/wrappers/php/tests/DiscoveryTest.php +++ b/wrappers/php/tests/DiscoveryTest.php @@ -13,31 +13,31 @@ class DiscoveryTest extends TestCase public static function setUpBeforeClass(): void { - preg_match('/[\r\n](\S+)\s*/', file_get_contents(self::TEST_DATA_DIR . '/dummy/public.key'), $matches); + preg_match('/[\r\n](\S+)\s*/', file_get_contents(self::TEST_DATA_DIR . '/public.key'), $matches); Discovery::insecureTestingSetExtraKey($matches[1]); } public function testValid(): void { $this->expectNotToPerformAssertions(); - Discovery::verify(file_get_contents(self::TEST_DATA_DIR . '/dummy/server_list.json.minisig'), - file_get_contents(self::TEST_DATA_DIR . '/dummy/server_list.json'), + Discovery::verify(file_get_contents(self::TEST_DATA_DIR . '/server_list.json.minisig'), + file_get_contents(self::TEST_DATA_DIR . '/server_list.json'), 'server_list.json', 0); } public function testInvalidSignature(): void { $this->expectException(InvalidSignatureException::class); - Discovery::verify(file_get_contents(self::TEST_DATA_DIR . '/dummy/random.txt'), - file_get_contents(self::TEST_DATA_DIR . '/dummy/server_list.json'), + Discovery::verify(file_get_contents(self::TEST_DATA_DIR . '/random.txt'), + file_get_contents(self::TEST_DATA_DIR . '/server_list.json'), 'server_list.json', 0); } public function testWrongKey(): void { $this->expectException(InvalidSignatureUnknownKeyException::class); - Discovery::verify(file_get_contents(self::TEST_DATA_DIR . '/dummy/server_list.json.wrong_key.minisig'), - file_get_contents(self::TEST_DATA_DIR . '/dummy/server_list.json'), + Discovery::verify(file_get_contents(self::TEST_DATA_DIR . '/server_list.json.wrong_key.minisig'), + file_get_contents(self::TEST_DATA_DIR . '/server_list.json'), 'server_list.json', 0); } @@ -45,16 +45,16 @@ class DiscoveryTest extends TestCase public function testOldSignature(): void { $this->expectException(SignatureTooOldException::class); - Discovery::verify(file_get_contents(self::TEST_DATA_DIR . '/dummy/server_list.json.minisig'), - file_get_contents(self::TEST_DATA_DIR . '/dummy/server_list.json'), + Discovery::verify(file_get_contents(self::TEST_DATA_DIR . '/server_list.json.minisig'), + file_get_contents(self::TEST_DATA_DIR . '/server_list.json'), 'server_list.json', 1 << 31); } public function testUnknownExpectedFileName(): void { $this->expectException(InvalidArgumentException::class); - Discovery::verify(file_get_contents(self::TEST_DATA_DIR . '/dummy/other_list.json.minisig'), - file_get_contents(self::TEST_DATA_DIR . '/dummy/other_list.json'), + Discovery::verify(file_get_contents(self::TEST_DATA_DIR . '/other_list.json.minisig'), + file_get_contents(self::TEST_DATA_DIR . '/other_list.json'), 'other_list.json', 0); } } diff --git a/wrappers/python/Makefile b/wrappers/python/Makefile index be4beaa..ba4cf5f 100644 --- a/wrappers/python/Makefile +++ b/wrappers/python/Makefile @@ -1,15 +1,27 @@ .PHONY: pack test clean +EXPORTS_PATH ?= ../../exports +EXPORTS_LIB_PATH ?= $(EXPORTS_PATH)/lib + +ifneq ($(MAKECMDGOALS),clean) +include $(EXPORTS_PATH)/platform.mk +endif + ifdef PLAT_NAME SETUP_ARGS += --plat-name=$(PLAT_NAME) endif # Build for current platform only pack: - ./setup.py bdist_wheel $(SETUP_ARGS) + ./setup.py bdist_wheel $(SETUP_ARGS) --exports-lib-path="$(EXPORTS_LIB_PATH)" test: - $(MAKE) -C ../../exports copy-to COPY_TARGET=../wrappers/python/eduvpncommon/lib +ifneq ($(EXPORTS_PATH),) +ifneq ($(wildcard $(EXPORTS_PATH)/Makefile),) + $(MAKE) -C "$(EXPORTS_PATH)" +endif +endif + install "$(EXPORTS_LIB_PATH)/$(GOOS)/$(GOARCH)/$(LIB_FILE)" -Dt "eduvpncommon/lib" python3 -m unittest test_discovery rm eduvpncommon/lib/* diff --git a/wrappers/python/README.md b/wrappers/python/README.md index 6175910..bce492e 100644 --- a/wrappers/python/README.md +++ b/wrappers/python/README.md @@ -2,9 +2,7 @@ ## Requirements -Python 3.6+ is assumed, but it may work with older versions. - -TODO Build +Python 3.6+ is assumed, but it may work with older versions. To build, `setuptools` and `wheel` are required. ## Build & test @@ -16,7 +14,11 @@ Build wheel using library for current platform: make pack ``` -Build wheel using library for specified platform (passed to setuptools `--plat-name`): +(This does not build the shared Go library.) + +Build wheel using library for specified platform (passed to setuptools `--plat-name`, +see [`get_build_platform`](https://setuptools.pypa.io/en/latest/pkg_resources.html?highlight=get_build_platform#platform-utilities) +for more): ```shell make pack PLAT_NAME=win32 @@ -28,9 +30,12 @@ To install the wheel, run: pip install dist/eduvpncommon-[version]-py3-none-[platform].whl ``` -You could also reference the discovery module directly and copy the library for the platform to the `eduvpncommon/lib` +You could also reference the discovery module directly and copy the library for the platform to the `eduvpncommon/lib/` folder. +If you do not build this as part of the full repository, specify `EXPORTS_PATH="path/to/exports-folder"` when calling +make. This folder must contain `platform.mk` and the `lib/` folder with built libraries. + Test: ```shell diff --git a/wrappers/python/eduvpncommon/discovery.py b/wrappers/python/eduvpncommon/discovery.py index f7a312e..f22df58 100644 --- a/wrappers/python/eduvpncommon/discovery.py +++ b/wrappers/python/eduvpncommon/discovery.py @@ -15,28 +15,30 @@ _lib_suffixes = defaultdict(lambda: ".so", { _os = platform.system().lower() -_libname = f"{_lib_prefixes[_os]}eduvpn_verify{_lib_suffixes[_os]}" -_lib = cdll.LoadLibrary(str(pathlib.Path(__file__).parent / "lib" / _libname)) +_libname = "eduvpn_common" +_libfile = f"{_lib_prefixes[_os]}{_libname}{_lib_suffixes[_os]}" +# Library should have been copied to the lib/ folder +_lib = cdll.LoadLibrary(str(pathlib.Path(__file__).parent / "lib" / _libfile)) -class GoSlice(Structure): +class _GoSlice(Structure): _fields_ = [("data", POINTER(c_char)), ("len", c_int64), ("cap", c_int64)] @staticmethod - def make(bs: bytes) -> "GoSlice": - return GoSlice((c_char * len(bs))(*bs), len(bs), len(bs)) + def make(bs: bytes) -> "_GoSlice": + return _GoSlice((c_char * len(bs))(*bs), len(bs), len(bs)) -_lib.Verify.argtypes, _lib.Verify.restype = [GoSlice, GoSlice, GoSlice, c_uint64], c_int64 -_lib.InsecureTestingSetExtraKey.argtypes, _lib.InsecureTestingSetExtraKey.restype = [GoSlice], None +_lib.Verify.argtypes, _lib.Verify.restype = [_GoSlice, _GoSlice, _GoSlice, c_uint64], c_int64 +_lib.InsecureTestingSetExtraKey.argtypes, _lib.InsecureTestingSetExtraKey.restype = [_GoSlice], None class VerifyErrorCode(Enum): - ErrUnknownExpectedFileName = 1 # Expected file name is not one of the recognized values. + ErrUnknownExpectedFileName = 1 # Unknown expected file name specified. The signature has not been verified. ErrInvalidSignature = 2 # Signature is invalid (for the expected file type). ErrInvalidSignatureUnknownKey = 3 # Signature was created with an unknown key and has not been verified. - ErrTooOld = 4 # Signature has a timestamp lower than the specified minimum signing time. - Unknown = -1 # Other unknown error + ErrTooOld = 4 # Signature timestamp smaller than specified minimum signing time (rollback). + Unknown = -1 # Other unknown error. class VerifyError(Exception): @@ -44,6 +46,7 @@ class VerifyError(Exception): code_int: int # Original error code also for VerifyErrorCode.Unknown def __init__(self, err: int): + assert err try: self.code = VerifyErrorCode(err) except ValueError: @@ -68,13 +71,13 @@ def verify(signature: bytes, signed_json: bytes, expected_file_name: str, min_si :param signature: .minisig signature file contents. :param signed_json: Signed .json file contents. :param expected_file_name: The file type to be verified, one of "server_list.json" or "organization_list.json". - :param min_sign_time: Minimum time for signature. Should be set to at least the time in a previously retrieved file. + :param min_sign_time: Minimum time for signature. Should be set to at least the time of the previous signature. :raises VerifyException: If signature verification fails or expectedFileName is not one of the allowed values. """ - err = _lib.Verify(GoSlice.make(signature), GoSlice.make(signed_json), - GoSlice.make(expected_file_name.encode()), min_sign_time) + err = _lib.Verify(_GoSlice.make(signature), _GoSlice.make(signed_json), + _GoSlice.make(expected_file_name.encode()), min_sign_time) if err: raise VerifyError(err) @@ -82,4 +85,4 @@ def verify(signature: bytes, signed_json: bytes, expected_file_name: str, min_si def _insecure_testing_set_extra_key(key_string: str) -> None: """Use for testing only, see Go documentation.""" - _lib.InsecureTestingSetExtraKey(GoSlice.make(key_string.encode())) + _lib.InsecureTestingSetExtraKey(_GoSlice.make(key_string.encode())) diff --git a/wrappers/python/setup.py b/wrappers/python/setup.py index db254aa..9e7bde4 100755 --- a/wrappers/python/setup.py +++ b/wrappers/python/setup.py @@ -1,17 +1,20 @@ #!/usr/bin/env python3 import os -import pathlib import shutil +import sys import typing from collections import defaultdict -import sys from setuptools import setup from wheel.bdist_wheel import bdist_wheel as _bdist_wheel +_libname = "eduvpn_common" + def getlibpath(plat_name: str) -> typing.Union[str, None]: + """Get library path for plat_name relative to exports/lib/ folder.""" + _plat_map = defaultdict(lambda: plat_name, { "win32": "win-x86", }) @@ -47,12 +50,21 @@ def getlibpath(plat_name: str) -> typing.Union[str, None]: processed_os = _os_map[plat_os] return f"{processed_os}/{_arch_map[plat_arch]}/" \ - f"{_lib_prefixes[processed_os]}eduvpn_verify{_lib_suffixes[processed_os]}" + f"{_lib_prefixes[processed_os]}{_libname}{_lib_suffixes[processed_os]}" +# Adapted from https://stackoverflow.com/a/51794740 # You would say there would be a better way to do all of this, but I couldn't find it class bdist_wheel(_bdist_wheel): + user_options = _bdist_wheel.user_options + [ + ("exports-lib-path=", None, "path to exports/lib directory"), + ] + + def initialize_options(self): + super().initialize_options() + self.exports_lib_path = "../../exports/lib" # default + def run(self): self.plat_name_supplied = True # Force use platform @@ -63,9 +75,10 @@ class bdist_wheel(_bdist_wheel): print(f"Building wheel for platform {self.plat_name}") - shutil.copy2(f"../../exports/{libpath}", "eduvpncommon/lib/") + # setuptools will only use paths inside the package for package_data, so we copy the library + tmp_lib = shutil.copy2(f"{self.exports_lib_path}/{libpath}", "eduvpncommon/lib/") _bdist_wheel.run(self) - os.remove(f"eduvpncommon/lib/{pathlib.Path(libpath).name}") + os.remove(tmp_lib) setup( @@ -73,6 +86,6 @@ setup( version="0.1.0", packages=["eduvpncommon"], python_requires=">=3.6", - package_data={"eduvpncommon": ["lib/*eduvpn_verify*"]}, + package_data={"eduvpncommon": [f"lib/*{_libname}*"]}, cmdclass={"bdist_wheel": bdist_wheel}, ) diff --git a/wrappers/python/test_discovery.py b/wrappers/python/test_discovery.py index 1282a3e..73c51c4 100755 --- a/wrappers/python/test_discovery.py +++ b/wrappers/python/test_discovery.py @@ -14,21 +14,21 @@ def read_bytes(path: str) -> bytes: class VerifyTests(unittest.TestCase): @classmethod def setUpClass(cls) -> None: - with open(f"{test_data_dir}/dummy/public.key") as f: + with open(f"{test_data_dir}/public.key") as f: discovery._insecure_testing_set_extra_key(f.readlines()[-1][:-1]) def testValid(self): discovery.verify( - read_bytes(f"{test_data_dir}/dummy/server_list.json.minisig"), - read_bytes(f"{test_data_dir}/dummy/server_list.json"), + read_bytes(f"{test_data_dir}/server_list.json.minisig"), + read_bytes(f"{test_data_dir}/server_list.json"), "server_list.json", 0 ) def testValidMemoryView(self): discovery.verify( - read_bytes(f"{test_data_dir}/dummy/server_list.json.minisig"), - memoryview(b"abc" + read_bytes(f"{test_data_dir}/dummy/server_list.json") + b"abc")[3:-3], + read_bytes(f"{test_data_dir}/server_list.json.minisig"), + memoryview(b"abc" + read_bytes(f"{test_data_dir}/server_list.json") + b"abc")[3:-3], "server_list.json", 0 ) @@ -36,8 +36,8 @@ class VerifyTests(unittest.TestCase): def testInvalidSignature(self): with self.assertRaises(discovery.VerifyError) as ctx: discovery.verify( - read_bytes(f"{test_data_dir}/dummy/random.txt"), - read_bytes(f"{test_data_dir}/dummy/server_list.json"), + read_bytes(f"{test_data_dir}/random.txt"), + read_bytes(f"{test_data_dir}/server_list.json"), "server_list.json", 0 ) @@ -46,8 +46,8 @@ class VerifyTests(unittest.TestCase): def testWrongKey(self): with self.assertRaises(discovery.VerifyError) as ctx: discovery.verify( - read_bytes(f"{test_data_dir}/dummy/server_list.json.wrong_key.minisig"), - read_bytes(f"{test_data_dir}/dummy/server_list.json"), + read_bytes(f"{test_data_dir}/server_list.json.wrong_key.minisig"), + read_bytes(f"{test_data_dir}/server_list.json"), "server_list.json", 0 ) @@ -56,8 +56,8 @@ class VerifyTests(unittest.TestCase): def testOldSignature(self): with self.assertRaises(discovery.VerifyError) as ctx: discovery.verify( - read_bytes(f"{test_data_dir}/dummy/server_list.json.minisig"), - read_bytes(f"{test_data_dir}/dummy/server_list.json"), + read_bytes(f"{test_data_dir}/server_list.json.minisig"), + read_bytes(f"{test_data_dir}/server_list.json"), "server_list.json", 1 << 31 ) @@ -66,8 +66,8 @@ class VerifyTests(unittest.TestCase): def TestUnknownExpectedFile(self): with self.assertRaises(discovery.VerifyError) as ctx: discovery.verify( - read_bytes(f"{test_data_dir}/dummy/other_list.json.minisig"), - read_bytes(f"{test_data_dir}/dummy/other_list.json"), + read_bytes(f"{test_data_dir}/other_list.json.minisig"), + read_bytes(f"{test_data_dir}/other_list.json"), "other_list.json", 0 ) diff --git a/wrappers/swift/CEduVpnCommon/Sources/CEduVpnCommon/module.modulemap b/wrappers/swift/CEduVpnCommon/Sources/CEduVpnCommon/module.modulemap index c85e48f..2c50cfd 100644 --- a/wrappers/swift/CEduVpnCommon/Sources/CEduVpnCommon/module.modulemap +++ b/wrappers/swift/CEduVpnCommon/Sources/CEduVpnCommon/module.modulemap @@ -1,5 +1,5 @@ module CEduVpnCommon { - header "Headers/eduvpn_verify.h" - link "eduvpn_verify" + header "Headers/eduvpn_common.h" + link "eduvpn_common" export * } diff --git a/wrappers/swift/Makefile b/wrappers/swift/Makefile index 84b3cc1..b46a177 100644 --- a/wrappers/swift/Makefile +++ b/wrappers/swift/Makefile @@ -1,24 +1,33 @@ -.PHONY: build test clean +.PHONY: build test install-header clean -ifneq (clean,$(MAKECMDGOALS)) -include ../../exports/platform.mk +EXPORTS_PATH ?= ../../exports +EXPORTS_LIB_PATH ?= $(EXPORTS_PATH)/lib -ifeq (Windows_NT,$(OS)) +ifneq ($(MAKECMDGOALS),clean) +include $(EXPORTS_PATH)/platform.mk + +LIB_DIR = $(EXPORTS_LIB_PATH)/$(GOOS)/$(GOARCH) + +ifeq ($(OS),Windows_NT) SWIFT = ./swift.cmd else SWIFT = swift endif endif -build: .build_lib - $(SWIFT) build --configuration release -Xlinker -L"../../exports/$(GOOS)/$(GOARCH)" +build: install-header + $(SWIFT) build --configuration release -Xlinker -L"$(LIB_DIR)" + +test: install-header + $(SWIFT) test --parallel -Xlinker -L"$(LIB_DIR)" -test: .build_lib - $(SWIFT) test --parallel -Xlinker -L"../../exports/$(GOOS)/$(GOARCH)" +install-header: +ifneq ($(EXPORTS_PATH),) +ifneq ($(wildcard $(EXPORTS_PATH)/Makefile),) + $(MAKE) -C "$(EXPORTS_PATH)" +endif +endif + install "$(LIB_DIR)/$(LIB_NAME).h" -Dt CEduVpnCommon/Sources/CEduVpnCommon/Headers # Copy header for modulemap clean: rm -rf .build/ CEduVpnCommon/Sources/CEduVpnCommon/Headers/*.h - -.build_lib: - $(MAKE) -C ../../exports - install "../../exports/$(GOOS)/$(GOARCH)/eduvpn_verify.h" -Dt CEduVpnCommon/Sources/CEduVpnCommon/Headers diff --git a/wrappers/swift/README.md b/wrappers/swift/README.md index 8259592..f28b028 100644 --- a/wrappers/swift/README.md +++ b/wrappers/swift/README.md @@ -2,7 +2,8 @@ ## Requirements -You will need to install the [Swift SDK](https://www.swift.org/getting-started), which includes the `swift` tool. +You will need to install the [Swift SDK](https://www.swift.org/getting-started), which includes the `swift` tool. This +project does not require Xcode as it uses the Swift Package Manager. ## Build & test @@ -18,7 +19,22 @@ Build `EduVpnCommon` using shared Go library for specified platform, e.g.: make GOOS=linux GOARCH=amd64 ``` -On Windows, you will also need to generate a .lib for the .dll. +When using this library, you will need to make sure that the linker can find the shared Go library. + +<small>On Windows, you will also need to generate a .lib import library for the .dll. You can +use `exports/generate_lib.ps1` +for this, passing in the path to the DLL file. Execute this from a Visual Studio Developer shell before building the +Swift project. Alternatively, you could use `objdump` and `llvm-dlltool`. You only need to update this if the list of +exported symbols changes.</small> + +If you just want to copy over the C header file to the right directory for the modulemap in `CEduVpnCommon`, run: + +```shell +make install-header +``` + +If you do not build this as part of the full repository, specify `EXPORTS_PATH="path/to/exports-folder"` when calling +make. This folder must contain `platform.mk` and the `lib/` folder with built libraries and headers. Test: diff --git a/wrappers/swift/Sources/EduVpnCommon/EduVpnCommon.swift b/wrappers/swift/Sources/EduVpnCommon/EduVpnCommon.swift index 340e94a..b849626 100644 --- a/wrappers/swift/Sources/EduVpnCommon/EduVpnCommon.swift +++ b/wrappers/swift/Sources/EduVpnCommon/EduVpnCommon.swift @@ -9,7 +9,7 @@ private extension Data { // This closure method guarantees this try withUnsafeBytes { (pointer: UnsafeRawBufferPointer) -> ResultType in // Note: UnsafeRawBufferPointer.startIndex will always be 0, see docs - // Cast to UnsafeMutableRawPointer, assumes it will not be written to + // Cast to UnsafeMutableRawPointer, assumes it will not actually be written to try body(GoSlice(data: UnsafeMutableRawPointer(mutating: pointer.baseAddress), len: GoInt(pointer.count), cap: GoInt(pointer.count))) } @@ -33,9 +33,9 @@ public enum VerifyErr: Error, Equatable { /// Signature has a timestamp lower than the specified minimum signing time. case ErrTooOld /// Other unknown error - case Unknown(code: GoInt) + case Unknown(code: GoInt8) - static func fromCode(_ code: GoInt) -> VerifyErr { + static func fromCode(_ code: GoInt8) -> VerifyErr { precondition(code != 0) switch code { case 1: return ErrUnknownExpectedFileName @@ -54,7 +54,7 @@ public enum VerifyErr: Error, Equatable { /// - signature: .minisig signature file contents. /// - signedJson: Signed .json file contents. /// - expectedFileName: The file type to be verified, one of "server_list.json" or "organization_list.json". -/// - minSignTime: Minimum time for signature. Should be set to at least the time in a previously retrieved file. +/// - minSignTime: Minimum time for signature. Should be set to at least the time of the previous signature. /// - Throws: VerifyErr: If signature verification fails or `expectedFileName` is not one of the allowed values. public func Verify(signature: Data, signedJson: Data, expectedFileName: String, minSignTime: Date) throws { let result = signature.withSlice { signatureData in diff --git a/wrappers/swift/Tests/EduVpnCommonTests/EduVpnCommonTests.swift b/wrappers/swift/Tests/EduVpnCommonTests/EduVpnCommonTests.swift index a508023..21186a9 100644 --- a/wrappers/swift/Tests/EduVpnCommonTests/EduVpnCommonTests.swift +++ b/wrappers/swift/Tests/EduVpnCommonTests/EduVpnCommonTests.swift @@ -6,14 +6,14 @@ final class EduVpnCommonTests: XCTestCase { override class func setUp() { // Swift is confused by CRLF, so on some systems we cannot just take the second-to-last element - InsecureTestingSetExtraKey(keyString: try! String(contentsOfFile: "\(testDataDir)/dummy/public.key") + InsecureTestingSetExtraKey(keyString: try! String(contentsOfFile: "\(testDataDir)/public.key") .components(separatedBy: .newlines).last(where: { !$0.isEmpty })!) } func testValid() throws { try Verify( - signature: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/server_list.json.minisig")), - signedJson: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/server_list.json")), + signature: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/server_list.json.minisig")), + signedJson: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/server_list.json")), expectedFileName: "server_list.json", minSignTime: Date(timeIntervalSince1970: 0)) } @@ -21,8 +21,8 @@ final class EduVpnCommonTests: XCTestCase { func testInvalidSignature() throws { XCTAssertThrowsError( try Verify( - signature: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/random.txt")), - signedJson: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/server_list.json")), + signature: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/random.txt")), + signedJson: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/server_list.json")), expectedFileName: "server_list.json", minSignTime: Date(timeIntervalSince1970: 0)), "", {err in XCTAssertEqual(err as? VerifyErr, VerifyErr.ErrInvalidSignature)}); @@ -31,8 +31,8 @@ final class EduVpnCommonTests: XCTestCase { func testWrongKey() throws { XCTAssertThrowsError( try Verify( - signature: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/server_list.json.wrong_key.minisig")), - signedJson: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/server_list.json")), + signature: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/server_list.json.wrong_key.minisig")), + signedJson: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/server_list.json")), expectedFileName: "server_list.json", minSignTime: Date(timeIntervalSince1970: 0)), "", {err in XCTAssertEqual(err as? VerifyErr, VerifyErr.ErrInvalidSignatureUnknownKey)}); @@ -41,8 +41,8 @@ final class EduVpnCommonTests: XCTestCase { func testOldSignature() throws { XCTAssertThrowsError( try Verify( - signature: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/server_list.json.minisig")), - signedJson: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/server_list.json")), + signature: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/server_list.json.minisig")), + signedJson: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/server_list.json")), expectedFileName: "server_list.json", minSignTime: Date(timeIntervalSince1970: TimeInterval(1 << 31))), "", {err in XCTAssertEqual(err as? VerifyErr, VerifyErr.ErrTooOld)}); @@ -51,8 +51,8 @@ final class EduVpnCommonTests: XCTestCase { func testUnknownExpectedFile() throws { XCTAssertThrowsError( try Verify( - signature: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/other_list.json.minisig")), - signedJson: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/dummy/other_list.json")), + signature: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/other_list.json.minisig")), + signedJson: try! Data(contentsOf: URL(fileURLWithPath: "\(EduVpnCommonTests.testDataDir)/other_list.json")), expectedFileName: "other_list.json", minSignTime: Date(timeIntervalSince1970: 0)), "", {err in XCTAssertEqual(err as? VerifyErr, VerifyErr.ErrUnknownExpectedFileName)}); diff --git a/wrappers/swift/swift.cmd b/wrappers/swift/swift.cmd index 87fba4b..57040bd 100755 --- a/wrappers/swift/swift.cmd +++ b/wrappers/swift/swift.cmd @@ -1,6 +1,6 @@ @echo off -:: Rename PATH -> Path +:: Rename PATH -> Path because of swift issue https://github.com/compnerd/swift-build/issues/413 set _p=%PATH% set PATH= set Path=%_p% |
