Skip to content

Instantly share code, notes, and snippets.

@jborean93
Last active August 19, 2024 03:22
Show Gist options
  • Save jborean93/1ab75303c9cc45cfb383c119f2a08fe0 to your computer and use it in GitHub Desktop.
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
#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
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();
}
}
<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