using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
[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 in a previously retrieved file.
/// If expectedFileName is not one of the allowed values.
/// If signature verification fails.
public static void Verify(
ArraySegment signatureFileContent,
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());
}
if (result != VerifyReturnCode.Ok)
{
if (result == VerifyReturnCode.ErrUnknownExpectedFileName)
throw new ArgumentException("unknown name", nameof(expectedFileName));
throw new VerifyException((VerifyErrorCode) 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 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(ArraySegment segment) where T : struct =>
new GoSliceHandle(segment.Array!, segment.Offset, segment.Count);
public static GoSliceHandle FromString(string str) =>
FromArray(new ArraySegment(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
{
/// Signature is invalid (for the expected file type).
ErrInvalidSignature = VerifyReturnCode.ErrUnknownExpectedFileName + 1,
/// Signature was created with an unknown key and has not been verified.
ErrInvalidSignatureUnknownKey,
/// Signature has a timestamp lower than the specified minimum signing time.
ErrTooOld
}
enum VerifyReturnCode
{
Ok,
ErrUnknownExpectedFileName
//...
}
}