-
-
Save jborean93/1ab75303c9cc45cfb383c119f2a08fe0 to your computer and use it in GitHub Desktop.
Example of how you could use RSA/ECDsa stored in Azure with Tmds.Ssh - https://github.com/tmds/Tmds.Ssh/issues/205
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
#Requires -Module Az.KeyVault | |
# Can be used to generate an RSA and ECDsa key in the vault provided. | |
# Outputs the OpenSSH public key for both keys. | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory, Position = 0)] | |
[string] | |
$VaultName | |
) | |
Function ConvertTo-OpenSshRsaPubKey { | |
[OutputType([string])] | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory)] | |
[byte[]] | |
$N, | |
[Parameter(Mandatory)] | |
[byte[]] | |
$E | |
) | |
# If the modulus has the highest bit set we need to pad the raw bytes. | |
$padding = 0 | |
if ($N[0] -band 0x80) { | |
$padding = 1 | |
} | |
[byte[]]$keyData = @( | |
[BitConverter]::GetBytes(7) | |
[Text.Encoding]::ASCII.GetBytes('ssh-rsa') | |
[BitConverter]::GetBytes($E.Length) | |
$E | |
[BitConverter]::GetBytes($N.Length + $padding) | |
if ($padding) { 0 * $padding} | |
$N | |
) | |
[Array]::Reverse($keyData, 0, 4) | |
[Array]::Reverse($keyData, 11, 4) | |
[Array]::Reverse($keyData, 15 + $E.Length, 4) | |
"ssh-rsa $([Convert]::ToBase64String($keyData))" | |
} | |
Function ConvertTo-OpenSshEcdsaPubKey { | |
[OutputType([string])] | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory)] | |
[string] | |
$KeyType, | |
[Parameter(Mandatory)] | |
[string] | |
$CurveName, | |
[Parameter(Mandatory)] | |
[byte[]] | |
$X, | |
[Parameter(Mandatory)] | |
[byte[]] | |
$Y | |
) | |
[byte[]]$keyData = @( | |
[BitConverter]::GetBytes($KeyType.Length) | |
[Text.Encoding]::ASCII.GetBytes($KeyType) | |
[BitConverter]::GetBytes($CurveName.Length) | |
[Text.Encoding]::ASCII.GetBytes($CurveName) | |
[BitConverter]::GetBytes($X.Length + $Y.Length + 1) | |
0x04 | |
$X | |
$Y | |
) | |
[Array]::Reverse($keyData, 0, 4) | |
[Array]::Reverse($keyData, $KeyType.Length + 4, 4) | |
[Array]::Reverse($keyData, $KeyType.Length + $CurveName.Length + 8, 4) | |
"$KeyType $([Convert]::ToBase64String($keyData))" | |
} | |
Connect-AzAccount | |
# Create an RSA key in Azure Key Vault | |
$rsaParams = @{ | |
VaultName = $VaultName | |
Name = 'RSAKey' | |
Destination = 'Software' | |
KeyType = 'RSA' | |
Size = 4096 | |
} | |
$rsaKey = Add-AzKeyVaultKey @rsaParams | |
ConvertTo-OpenSshRsaPubKey -N $rsaKey.Key.N -E $rsaKey.Key.E | |
$ecParams = @{ | |
VaultName = $VaultName | |
Name = 'ECDSAKey' | |
Destination = 'Software' | |
KeyType = 'EC' | |
Size = 256 | |
} | |
$ecdsaKey = Add-AzKeyVaultKey @ecParams | |
ConvertTo-OpenSshEcdsaPubKey -KeyType ecdsa-sha2-nistp256 -CurveName nistp256 -X $ecdsaKey.Key.X -Y $ecdsaKey.Key.Y |
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
using Azure.Identity; | |
using Azure.Security.KeyVault.Keys; | |
using Azure.Security.KeyVault.Keys.Cryptography; | |
using System; | |
using System.Security.Cryptography; | |
using System.Threading.Tasks; | |
using Tmds.Ssh; | |
using AzureSignatureAlgorithm = Azure.Security.KeyVault.Keys.Cryptography.SignatureAlgorithm; | |
namespace SshWithAzureExample; | |
class Program | |
{ | |
static async Task Main(string[] args) | |
{ | |
if (args.Length != 4) | |
{ | |
Console.Error.WriteLine("Usage: SshAzureKey <vaultName> <keyName> <destination> <command>"); | |
return; | |
} | |
string vaultName = args[0]; | |
string keyName = args[1]; | |
string destination = args[2]; | |
string command = args[3]; | |
PrivateKeyCredential keyCred = await GetAzureKeyCredential(vaultName, keyName); | |
var settings = new SshClientSettings(destination) | |
{ | |
UpdateKnownHostsFileAfterAuthentication = false, | |
HostAuthentication = (_1, _2, _3) => ValueTask.FromResult(true), | |
Credentials = [ keyCred ], | |
}; | |
using var client = new SshClient(settings); | |
using var process = await client.ExecuteAsync(command); | |
await foreach ((bool isError, string line) in process.ReadAllLinesAsync()) | |
{ | |
if (isError) | |
{ | |
Console.Error.WriteLine(line); | |
} | |
else | |
{ | |
Console.WriteLine(line); | |
} | |
} | |
} | |
private static async Task<PrivateKeyCredential> GetAzureKeyCredential(string vaultName, string keyName) | |
{ | |
DefaultAzureCredential cred = new(includeInteractiveCredentials: false); | |
string keyVaultUrl = $"https://{vaultName}.vault.azure.net/"; | |
KeyClient keyClient = new KeyClient(new Uri(keyVaultUrl), cred); | |
KeyVaultKey key = await keyClient.GetKeyAsync(keyName); | |
if (key.KeyType == KeyType.Rsa) | |
{ | |
CryptographyClient azureClient = new CryptographyClient(key.Id, cred); | |
RSAParameters pubParams = key.Key.ToRSA(includePrivateParameters: false) | |
.ExportParameters(false); | |
return new PrivateKeyCredential(() => new AzureRSA(azureClient, pubParams)); | |
} | |
else if (key.KeyType == KeyType.Ec) | |
{ | |
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}"), | |
}; | |
CryptographyClient azureClient = new CryptographyClient(key.Id, cred); | |
ECParameters pubParams = key.Key.ToECDsa(includePrivateParameters: false) | |
.ExportParameters(false); | |
return new PrivateKeyCredential(() => new AzureECDsa(azureClient, pubParams, sigAlgo)); | |
} | |
else | |
{ | |
throw new NotImplementedException($"Unsupported key type {key.KeyType}"); | |
} | |
} | |
} | |
sealed class AzureRSA : RSA | |
{ | |
private readonly CryptographyClient _cryptoClient; | |
private readonly RSAParameters _publicParameters; | |
public AzureRSA(CryptographyClient client, RSAParameters publicParameters) | |
{ | |
KeySizeValue = publicParameters.Modulus!.Length * 8; | |
_cryptoClient = client; | |
_publicParameters = publicParameters; | |
} | |
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}"), | |
}; | |
byte[] res = _cryptoClient.Sign(sigAlgo, hash).Signature; | |
return res; | |
} | |
} | |
sealed class AzureECDsa : ECDsa | |
{ | |
private readonly CryptographyClient _cryptoClient; | |
private readonly ECParameters _publicParameters; | |
private readonly AzureSignatureAlgorithm _signatureAlgorithm; | |
public AzureECDsa(CryptographyClient client, ECParameters publicParameters, AzureSignatureAlgorithm signatureAlgorithm) | |
{ | |
KeySizeValue = publicParameters.Q.X!.Length * 8; | |
_cryptoClient = client; | |
_publicParameters = publicParameters; | |
_signatureAlgorithm = signatureAlgorithm; | |
} | |
public override ECParameters ExportParameters(bool includePrivateParameters) | |
{ | |
if (includePrivateParameters) | |
{ | |
throw new CryptographicException("Cannot export private parameters"); | |
} | |
return _publicParameters; | |
} | |
public override byte[] SignHash(byte[] hash) | |
=> _cryptoClient.Sign(_signatureAlgorithm, hash).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
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<OutputType>Exe</OutputType> | |
<TargetFramework>net9.0</TargetFramework> | |
<Nullable>enable</Nullable> | |
<UseAppHost>false</UseAppHost> | |
</PropertyGroup> | |
<ItemGroup> | |
<ProjectReference Include="..\..\src\Tmds.Ssh\Tmds.Ssh.csproj" /> | |
</ItemGroup> | |
<ItemGroup> | |
<PackageReference Include="Azure.Identity" /> | |
<PackageReference Include="Azure.Security.KeyVault.Keys" /> | |
</ItemGroup> | |
</Project> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment