Skip to content

Commit 3ee95e7

Browse files
authored
Add SSH key support with libssh2 (#27)
1 parent 9b2bac1 commit 3ee95e7

15 files changed

Lines changed: 544 additions & 32 deletions

LibGit2Sharp.Tests/GlobalSettingsFixture.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ public void LoadFromSpecifiedPath(string architecture)
6161
{
6262
Skip.IfNot(Platform.IsRunningOnNetFramework(), ".NET Framework only test.");
6363

64-
var nativeDllFileName = NativeDllName.Name + ".dll";
6564
var testDir = Path.GetDirectoryName(typeof(GlobalSettingsFixture).Assembly.Location);
6665
var testAppExe = Path.Combine(testDir, $"NativeLibraryLoadTestApp.{architecture}.exe");
6766
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
@@ -71,7 +70,10 @@ public void LoadFromSpecifiedPath(string architecture)
7170
try
7271
{
7372
Directory.CreateDirectory(platformDir);
74-
File.Copy(Path.Combine(libraryPath, nativeDllFileName), Path.Combine(platformDir, nativeDllFileName));
73+
foreach (var file in Directory.GetFiles(libraryPath, "*.dll"))
74+
{
75+
File.Copy(file, Path.Combine(platformDir, Path.GetFileName(file)));
76+
}
7577

7678
var (output, exitCode) = ProcessHelper.RunProcess(testAppExe, arguments: $@"{NativeDllName.Name} ""{platformDir}""", workingDirectory: tempDir);
7779

LibGit2Sharp.Tests/NetworkFixture.cs

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ public void CanListRemoteReferences(string url)
2222
Remote remote = repo.Network.Remotes.Add(remoteName, url);
2323
IList<Reference> references = repo.Network.ListReferences(remote).ToList();
2424

25-
2625
foreach (var reference in references)
2726
{
2827
// None of those references point to an existing
@@ -136,6 +135,133 @@ public void CanListRemoteReferencesWithCredentials()
136135
}
137136
}
138137

138+
[Theory]
139+
[InlineData("http://github.com/libgit2/TestGitRepository")]
140+
[InlineData("https://github.com/libgit2/TestGitRepository")]
141+
public void CanListRemoteReferencesWithListRemoteOptions(string url)
142+
{
143+
string remoteName = "testRemote";
144+
145+
string repoPath = InitNewRepository();
146+
147+
using (var repo = new Repository(repoPath))
148+
{
149+
Remote remote = repo.Network.Remotes.Add(remoteName, url);
150+
var options = new ListRemoteOptions
151+
{
152+
ProxyOptions = new ProxyOptions()
153+
};
154+
155+
IList<Reference> references = repo.Network.ListReferences(remote, options).ToList();
156+
157+
foreach (var reference in references)
158+
{
159+
Assert.Null(reference.ResolveToDirectReference().Target);
160+
}
161+
162+
List<Tuple<string, string>> actualRefs = references.
163+
Select(directRef => new Tuple<string, string>(directRef.CanonicalName, directRef.ResolveToDirectReference()
164+
.TargetIdentifier)).ToList();
165+
166+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs.Count, actualRefs.Count);
167+
Assert.True(references.Single(reference => reference.CanonicalName == "HEAD") is SymbolicReference);
168+
for (int i = 0; i < TestRemoteRefs.ExpectedRemoteRefs.Count; i++)
169+
{
170+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs[i].Item2, actualRefs[i].Item2);
171+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs[i].Item1, actualRefs[i].Item1);
172+
}
173+
}
174+
}
175+
176+
[Theory]
177+
[InlineData("http://github.com/libgit2/TestGitRepository")]
178+
[InlineData("https://github.com/libgit2/TestGitRepository")]
179+
public void CanListRemoteReferencesFromUrlWithListRemoteOptions(string url)
180+
{
181+
string repoPath = InitNewRepository();
182+
183+
using (var repo = new Repository(repoPath))
184+
{
185+
var options = new ListRemoteOptions
186+
{
187+
ProxyOptions = new ProxyOptions()
188+
};
189+
190+
IList<Reference> references = repo.Network.ListReferences(url, options).ToList();
191+
192+
foreach (var reference in references)
193+
{
194+
Assert.Null(reference.ResolveToDirectReference().Target);
195+
}
196+
197+
List<Tuple<string, string>> actualRefs = references.
198+
Select(directRef => new Tuple<string, string>(directRef.CanonicalName, directRef.ResolveToDirectReference()
199+
.TargetIdentifier)).ToList();
200+
201+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs.Count, actualRefs.Count);
202+
Assert.True(references.Single(reference => reference.CanonicalName == "HEAD") is SymbolicReference);
203+
for (int i = 0; i < TestRemoteRefs.ExpectedRemoteRefs.Count; i++)
204+
{
205+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs[i].Item2, actualRefs[i].Item2);
206+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs[i].Item1, actualRefs[i].Item1);
207+
}
208+
}
209+
}
210+
211+
[Theory]
212+
[InlineData("https://github.com/libgit2/TestGitRepository")]
213+
public void CanListRemoteReferencesWithCertificateCheckCallback(string url)
214+
{
215+
string repoPath = InitNewRepository();
216+
217+
bool certificateCheckCalled = false;
218+
219+
using (var repo = new Repository(repoPath))
220+
{
221+
var options = new ListRemoteOptions
222+
{
223+
CertificateCheck = (cert, valid, host) =>
224+
{
225+
certificateCheckCalled = true;
226+
return true;
227+
}
228+
};
229+
230+
IList<Reference> references = repo.Network.ListReferences(url, options).ToList();
231+
232+
Assert.True(certificateCheckCalled);
233+
Assert.NotEmpty(references);
234+
}
235+
}
236+
237+
[SkippableFact]
238+
public void CanListRemoteReferencesWithCredentialsInListRemoteOptions()
239+
{
240+
InconclusiveIf(() => string.IsNullOrEmpty(Constants.PrivateRepoUrl),
241+
"Populate Constants.PrivateRepo* to run this test");
242+
243+
string remoteName = "origin";
244+
245+
string repoPath = InitNewRepository();
246+
247+
using (var repo = new Repository(repoPath))
248+
{
249+
Remote remote = repo.Network.Remotes.Add(remoteName, Constants.PrivateRepoUrl);
250+
251+
var options = new ListRemoteOptions
252+
{
253+
CredentialsProvider = Constants.PrivateRepoCredentials
254+
};
255+
256+
var references = repo.Network.ListReferences(remote, options);
257+
258+
foreach (var reference in references)
259+
{
260+
Assert.NotNull(reference);
261+
}
262+
}
263+
}
264+
139265
[Theory]
140266
[InlineData(FastForwardStrategy.Default)]
141267
[InlineData(FastForwardStrategy.NoFastForward)]

