summaryrefslogtreecommitdiff
path: root/wrappers/csharp
diff options
context:
space:
mode:
authorStevenWdV <stevenwdv@gmail.com>2021-11-20 18:09:09 +0100
committerStevenWdV <stevenwdv@gmail.com>2021-11-22 12:37:33 +0100
commit8878d8705f0b0fcddb3979194340ca39df897580 (patch)
tree6c920d0b9d40584dfe6bf7e5b2e865acff72e72f /wrappers/csharp
parentb8d368b93479233a8ecbeba3daf4b10bee8f0a4a (diff)
Add C bindings and a C# wrapper
Diffstat (limited to 'wrappers/csharp')
-rw-r--r--wrappers/csharp/.gitignore4
-rw-r--r--wrappers/csharp/Discovery.cs135
-rw-r--r--wrappers/csharp/EduVpnCommon.csproj27
-rw-r--r--wrappers/csharp/EduVpnCommon.sln22
-rw-r--r--wrappers/csharp/EduVpnCommonTests/EduVpnCommonTests.csproj19
-rw-r--r--wrappers/csharp/EduVpnCommonTests/VerifyTests.cs72
-rw-r--r--wrappers/csharp/Makefile4
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