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