-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds an example of using this library with a private key hosted in Azure Key Vault.
- Loading branch information
Showing
10 changed files
with
477 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)}"; | ||
} | ||
} |
Oops, something went wrong.