From 0558216319eec6cff894029e1bad1863273b7b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Thu, 2 Jul 2015 00:14:13 +0200 Subject: [PATCH] Allow the caller to inspect the host's certificate A caller might want to inspect the certificate or simply ignore who the server claims to be. --- LibGit2Sharp.Tests/CloneFixture.cs | 60 ++++++++++++++++++++++ LibGit2Sharp/Certificate.cs | 9 ++++ LibGit2Sharp/CertificateSsh.cs | 53 +++++++++++++++++++ LibGit2Sharp/CertificateX509.cs | 32 ++++++++++++ LibGit2Sharp/Core/GitCertificate.cs | 10 ++++ LibGit2Sharp/Core/GitCertificateSsh.cs | 23 +++++++++ LibGit2Sharp/Core/GitCertificateSshType.cs | 11 ++++ LibGit2Sharp/Core/GitCertificateType.cs | 29 +++++++++++ LibGit2Sharp/Core/GitCertificateX509.cs | 22 ++++++++ LibGit2Sharp/Core/GitRemoteCallbacks.cs | 2 +- LibGit2Sharp/Core/NativeMethods.cs | 2 + LibGit2Sharp/FetchOptionsBase.cs | 6 +++ LibGit2Sharp/Handlers.cs | 10 ++++ LibGit2Sharp/LibGit2Sharp.csproj | 8 +++ LibGit2Sharp/PushOptions.cs | 6 +++ LibGit2Sharp/RemoteCallbacks.cs | 41 +++++++++++++++ 16 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 LibGit2Sharp/Certificate.cs create mode 100644 LibGit2Sharp/CertificateSsh.cs create mode 100644 LibGit2Sharp/CertificateX509.cs create mode 100644 LibGit2Sharp/Core/GitCertificate.cs create mode 100644 LibGit2Sharp/Core/GitCertificateSsh.cs create mode 100644 LibGit2Sharp/Core/GitCertificateSshType.cs create mode 100644 LibGit2Sharp/Core/GitCertificateType.cs create mode 100644 LibGit2Sharp/Core/GitCertificateX509.cs diff --git a/LibGit2Sharp.Tests/CloneFixture.cs b/LibGit2Sharp.Tests/CloneFixture.cs index 46ef2ef4d..ae98788eb 100644 --- a/LibGit2Sharp.Tests/CloneFixture.cs +++ b/LibGit2Sharp.Tests/CloneFixture.cs @@ -237,6 +237,66 @@ public void CanCloneFromBBWithCredentials(string url, string user, string pass, } } + [SkippableTheory] + [InlineData("https://github.com/libgit2/TestGitRepository.git", "github.com", typeof(CertificateX509))] + [InlineData("git@github.com:libgit2/TestGitRepository.git", "github.com", typeof(CertificateSsh))] + public void CanInspectCertificateOnClone(string url, string hostname, Type certType) + { + var scd = BuildSelfCleaningDirectory(); + + InconclusiveIf( + () => + certType == typeof (CertificateSsh) && !GlobalSettings.Version.Features.HasFlag(BuiltInFeatures.Ssh), + "SSH not supported"); + + bool wasCalled = false; + bool checksHappy = false; + + var options = new CloneOptions { + CertificateCheck = (cert, valid, host) => { + wasCalled = true; + + Assert.Equal(hostname, host); + Assert.Equal(certType, cert.GetType()); + + if (certType == typeof(CertificateX509)) { + Assert.True(valid); + var x509 = ((CertificateX509)cert).Certificate; + // we get a string with the different fields instead of a structure, so... + Assert.True(x509.Subject.Contains("CN=github.com,")); + checksHappy = true; + return false; + } + + if (certType == typeof(CertificateSsh)) { + var hostkey = (CertificateSsh)cert; + Assert.True(hostkey.HasMD5); + /* + * Once you've connected and thus your ssh has stored the hostkey, + * you can get the hostkey for a host with + * + * ssh-keygen -F github.com -l | tail -n 1 | cut -d ' ' -f 2 | tr -d ':' + * + * though GitHub's hostkey won't change anytime soon. + */ + Assert.Equal("1627aca576282d36631b564debdfa648", + BitConverter.ToString(hostkey.HashMD5).ToLower().Replace("-", "")); + checksHappy = true; + return false; + } + + return false; + }, + }; + + Assert.Throws(() => + Repository.Clone(url, scd.DirectoryPath, options) + ); + + Assert.True(wasCalled); + Assert.True(checksHappy); + } + [Fact] public void CloningAnUrlWithoutPathThrows() { diff --git a/LibGit2Sharp/Certificate.cs b/LibGit2Sharp/Certificate.cs new file mode 100644 index 000000000..95472a24c --- /dev/null +++ b/LibGit2Sharp/Certificate.cs @@ -0,0 +1,9 @@ +namespace LibGit2Sharp +{ + /// + /// Top-level certificate type. The usable certificates inherit from this class. + /// + public abstract class Certificate + { + } +} diff --git a/LibGit2Sharp/CertificateSsh.cs b/LibGit2Sharp/CertificateSsh.cs new file mode 100644 index 000000000..01510ae68 --- /dev/null +++ b/LibGit2Sharp/CertificateSsh.cs @@ -0,0 +1,53 @@ +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// This class represents the hostkey which is avaiable when connecting to a SSH host. + /// + public class CertificateSsh : Certificate + { + /// + /// For mocking purposes + /// + protected CertificateSsh() + { } + + /// + /// The MD5 hash of the host. Meaningful if is true + /// + public readonly byte[] HashMD5; + + /// + /// The SHA1 hash of the host. Meaningful if is true + /// + public readonly byte[] HashSHA1; + + /// + /// True if we have the MD5 hostkey hash from the server + /// + public readonly bool HasMD5; + + /// + /// True if we have the SHA1 hostkey hash from the server + /// + public readonly bool HasSHA1; + + /// + /// True if we have the SHA1 hostkey hash from the server + /// public readonly bool HasSHA1; + + internal CertificateSsh(GitCertificateSsh cert) + { + + HasMD5 = cert.type.HasFlag(GitCertificateSshType.MD5); + HasSHA1 = cert.type.HasFlag(GitCertificateSshType.SHA1); + + HashMD5 = new byte[16]; + cert.HashMD5.CopyTo(HashMD5, 0); + + HashSHA1 = new byte[20]; + cert.HashSHA1.CopyTo(HashSHA1, 0); + } + } +} diff --git a/LibGit2Sharp/CertificateX509.cs b/LibGit2Sharp/CertificateX509.cs new file mode 100644 index 000000000..6ffed937c --- /dev/null +++ b/LibGit2Sharp/CertificateX509.cs @@ -0,0 +1,32 @@ +using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// Conains a X509 certificate + /// + public class CertificateX509 : Certificate + { + + /// + /// For mocking purposes + /// + protected CertificateX509() + { } + + /// + /// The certificate. + /// + public virtual X509Certificate Certificate { get; private set; } + + internal CertificateX509(GitCertificateX509 cert) + { + int len = checked((int) cert.len.ToUInt32()); + byte[] data = new byte[len]; + Marshal.Copy(cert.data, data, 0, len); + Certificate = new X509Certificate(data); + } + } +} diff --git a/LibGit2Sharp/Core/GitCertificate.cs b/LibGit2Sharp/Core/GitCertificate.cs new file mode 100644 index 000000000..2a237fb22 --- /dev/null +++ b/LibGit2Sharp/Core/GitCertificate.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace LibGit2Sharp.Core +{ + [StructLayout(LayoutKind.Sequential)] + internal struct GitCertificate + { + public GitCertificateType type; + } +} diff --git a/LibGit2Sharp/Core/GitCertificateSsh.cs b/LibGit2Sharp/Core/GitCertificateSsh.cs new file mode 100644 index 000000000..4bb88a0d1 --- /dev/null +++ b/LibGit2Sharp/Core/GitCertificateSsh.cs @@ -0,0 +1,23 @@ +using System.Runtime.InteropServices; + +namespace LibGit2Sharp.Core +{ + [StructLayout(LayoutKind.Sequential)] + internal struct GitCertificateSsh + { + public GitCertificateType cert_type; + public GitCertificateSshType type; + + /// + /// The MD5 hash (if appropriate) + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] HashMD5; + + /// + /// The MD5 hash (if appropriate) + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] + public byte[] HashSHA1; + } +} diff --git a/LibGit2Sharp/Core/GitCertificateSshType.cs b/LibGit2Sharp/Core/GitCertificateSshType.cs new file mode 100644 index 000000000..a5151123c --- /dev/null +++ b/LibGit2Sharp/Core/GitCertificateSshType.cs @@ -0,0 +1,11 @@ +using System; + +namespace LibGit2Sharp.Core +{ + [Flags] + internal enum GitCertificateSshType + { + MD5 = (1 << 0), + SHA1 = (1 << 1), + } +} diff --git a/LibGit2Sharp/Core/GitCertificateType.cs b/LibGit2Sharp/Core/GitCertificateType.cs new file mode 100644 index 000000000..1b06b1af3 --- /dev/null +++ b/LibGit2Sharp/Core/GitCertificateType.cs @@ -0,0 +1,29 @@ +namespace LibGit2Sharp.Core +{ + /// + /// Git certificate types to present to the user + /// + internal enum GitCertificateType + { + /// + /// No information about the certificate is available. + /// + None = 0, + + /// + /// The certificate is a x509 certificate + /// + X509 = 1, + + /// + /// The "certificate" is in fact a hostkey identification for ssh. + /// + Hostkey = 2, + + /// + /// The "certificate" is in fact a collection of `name:content` strings + /// containing information about the certificate. + /// + StrArray = 3, + } +} diff --git a/LibGit2Sharp/Core/GitCertificateX509.cs b/LibGit2Sharp/Core/GitCertificateX509.cs new file mode 100644 index 000000000..36df3c3ca --- /dev/null +++ b/LibGit2Sharp/Core/GitCertificateX509.cs @@ -0,0 +1,22 @@ +using System; +using System.Runtime.InteropServices; + +namespace LibGit2Sharp.Core +{ + [StructLayout(LayoutKind.Sequential)] + internal struct GitCertificateX509 + { + /// + /// Type of the certificate, in this case, GitCertificateType.X509 + /// + public GitCertificateType cert_type; + /// + /// Pointer to the X509 certificate data + /// + public IntPtr data; + /// + /// The size of the certificate data + /// + public UIntPtr len; + } +} diff --git a/LibGit2Sharp/Core/GitRemoteCallbacks.cs b/LibGit2Sharp/Core/GitRemoteCallbacks.cs index 71537f762..4c797b596 100644 --- a/LibGit2Sharp/Core/GitRemoteCallbacks.cs +++ b/LibGit2Sharp/Core/GitRemoteCallbacks.cs @@ -17,7 +17,7 @@ internal struct GitRemoteCallbacks internal NativeMethods.git_cred_acquire_cb acquire_credentials; - internal IntPtr certificate_check; + internal NativeMethods.git_transport_certificate_check_cb certificate_check; internal NativeMethods.git_transfer_progress_callback download_progress; diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index e3389acbf..a1b5b5cf1 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -1653,6 +1653,8 @@ internal static extern int git_tag_delete( internal delegate int git_transport_cb(out IntPtr transport, IntPtr remote, IntPtr payload); + internal delegate int git_transport_certificate_check_cb(IntPtr cert, int valid, IntPtr hostname, IntPtr payload); + [DllImport(libgit2)] internal static extern int git_transport_register( [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string prefix, diff --git a/LibGit2Sharp/FetchOptionsBase.cs b/LibGit2Sharp/FetchOptionsBase.cs index 483286668..7b946e9e1 100644 --- a/LibGit2Sharp/FetchOptionsBase.cs +++ b/LibGit2Sharp/FetchOptionsBase.cs @@ -33,6 +33,12 @@ internal FetchOptionsBase() /// public CredentialsHandler CredentialsProvider { get; set; } + /// + /// This hanlder will be called to let the user make a decision on whether to allow + /// the connection to preoceed based on the certificate presented by the server. + /// + public CertificateCheckHandler CertificateCheck { get; set; } + /// /// Starting to operate on a new repository. /// diff --git a/LibGit2Sharp/Handlers.cs b/LibGit2Sharp/Handlers.cs index fd20c7e8e..7e0b572c4 100644 --- a/LibGit2Sharp/Handlers.cs +++ b/LibGit2Sharp/Handlers.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; namespace LibGit2Sharp.Handlers { @@ -32,6 +33,15 @@ namespace LibGit2Sharp.Handlers /// Credential types which the server accepts public delegate Credentials CredentialsHandler(string url, string usernameFromUrl, SupportedCredentialTypes types); + /// + /// Delegate definition for the certificate validation + /// + /// The certificate which the server sent + /// The hostname which we tried to connect to + /// Whether libgit2 thinks this certificate is valid + /// True to continue, false to cancel + public delegate bool CertificateCheckHandler(Certificate certificate, bool valid, string host); + /// /// Delegate definition for transfer progress callback. /// diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 9a00d0dad..a3f87d05a 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -372,6 +372,14 @@ + + + + + + + + diff --git a/LibGit2Sharp/PushOptions.cs b/LibGit2Sharp/PushOptions.cs index 10eac1392..b5afc3eb2 100644 --- a/LibGit2Sharp/PushOptions.cs +++ b/LibGit2Sharp/PushOptions.cs @@ -12,6 +12,12 @@ public sealed class PushOptions /// public CredentialsHandler CredentialsProvider { get; set; } + /// + /// This hanlder will be called to let the user make a decision on whether to allow + /// the connection to preoceed based on the certificate presented by the server. + /// + public CertificateCheckHandler CertificateCheck { get; set; } + /// /// If the transport being used to push to the remote requires the creation /// of a pack file, this controls the number of worker threads used by diff --git a/LibGit2Sharp/RemoteCallbacks.cs b/LibGit2Sharp/RemoteCallbacks.cs index 6dacf4925..3f7342ca4 100644 --- a/LibGit2Sharp/RemoteCallbacks.cs +++ b/LibGit2Sharp/RemoteCallbacks.cs @@ -27,6 +27,7 @@ internal RemoteCallbacks(PushOptions pushOptions) PushTransferProgress = pushOptions.OnPushTransferProgress; PackBuilderProgress = pushOptions.OnPackBuilderProgress; CredentialsProvider = pushOptions.CredentialsProvider; + CertificateCheck = pushOptions.CertificateCheck; PushStatusError = pushOptions.OnPushStatusError; PrePushCallback = pushOptions.OnNegotiationCompletedBeforePush; } @@ -42,6 +43,7 @@ internal RemoteCallbacks(FetchOptionsBase fetchOptions) DownloadTransferProgress = fetchOptions.OnTransferProgress; UpdateTips = fetchOptions.OnUpdateTips; CredentialsProvider = fetchOptions.CredentialsProvider; + CertificateCheck = fetchOptions.CertificateCheck; } #region Delegates @@ -90,6 +92,11 @@ internal RemoteCallbacks(FetchOptionsBase fetchOptions) /// private readonly CredentialsHandler CredentialsProvider; + /// + /// Callback to perform validation on the certificate + /// + private readonly CertificateCheckHandler CertificateCheck; + internal GitRemoteCallbacks GenerateCallbacks() { var callbacks = new GitRemoteCallbacks { version = 1 }; @@ -114,6 +121,11 @@ internal GitRemoteCallbacks GenerateCallbacks() callbacks.acquire_credentials = GitCredentialHandler; } + if (CertificateCheck != null) + { + callbacks.certificate_check = GitCertificateCheck; + } + if (DownloadTransferProgress != null) { callbacks.download_progress = GitDownloadTransferProgressHandler; @@ -278,6 +290,35 @@ private int GitCredentialHandler( return cred.GitCredentialHandler(out ptr); } + private int GitCertificateCheck(IntPtr certPtr, int valid, IntPtr cHostname, IntPtr payload) + { + string hostname = LaxUtf8Marshaler.FromNative(cHostname); + GitCertificate baseCert = certPtr.MarshalAs(); + Certificate cert = null; + + switch (baseCert.type) + { + case GitCertificateType.X509: + cert = new CertificateX509(certPtr.MarshalAs()); + break; + case GitCertificateType.Hostkey: + cert = new CertificateSsh(certPtr.MarshalAs()); + break; + } + + bool result = false; + try + { + result = CertificateCheck(cert, valid != 0, hostname); + } + catch (Exception exception) + { + Proxy.giterr_set_str(GitErrorCategory.Callback, exception); + } + + return Proxy.ConvertResultToCancelFlag(result); + } + private int GitPushNegotiationHandler(IntPtr updates, UIntPtr len, IntPtr payload) { if (updates == IntPtr.Zero)