LibGit2Sharp.Tests/PushFixture.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,18 @@ public void CanInvokePrePushCallbackAndFail()
135135
Assert.True(prePushHandlerCalled);
136136
}
137137

138+
[Fact]
139+
public void CanPushWithRemoteProgressCallback()
140+
{
141+
PushOptions options = new PushOptions()
142+
{
143+
OnPushStatusError = OnPushStatusError,
144+
OnPushRemoteProgress = (progress) => { return true; },
145+
};
146+
147+
AssertPush(repo => repo.Network.Push(repo.Network.Remotes["origin"], "HEAD", @"refs/heads/master", options));
148+
}
149+
138150
[Fact]
139151
public void PushingABranchThatDoesNotTrackAnUpstreamBranchThrows()
140152
{

LibGit2Sharp.Tests/RepositoryFixture.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,30 @@ public void CanListRemoteReferences(string url)
724724
}
725725
}
726726

727+
[Theory]
728+
[InlineData("http://github.com/libgit2/TestGitRepository")]
729+
[InlineData("https://github.com/libgit2/TestGitRepository")]
730+
public void CanListRemoteReferencesWithListRemoteOptions(string url)
731+
{
732+
var options = new ListRemoteOptions
733+
{
734+
ProxyOptions = new ProxyOptions()
735+
};
736+
737+
IEnumerable<Reference> references = Repository.ListRemoteReferences(url, options).ToList();
738+
739+
List<Tuple<string, string>> actualRefs = references.
740+
Select(reference => new Tuple<string, string>(reference.CanonicalName, reference.ResolveToDirectReference().TargetIdentifier)).ToList();
741+
742+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs.Count, actualRefs.Count);
743+
Assert.True(references.Single(reference => reference.CanonicalName == "HEAD") is SymbolicReference);
744+
for (int i = 0; i < TestRemoteRefs.ExpectedRemoteRefs.Count; i++)
745+
{
746+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs[i].Item2, actualRefs[i].Item2);
747+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs[i].Item1, actualRefs[i].Item1);
748+
}
749+
}
750+
727751
[Fact]
728752
public void CanListRemoteReferencesWithDetachedRemoteHead()
729753
{

LibGit2Sharp/CertificateSsh.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ protected CertificateSsh()
2525
/// </summary>
2626
public readonly byte[] HashSHA1;
2727

28+
/// <summary>
29+
/// The SHA256 hash of the host. Meaningful if <see cref="HasSHA256"/> is true
30+
/// </summary>
31+
public readonly byte[] HashSHA256;
32+
2833
/// <summary>
2934
/// True if we have the MD5 hostkey hash from the server
3035
/// </summary>
@@ -35,11 +40,17 @@ protected CertificateSsh()
3540
/// </summary>
3641
public readonly bool HasSHA1;
3742

43+
/// <summary>
44+
/// True if we have the SHA256 hostkey hash from the server
45+
/// </summary>
46+
public readonly bool HasSHA256;
47+
3848
internal unsafe CertificateSsh(git_certificate_ssh* cert)
3949
{
4050

4151
HasMD5 = cert->type.HasFlag(GitCertificateSshType.MD5);
4252
HasSHA1 = cert->type.HasFlag(GitCertificateSshType.SHA1);
53+
HasSHA256 = cert->type.HasFlag(GitCertificateSshType.SHA256);
4354

4455
HashMD5 = new byte[16];
4556
for (var i = 0; i < HashMD5.Length; i++)
@@ -52,6 +63,12 @@ internal unsafe CertificateSsh(git_certificate_ssh* cert)
5263
{
5364
HashSHA1[i] = cert->HashSHA1[i];
5465
}
66+
67+
HashSHA256 = new byte[32];
68+
for (var i = 0; i < HashSHA256.Length; i++)
69+
{
70+
HashSHA256[i] = cert->HashSHA256[i];
71+
}
5572
}
5673

5774
internal unsafe IntPtr ToPointer()
@@ -65,6 +82,10 @@ internal unsafe IntPtr ToPointer()
6582
{
6683
sshCertType |= GitCertificateSshType.SHA1;
6784
}
85+
if (HasSHA256)
86+
{
87+
sshCertType |= GitCertificateSshType.SHA256;
88+
}
6889

6990
var gitCert = new git_certificate_ssh()
7091
{
@@ -88,6 +109,14 @@ internal unsafe IntPtr ToPointer()
88109
}
89110
}
90111

