Skip to content

Commit

Permalink
Add Azure Key example (#226)
Browse files Browse the repository at this point in the history
Adds an example of using this library with a private key hosted in Azure Key Vault.
  • Loading branch information
jborean93 authored Sep 16, 2024
1 parent d0409d5 commit 1d63109
Show file tree
Hide file tree
Showing 10 changed files with 477 additions and 4 deletions.
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="Tmds.ExecFunction" Version="0.7.1" />
<!-- Example dependencies -->
<PackageVersion Include="Azure.Identity" Version="1.12.0" />
<PackageVersion Include="Azure.Security.KeyVault.Keys" Version="4.6.0" />
</ItemGroup>
</Project>
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ $ dotnet run
hello world!
```

## Examples

The following are some example projects that show how Tmds.Ssh can be used:

- [scp](./examples/scp) - SCP client to copy/fetch files
- [ssh](./examples/ssh) - SSH client
- [azure_key](./examples/azure_key) - SSH client with private keys stored in Azure Key Vault.

## API

```cs
Expand Down
47 changes: 47 additions & 0 deletions examples/azure_key/AzureECDsaKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Azure.Security.KeyVault.Keys.Cryptography;
using System;
using System.Security.Cryptography;
using System.Threading;
using AzureSignatureAlgorithm = Azure.Security.KeyVault.Keys.Cryptography.SignatureAlgorithm;

namespace Tmds.Ssh.AzureKeyExample;

sealed class AzureECDsaKey : ECDsa
{
private readonly CryptographyClient _cryptoClient;
private readonly ECParameters _publicParameters;
private readonly AzureSignatureAlgorithm _signatureAlgorithm;
private readonly CancellationToken _cancellationToken;

public AzureECDsaKey(
CryptographyClient client,
ECParameters publicParameters,
AzureSignatureAlgorithm signatureAlgorithm,
CancellationToken cancellationToken)
{
KeySizeValue = publicParameters.Q.X!.Length * 8;
_cryptoClient = client;
_publicParameters = publicParameters;
_signatureAlgorithm = signatureAlgorithm;
_cancellationToken = cancellationToken;
}

public override ECParameters ExportParameters(bool includePrivateParameters)
{
if (includePrivateParameters)
{
throw new CryptographicException("Cannot export private parameters");
}

return _publicParameters;
}

public override byte[] SignHash(byte[] hash)
=> _cryptoClient.SignAsync(
_signatureAlgorithm,
hash,
_cancellationToken).GetAwaiter().GetResult().Signature;

public override bool VerifyHash(byte[] hash, byte[] signature)
=> throw new NotImplementedException();
}
55 changes: 55 additions & 0 deletions examples/azure_key/AzureKeyCredential.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Azure.Core;
using Azure.Security.KeyVault.Keys;
using Azure.Security.KeyVault.Keys.Cryptography;
using System;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using AzureSignatureAlgorithm = Azure.Security.KeyVault.Keys.Cryptography.SignatureAlgorithm;

namespace Tmds.Ssh.AzureKeyExample;

sealed class AzureKeyCredential : PrivateKeyCredential
{
public AzureKeyCredential(TokenCredential credential, KeyVaultKey key) :
base((c) => GetAzureKeyAsync(credential, key, c), $"azure:{key.Id}")
{ }

private static ValueTask<Key> GetAzureKeyAsync(
TokenCredential credential,
KeyVaultKey key,
CancellationToken cancellationToken = default)
{
CryptographyClient azureClient = new CryptographyClient(key.Id, credential);

Key privateKey;
if (key.KeyType == KeyType.Rsa)
{
RSAParameters pubParams = key.Key.ToRSA(includePrivateParameters: false)
.ExportParameters(false);

privateKey = new Key(new AzureRsaKey(azureClient, pubParams, cancellationToken));
}
else if (key.KeyType == KeyType.Ec)
{
ECParameters pubParams = key.Key.ToECDsa(includePrivateParameters: false)
.ExportParameters(false);

AzureSignatureAlgorithm sigAlgo = key.Key.CurveName.ToString() switch
{
"P-256" => AzureSignatureAlgorithm.ES256,
"P-384" => AzureSignatureAlgorithm.ES384,
"P-521" => AzureSignatureAlgorithm.ES512,
_ => throw new NotImplementedException($"Unsupported curve {key.Key.CurveName}"),
};

privateKey = new Key(new AzureECDsaKey(azureClient, pubParams, sigAlgo, cancellationToken));
}
else
{
throw new NotImplementedException($"Unsupported Azure key type {key.KeyType}");
}

return ValueTask.FromResult(privateKey);
}
}
59 changes: 59 additions & 0 deletions examples/azure_key/AzureRsaKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Azure.Security.KeyVault.Keys.Cryptography;
using System;
using System.Security.Cryptography;
using System.Threading;
using AzureSignatureAlgorithm = Azure.Security.KeyVault.Keys.Cryptography.SignatureAlgorithm;

namespace Tmds.Ssh.AzureKeyExample;

sealed class AzureRsaKey : RSA
{
private readonly CryptographyClient _cryptoClient;
private readonly RSAParameters _publicParameters;
private readonly CancellationToken _cancellationToken;

public AzureRsaKey(
CryptographyClient client,
RSAParameters publicParameters,
CancellationToken cancellationToken)
{
KeySizeValue = publicParameters.Modulus!.Length * 8;
_cryptoClient = client;
_publicParameters = publicParameters;
_cancellationToken = cancellationToken;
}

public override RSAParameters ExportParameters(bool includePrivateParameters)
{
if (includePrivateParameters)
{
throw new CryptographicException("Cannot export private parameters");
}

return _publicParameters;
}

public override void ImportParameters(RSAParameters parameters)
=> throw new NotImplementedException();

public override byte[] SignHash(byte[] hash, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
{
if (padding != RSASignaturePadding.Pkcs1)
{
throw new CryptographicException($"Unsupported padding {padding}");
}

AzureSignatureAlgorithm sigAlgo = hashAlgorithm.Name switch
{
"SHA256" => AzureSignatureAlgorithm.RS256,
"SHA512" => AzureSignatureAlgorithm.RS512,
_ => throw new CryptographicException($"Unsupported hash algorithm {hashAlgorithm.Name}"),
};

SignResult res = _cryptoClient.SignAsync(
sigAlgo,
hash,
_cancellationToken).GetAwaiter().GetResult();
return res.Signature;
}
}
188 changes: 188 additions & 0 deletions examples/azure_key/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
using Azure.Core;
using Azure.Identity;
using Azure.Security.KeyVault.Keys;
using System;
using System.Buffers.Binary;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace Tmds.Ssh.AzureKeyExample;

class Program
{
static async Task<int> Main(string[] args)
{
if (args.Length == 0 || (args[0] != "ssh" && args[0] != "print_pub_key"))
{
Console.Error.WriteLine("Usage: azure_key {ssh,print_pub_key} <vaultName> <keyName>");
return 255;
}

string action = args[0];
if (args.Length < 3)
{
if (action == "ssh")
{
Console.Error.WriteLine("Usage: azure_key ssh <vaultName> <keyName> [<destination>] [<command>]");
}
else
{
Console.Error.WriteLine("Usage: azure_key print_pub_key <vaultName> <keyName>");
}
return 255;
}

string vaultName = args[1];
string keyName = args[2];

DefaultAzureCredential cred = new(includeInteractiveCredentials: true);
string keyVaultUrl = $"https://{vaultName}.vault.azure.net/";
KeyClient keyClient = new KeyClient(new Uri(keyVaultUrl), cred);
KeyVaultKey key = await keyClient.GetKeyAsync(keyName);

if (action == "print_pub_key")
{
return PrintPublicKeyAsync(key);
}
else
{
string destination = args.Length >= 4 ? args[3] : "localhost";
string command = args.Length >= 5 ? args[4] : "echo 'hello world'";
return await SshExecAsync(cred, key, destination, command);
}
}

private static int PrintPublicKeyAsync(KeyVaultKey key)
{
string pubKey;
if (key.KeyType == KeyType.Rsa)
{
RSAParameters pubParams = key.Key.ToRSA(includePrivateParameters: false)
.ExportParameters(false);
pubKey = GetRsaPubKey(pubParams);
}
else if (key.KeyType == KeyType.Ec)
{
ECParameters pubParams = key.Key.ToECDsa(includePrivateParameters: false)
.ExportParameters(false);
pubKey = GetEcdsaPubKey(pubParams);
}
else
{
throw new NotImplementedException($"Unsupported Azure key type {key.KeyType}");
}

Console.WriteLine(pubKey);
return 0;
}

private static async Task<int> SshExecAsync(TokenCredential credential, KeyVaultKey key, string destination, string command)
{
SshClientSettings clientSettings = new SshClientSettings(destination)
{
Credentials = [new AzureKeyCredential(credential, key)],
};
using SshClient client = new SshClient(clientSettings);

using var process = await client.ExecuteAsync(command);
Task[] tasks = new[]
{
PrintToConsole(process),
ReadInputFromConsole(process)
};
Task.WaitAny(tasks);
PrintExceptions(tasks);

return process.ExitCode;

static async Task PrintToConsole(RemoteProcess process)
{
await foreach ((bool isError, string line) in process.ReadAllLinesAsync())
{
Console.WriteLine(line);
}
}

static async Task ReadInputFromConsole(RemoteProcess process)
{
// note: Console doesn't have an async ReadLine that accepts a CancellationToken...
await Task.Yield();
var cancellationToken = process.ExecutionAborted;
while (!cancellationToken.IsCancellationRequested)
{
string? line = Console.ReadLine();
if (line == null)
{
break;
}
await process.WriteLineAsync(line);
}
}

static void PrintExceptions(Task[] tasks)
{
foreach (var task in tasks)
{
Exception? innerException = task.Exception?.InnerException;
if (innerException is not null)
{
System.Console.WriteLine("Exception:");
Console.WriteLine(innerException);
}
}
}
}

private static string GetRsaPubKey(RSAParameters pubParams)
{
byte[] n = pubParams.Modulus!;
byte[] e = pubParams.Exponent!;

// If the modulus has the highest bit set, we need to pad it with a 0
// byte.
int padding = 0;
if ((n[0] & 0x80) != 0)
{
padding = 1;
}

Span<byte> keyData = stackalloc byte[4 + 7 + 4 + e.Length + 4 + padding + n.Length];
BinaryPrimitives.WriteInt32BigEndian(keyData, 7);
Encoding.ASCII.GetBytes("ssh-rsa", keyData.Slice(4));
BinaryPrimitives.WriteInt32BigEndian(keyData.Slice(11), e.Length);
e.CopyTo(keyData.Slice(15, e.Length));
BinaryPrimitives.WriteInt32BigEndian(keyData.Slice(15 + e.Length), n.Length + padding);
keyData[19 + e.Length] = 0;
n.CopyTo(keyData.Slice(19 + e.Length + padding));

return $"ssh-rsa {Convert.ToBase64String(keyData)}";
}

private static string GetEcdsaPubKey(ECParameters pubParams)
{
byte[] x = pubParams.Q.X!;
byte[] y = pubParams.Q.Y!;

string curveName = pubParams.Curve.Oid?.FriendlyName switch
{
"ECDSA_P256" => "nistp256",
"ECDSA_P384" => "nistp384",
"ECDSA_P521" => "nistp521",
_ => throw new NotImplementedException($"Unsupported ECDSA curve {pubParams.Curve.Oid?.FriendlyName}"),
};
string keyType = $"ecdsa-sha2-{curveName}";

Span<byte> keyData = stackalloc byte[4 + keyType.Length + 4 + curveName.Length + 4 + 1 + x.Length + y.Length];
BinaryPrimitives.WriteInt32BigEndian(keyData, keyType.Length);
Encoding.ASCII.GetBytes(keyType, keyData.Slice(4));
BinaryPrimitives.WriteInt32BigEndian(keyData.Slice(4 + keyType.Length), curveName.Length);
Encoding.ASCII.GetBytes(curveName, keyData.Slice(8 + keyType.Length));
BinaryPrimitives.WriteInt32BigEndian(keyData.Slice(8 + keyType.Length + curveName.Length), x.Length + y.Length + 1);
keyData[12 + keyType.Length + curveName.Length] = 0x04;
x.CopyTo(keyData.Slice(13 + keyType.Length + curveName.Length));
y.CopyTo(keyData.Slice(13 + keyType.Length + curveName.Length + x.Length));

return $"{keyType} {Convert.ToBase64String(keyData)}";
}
}
Loading

0 comments on commit 1d63109

Please sign in to comment.