summaryrefslogtreecommitdiff
path: root/wrappers/csharp/Discovery.cs
blob: 183d94fe87e98e96e6e36371b844539837bf7f6f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
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 values.</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

		//...
	}
}