112+
fixed (byte* p = &HashSHA256[0])
113+
{
114+
for (var i = 0; i < HashSHA256.Length; i++)
115+
{
116+
gitCert.HashSHA256[i] = p[i];
117+
}
118+
}
119+
91120
var ptr = Marshal.AllocHGlobal(Marshal.SizeOf(gitCert));
92121
Marshal.StructureToPtr(gitCert, ptr, false);
93122

LibGit2Sharp/Core/NativeMethods.cs

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,37 @@ private static IntPtr ResolveDll(string libraryName, Assembly assembly, DllImpor
102102
// libc/OpenSSL libraries. Try them out.
103103
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
104104
{
105-
// The libraries are located at 'runtimes/<rid>/native/lib{libraryName}.so'
106-
// The <rid> ends with the processor architecture. e.g. fedora-x64.
107105
string assemblyDirectory = Path.GetDirectoryName(AppContext.BaseDirectory);
108106
string processorArchitecture = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();
109107
string runtimesDirectory = Path.Combine(assemblyDirectory, "runtimes");
110108

109+
// The default libgit2 binary is linked against OpenSSL 3. On hosts that have only
110+
// libcrypto.so.1.1 fall back to the OpenSSL-1.1 variant shipped alongside it. We probe
111+
// both layouts: flat (self-contained publish copies natives next to the assembly) and
112+
// 'runtimes/<rid>/native/' (framework-dependent / build output).
113+
if (!NativeLibrary.TryLoad("libcrypto.so.3", out _) && NativeLibrary.TryLoad("libcrypto.so.1.1", out _))
114+
{
115+
string variantFile = $"lib{libraryName}-openssl1.1.so";
116+
117+
string flatVariantPath = Path.Combine(assemblyDirectory, variantFile);
118+
if (NativeLibrary.TryLoad(flatVariantPath, out handle))
119+
{
120+
return handle;
121+
}
122+
123+
if (Directory.Exists(runtimesDirectory))
124+
{
125+
foreach (var runtimeFolder in Directory.GetDirectories(runtimesDirectory, $"*-{processorArchitecture}"))
126+
{
127+
string variantPath = Path.Combine(runtimeFolder, "native", variantFile);
128+
if (NativeLibrary.TryLoad(variantPath, out handle))
129+
{
130+
return handle;
131+
}
132+
}
133+
}
134+
}
135+
111136
if (Directory.Exists(runtimesDirectory))
112137
{
113138
foreach (var runtimeFolder in Directory.GetDirectories(runtimesDirectory, $"*-{processorArchitecture}"))
@@ -132,8 +157,26 @@ private static IntPtr ResolveDll(string libraryName, Assembly assembly, DllImpor
132157
[DllImport("libdl", EntryPoint = "dlopen")]
133158
private static extern IntPtr LoadUnixLibrary(string path, int flags);
134159

135-
[DllImport("kernel32", EntryPoint = "LoadLibrary")]
136-
private static extern IntPtr LoadWindowsLibrary(string path);
160+
[DllImport("kernel32", EntryPoint = "AddDllDirectory", CharSet = CharSet.Unicode)]
161+
private static extern IntPtr AddDllDirectory(string path);
162+
163+
[DllImport("kernel32", EntryPoint = "LoadLibraryExW", CharSet = CharSet.Unicode)]
164+
private static extern IntPtr LoadWindowsLibraryEx(string path, IntPtr hFile, uint flags);
165+
166+
private const uint LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000;
167+
168+
// Use AddDllDirectory + LoadLibraryEx so that transitive native dependencies
169+
// (e.g. libssh2 -> libcrypto) in the same directory are resolved at load time.
170+
private static IntPtr LoadWindowsLibrary(string path)
171+
{
172+
var directory = Path.GetDirectoryName(path);
173+
if (directory != null)
174+
{
175+
AddDllDirectory(directory);
176+
}
177+
178+
return LoadWindowsLibraryEx(path, IntPtr.Zero, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
179+
}
137180

138181
// Avoid inlining this method because otherwise mono's JITter may try
139182
// to load the library _before_ we've configured the path.

LibGit2Sharp/Core/SshExtensions.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using LibGit2Sharp.Core;
2+
using System;
3+
using System.Runtime.InteropServices;
4+
5+
namespace LibGit2Sharp.Ssh
6+
{
7+
internal static class NativeMethods
8+
{
9+
private const string libgit2 = NativeDllName.Name;
10+
11+
[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
12+
internal static extern int git_cred_ssh_key_new(
13+
out IntPtr cred,
14+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string username,
15+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string publickey,
16+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string privatekey,
17+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string passphrase);
18+
19+
[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
20+
internal static extern int git_cred_ssh_key_memory_new(
21+
out IntPtr cred,
22+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string username,
23+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string publickey,
24+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string privatekey,
25+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string passphrase);
26+
}
27+
}

LibGit2Sharp/LibGit2Sharp.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
</PropertyGroup>
3131

3232
<ItemGroup>
33-
<PackageReference Include="Octopus.LibGit2Sharp.NativeBinaries" Version="2.0.323-octopus.1" PrivateAssets="none" />
33+
<PackageReference Include="Octopus.LibGit2Sharp.NativeBinaries" Version="2.0.323-octopus.3" PrivateAssets="none" />
3434
<PackageReference Include="MinVer" Version="6.0.0" PrivateAssets="all" />
3535
</ItemGroup>
3636

0 commit comments

Comments
 (0)