Skip to content

Commit d49d264

Browse files
committed
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.
1 parent a07186e commit d49d264

16 files changed

+289
-1
lines changed

LibGit2Sharp.Tests/CloneFixture.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,44 @@ public void CanCloneFromBBWithCredentials(string url, string user, string pass,
237237
}
238238
}
239239

240+
[SkippableTheory]
241+
[InlineData("https://github.com/libgit2/TestGitRepository.git", "github.com", typeof(CertificateX509))]
242+
[InlineData("[email protected]/libgit2/TestGitRepository.git", "github.com", typeof(CertificateSsh))]
243+
public void CanInspectCertificateOnClone(string url, string hostname, Type certType)
244+
{
245+
var scd = BuildSelfCleaningDirectory();
246+
247+
if (certType == typeof(CertificateSsh) && !GlobalSettings.Version.Features.HasFlag(BuiltInFeatures.Ssh))
248+
{
249+
throw new SkipException("SSH not supported");
250+
}
251+
252+
Assert.Throws<UserCancelledException>(() => {
253+
Repository.Clone(url, scd.DirectoryPath, new CloneOptions()
254+
{
255+
CertificateCheck = (cert, valid, host) =>
256+
{
257+
Assert.Equal(hostname, host);
258+
Assert.Equal(certType, cert.GetType());
259+
if (certType == typeof(CertificateX509))
260+
{
261+
Assert.True(valid);
262+
var x509 = ((CertificateX509)cert).Certificate;
263+
// we get a string with the different fields instead of a structure, so...
264+
Assert.True(x509.Subject.Contains("CN=github.com,"));
265+
}
266+
else
267+
{
268+
var hostkey = (CertificateSsh)cert;
269+
Assert.True(hostkey.HasMD5);
270+
Assert.Equal("1627aca576282d36631b564debdfa648", BitConverter.ToString(hostkey.HashMD5));
271+
}
272+
return false;
273+
},
274+
});
275+
});
276+
}
277+
240278
[Fact]
241279
public void CloningAnUrlWithoutPathThrows()
242280
{

LibGit2Sharp/Certificate.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace LibGit2Sharp
2+
{
3+
/// <summary>
4+
/// Top-level certificate type. The usable certificates inherit from this class.
5+
/// </summary>
6+
public abstract class Certificate
7+
{
8+
}
9+
}

LibGit2Sharp/CertificateSsh.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using LibGit2Sharp.Core;
2+
3+
namespace LibGit2Sharp
4+
{
5+
/// <summary>
6+
/// This class represents the hostkey which is avaiable when connecting to a SSH host.
7+
/// </summary>
8+
public class CertificateSsh : Certificate
9+
{
10+
/// <summary>
11+
/// For mocking purposes
12+
/// </summary>
13+
protected CertificateSsh()
14+
{ }
15+
16+
/// <summary>
17+
/// The MD5 hash of the host. Meaningful if <see cref="HasMD5"/> is true
18+
/// </summary>
19+
public readonly byte[] HashMD5;
20+
21+
/// <summary>
22+
/// The SHA1 hash of the host. Meaningful if <see cref="HasSHA1"/> is true
23+
/// </summary>
24+
public readonly byte[] HashSHA1;
25+
26+
/// <summary>
27+
/// True if we have the MD5 hostkey hash from the server
28+
/// </summary>
29+
public readonly bool HasMD5;
30+
31+
/// <summary>
32+
/// True if we have the SHA1 hostkey hash from the server
33+
/// </summary>
34+
public readonly bool HasSHA1;
35+
36+
/// <summary>
37+
/// True if we have the SHA1 hostkey hash from the server
38+
/// </summary>public readonly bool HasSHA1;
39+
40+
internal CertificateSsh(GitCertificateSsh cert)
41+
{
42+
43+
HasMD5 = cert.type.HasFlag(GitCertificateSshType.MD5);
44+
HasSHA1 = cert.type.HasFlag(GitCertificateSshType.SHA1);
45+
46+
HashMD5 = new byte[16];
47+
cert.HashMD5.CopyTo(HashMD5, 0);
48+
49+
HashSHA1 = new byte[20];
50+
cert.HashSHA1.CopyTo(HashSHA1, 0);
51+
}
52+
}
53+
}

LibGit2Sharp/CertificateX509.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System.Runtime.InteropServices;
2+
using System.Security.Cryptography.X509Certificates;
3+
using LibGit2Sharp.Core;
4+
5+
namespace LibGit2Sharp
6+
{
7+
/// <summary>
8+
/// Conains a X509 certificate
9+
/// </summary>
10+
public class CertificateX509 : Certificate
11+
{
12+
13+
/// <summary>
14+
/// For mocking purposes
15+
/// </summary>
16+
protected CertificateX509()
17+
{ }
18+
19+
/// <summary>
20+
/// The certificate.
21+
/// </summary>
22+
public virtual X509Certificate Certificate { get; private set; }
23+
24+
internal CertificateX509(GitCertificateX509 cert)
25+
{
26+
int len = checked((int) cert.len.ToUInt32());
27+
byte[] data = new byte[len];
28+
Marshal.Copy(cert.data, data, 0, len);
29+
Certificate = new X509Certificate(data);
30+
}
31+
}
32+
}

LibGit2Sharp/Core/GitCertificate.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Runtime.InteropServices;
2+
3+
namespace LibGit2Sharp.Core
4+
{
5+
[StructLayout(LayoutKind.Sequential)]
6+
internal struct GitCertificate
7+
{
8+
public GitCertificateType type;
9+
}
10+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.Runtime.InteropServices;
2+
3+
namespace LibGit2Sharp.Core
4+
{
5+
[StructLayout(LayoutKind.Sequential)]
6+
internal struct GitCertificateSsh
7+
{
8+
public GitCertificateType cert_type;
9+
public GitCertificateSshType type;
10+
11+
/// <summary>
12+
/// The MD5 hash (if appropriate)
13+
/// </summary>
14+
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
15+
public byte[] HashMD5;
16+
17+
/// <summary>
18+
/// The MD5 hash (if appropriate)
19+
/// </summary>
20+
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
21+
public byte[] HashSHA1;
22+
}
23+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
3+
namespace LibGit2Sharp.Core
4+
{
5+
[Flags]
6+
internal enum GitCertificateSshType
7+
{
8+
MD5 = (1 << 0),
9+
SHA1 = (1 << 1),
10+
}
11+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace LibGit2Sharp.Core
2+
{
3+
/// <summary>
4+
/// Git certificate types to present to the user
5+
/// </summary>
6+
internal enum GitCertificateType
7+
{
8+
/// <summary>
9+
/// The certificate is a x509 certificate
10+
/// </summary>
11+
X509 = 0,
12+
/// <summary>
13+
/// The "certificate" is in fact a hostkey identification for ssh.
14+
/// </summary>
15+
Hostkey = 1,
16+
}
17+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
4+
namespace LibGit2Sharp.Core
5+
{
6+
[StructLayout(LayoutKind.Sequential)]
7+
internal struct GitCertificateX509
8+
{
9+
/// <summary>
10+
/// Type of the certificate, in this case, GitCertificateType.X509
11+
/// </summary>
12+
public GitCertificateType cert_type;
13+
/// <summary>
14+
/// Pointer to the X509 certificate data
15+
/// </summary>
16+
public IntPtr data;
17+
/// <summary>
18+
/// The size of the certificate data
19+
/// </summary>
20+
public UIntPtr len;
21+
}
22+
}

LibGit2Sharp/Core/GitRemoteCallbacks.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal struct GitRemoteCallbacks
1717

1818
internal NativeMethods.git_cred_acquire_cb acquire_credentials;
1919

20-
internal IntPtr certificate_check;
20+
internal NativeMethods.git_transport_certificate_check_cb certificate_check;
2121

2222
internal NativeMethods.git_transfer_progress_callback download_progress;
2323

LibGit2Sharp/Core/NativeMethods.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1653,6 +1653,8 @@ internal static extern int git_tag_delete(
16531653

16541654
internal delegate int git_transport_cb(out IntPtr transport, IntPtr remote, IntPtr payload);
16551655

1656+
internal delegate int git_transport_certificate_check_cb(IntPtr cert, int valid, IntPtr hostname, IntPtr payload);
1657+
16561658
[DllImport(libgit2)]
16571659
internal static extern int git_transport_register(
16581660
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string prefix,

LibGit2Sharp/FetchOptionsBase.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ internal FetchOptionsBase()
3434
/// </summary>
3535
public CredentialsHandler CredentialsProvider { get; set; }
3636

37+
/// <summary>
38+
/// This hanlder will be called to let the user make a decision on whether to allow
39+
/// the connection to preoceed based on the certificate presented by the server.
40+
/// </summary>
41+
public CertificateCheckHandler CertificateCheck { get; set; }
42+
3743
/// <summary>
3844
/// Starting to operate on a new repository.
3945
/// </summary>

LibGit2Sharp/Handlers.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Generic;
2+
using System.Security.Cryptography.X509Certificates;
23

34
namespace LibGit2Sharp.Handlers
45
{
@@ -32,6 +33,15 @@ namespace LibGit2Sharp.Handlers
3233
/// <param name="types">Credential types which the server accepts</param>
3334
public delegate Credentials CredentialsHandler(string url, string usernameFromUrl, SupportedCredentialTypes types);
3435

36+
/// <summary>
37+
/// Delegate definition for the certificate validation
38+
/// </summary>
39+
/// <param name="certificate">The certificate which the server sent</param>
40+
/// <param name="host">The hostname which we tried to connect to</param>
41+
/// <param name="valid">Whether libgit2 thinks this certificate is valid</param>
42+
/// <returns>True to continue, false to cancel</returns>
43+
public delegate bool CertificateCheckHandler(Certificate certificate, bool valid, string host);
44+
3545
/// <summary>
3646
/// Delegate definition for transfer progress callback.
3747
/// </summary>

LibGit2Sharp/LibGit2Sharp.csproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,14 @@
372372
<Compile Include="StashApplyOptions.cs" />
373373
<Compile Include="StashApplyStatus.cs" />
374374
<Compile Include="Core\GitStashApplyOpts.cs" />
375+
<Compile Include="Core\GitCertificateType.cs" />
376+
<Compile Include="Core\GitCertificate.cs" />
377+
<Compile Include="Core\GitCertificateX509.cs" />
378+
<Compile Include="Certificate.cs" />
379+
<Compile Include="CertificateX509.cs" />
380+
<Compile Include="Core\GitCertificateSsh.cs" />
381+
<Compile Include="Core\GitCertificateSshType.cs" />
382+
<Compile Include="CertificateSsh.cs" />
375383
</ItemGroup>
376384
<ItemGroup>
377385
<CodeAnalysisDictionary Include="CustomDictionary.xml" />

LibGit2Sharp/PushOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ public sealed class PushOptions
1212
/// </summary>
1313
public CredentialsHandler CredentialsProvider { get; set; }
1414

15+
/// <summary>
16+
/// This hanlder will be called to let the user make a decision on whether to allow
17+
/// the connection to preoceed based on the certificate presented by the server.
18+
/// </summary>
19+
public CertificateCheckHandler CertificateCheck { get; set; }
20+
1521
/// <summary>
1622
/// If the transport being used to push to the remote requires the creation
1723
/// of a pack file, this controls the number of worker threads used by

LibGit2Sharp/RemoteCallbacks.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ internal RemoteCallbacks(PushOptions pushOptions)
2727
PushTransferProgress = pushOptions.OnPushTransferProgress;
2828
PackBuilderProgress = pushOptions.OnPackBuilderProgress;
2929
CredentialsProvider = pushOptions.CredentialsProvider;
30+
CertificateCheck = pushOptions.CertificateCheck;
3031
PushStatusError = pushOptions.OnPushStatusError;
3132
PrePushCallback = pushOptions.OnNegotiationCompletedBeforePush;
3233
}
@@ -42,6 +43,7 @@ internal RemoteCallbacks(FetchOptionsBase fetchOptions)
4243
DownloadTransferProgress = fetchOptions.OnTransferProgress;
4344
UpdateTips = fetchOptions.OnUpdateTips;
4445
CredentialsProvider = fetchOptions.CredentialsProvider;
46+
CertificateCheck = fetchOptions.CertificateCheck;
4547
}
4648

4749
#region Delegates
@@ -90,6 +92,11 @@ internal RemoteCallbacks(FetchOptionsBase fetchOptions)
9092
/// </summary>
9193
private readonly CredentialsHandler CredentialsProvider;
9294

95+
/// <summary>
96+
/// Callback to perform validation on the certificate
97+
/// </summary>
98+
private readonly CertificateCheckHandler CertificateCheck;
99+
93100
internal GitRemoteCallbacks GenerateCallbacks()
94101
{
95102
var callbacks = new GitRemoteCallbacks { version = 1 };
@@ -114,6 +121,11 @@ internal GitRemoteCallbacks GenerateCallbacks()
114121
callbacks.acquire_credentials = GitCredentialHandler;
115122
}
116123

124+
if (CertificateCheck != null)
125+
{
126+
callbacks.certificate_check = GitCertificateCheck;
127+
}
128+
117129
if (DownloadTransferProgress != null)
118130
{
119131
callbacks.download_progress = GitDownloadTransferProgressHandler;
@@ -273,6 +285,35 @@ private int GitCredentialHandler(out IntPtr ptr, IntPtr cUrl, IntPtr usernameFro
273285
return cred.GitCredentialHandler(out ptr);
274286
}
275287

288+
private int GitCertificateCheck(IntPtr certPtr, int valid, IntPtr cHostname, IntPtr payload)
289+
{
290+
string hostname = LaxUtf8Marshaler.FromNative(cHostname);
291+
GitCertificate baseCert = certPtr.MarshalAs<GitCertificate>();
292+
Certificate cert = null;
293+
294+
switch (baseCert.type)
295+
{
296+
case GitCertificateType.X509:
297+
cert = new CertificateX509(certPtr.MarshalAs<GitCertificateX509>());
298+
break;
299+
case GitCertificateType.Hostkey:
300+
cert = new CertificateSsh(certPtr.MarshalAs<GitCertificateSsh>());
301+
break;
302+
}
303+
304+
bool result = false;
305+
try
306+
{
307+
result = CertificateCheck(cert, valid != 0, hostname);
308+
}
309+
catch (Exception exception)
310+
{
311+
Proxy.giterr_set_str(GitErrorCategory.Callback, exception);
312+
}
313+
314+
return Proxy.ConvertResultToCancelFlag(result);
315+
}
316+
276317
private int GitPushNegotiationHandler(IntPtr updates, UIntPtr len, IntPtr payload)
277318
{
278319
if (updates == IntPtr.Zero)

0 commit comments

Comments
 (0)