summaryrefslogtreecommitdiff
path: root/wrappers/csharp/Discovery.cs
blob: da4a6eaee4c55f514938bcae3e4023aecd3bf113 (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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
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
{
	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 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(
			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());
			}

			switch (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);
			}
		}

		/// <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 LibName = "eduvpn_common";

		[DllImport(LibName)]
		static extern VerifyReturnCode Verify(GoSlice signatureFileContent, GoSlice signedJson, GoSlice expectedFileName, ulong minSignTime);

		[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_;
			readonly GoSlice slice_;

			public GoSlice Slice => gcHandle_.IsAllocated
				? slice_
				: throw new InvalidOperationException("Handle was disposed");

			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);
			}

			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_;
			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) { }
		}
	}

	/// <summary>Verification failed, do not trust the file.</summary>
	public abstract class VerifyException : Exception
	{
		protected VerifyException(string message) : base(message) { }
	}

	/// <summary>Signature is invalid (for the expected file type).</summary>
	public sealed class InvalidSignatureException : VerifyException
	{
		public InvalidSignatureException() : base("invalid signature") { }
	}

	/// <summary>Signature was created with an unknown key and has not been verified.</summary>
	public sealed class InvalidSignatureUnknownKeyException : VerifyException
	{
		public InvalidSignatureUnknownKeyException() : base("invalid signature (unknown key)") { }
	}

	/// <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>Other unknown error.</summary>
	public sealed class UnknownVerifyException : VerifyException
	{
		public UnknownVerifyException(sbyte code) : base($"unknown verify error ({code})") => Debug.Assert(code != 0);
	}

	enum VerifyReturnCode : sbyte
	{
		Ok,
		ErrUnknownExpectedFileName,
		ErrInvalidSignature,
		ErrInvalidSignatureUnknownKey,
		ErrTooOld
	}
}