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
{
///
/// 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.
///
/// .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 of the previous signature.
/// If expectedFileName is not one of the allowed values.
/// If signature verification fails.
public static void Verify(
ArraySegment signatureFileContent, // Span would be nicer, but is not available in .NET Standard 2.0
ArraySegment 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);
}
}
/// Use for testing only, see Go documentation.
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);
///
/// Safe auto-disposing Go slice handle.
/// Non-copying alternative to `Marshal.AllocHGlobal` etc.
///
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(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_;
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) { }
}
}
/// Verification failed, do not trust the file.
public abstract class VerifyException : Exception
{
protected VerifyException(string message) : base(message) { }
}
/// Signature is invalid (for the expected file type).
public sealed class InvalidSignatureException : VerifyException
{
public InvalidSignatureException() : base("invalid signature") { }
}
/// Signature was created with an unknown key and has not been verified.
public sealed class InvalidSignatureUnknownKeyException : VerifyException
{
public InvalidSignatureUnknownKeyException() : base("invalid signature (unknown key)") { }
}
/// Signature timestamp smaller than specified minimum signing time (rollback).
public sealed class SignatureTooOldException : VerifyException
{
public SignatureTooOldException() : base("replay of previous signature (rollback)") { }
}
/// Other unknown error.
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
}
}