Olá pessoal,
Recentemente encontrei um projeto no github que usava apenas o C# e o .net para gerar uma chave SSH (publica e privada). O projeto se chama SshKeyGenerator e achei bem interessante a abordagem, e resolvi portar isso para powershell usando Classes e uma funcão (cmdlet) para encapsular a chamada. O objetivo dele é nos ajudar gerando chaves ssh de maneira que não tenha dependência de executáveis externos, que muitas vezes se torna um problema quando estamos em contextos de automação, por exemplo dentro de um container.
O projeto usa o RSACryptoServiceProvider para gerar a chave e converter para os valores adequados.
A função foi escrita de maneira que uma vez em memória, as chaves possam ser geradas com apenas uma linha:
Retrieve input parameter default values for the 'path/to/workflow' workflow.
New-SSHRSAKey -KeySize 2048 -Comment 'guido.oliveira' -OutputType Base64
Retrieve input parameter default values for the 'path/to/workflow' workflow.
New-SSHRSAKey -KeySize 2048 -Comment 'ssh.test' -OutputType File
Retrieve input parameter default values for the 'path/to/workflow' workflow.
New-SSHRSAKey -KeySize 2048 -Comment 'ssh.test' -OutputType File -FilePath 'path/to/file'
Retrieve input parameter default values for the 'path/to/workflow' workflow.
New-SSHRSAKey -KeySize 2048 -Comment 'guido.oliveira' -OutputType Base64
A classe em powershell ficou da seguinte maneira:
class SshKeyGenerator : System.IDisposable {
hidden [System.Security.Cryptography.RSACryptoServiceProvider]$csp
SshKeyGenerator([int] $keySize) {
$this.csp =[System.Security.Cryptography.RSACryptoServiceProvider]::new($keySize)
}
# Returns the private key in x509 format
[string] ToPrivateKey() {
if ($this.csp.PublicOnly) {
#throw new ArgumentException("CSP does not contain a private key", nameof($this.csp));
}
$parameters = $this.csp.ExportParameters($true);
$stream = [System.IO.MemoryStream]::new()
$writer = [System.IO.BinaryWriter]::new($stream)
$writer.Write([byte]0x30)
$innerStream = [System.IO.MemoryStream]::new()
$innerWriter = [System.IO.BinaryWriter]::new($innerStream)
$this.EncodeIntegerBigEndian($innerWriter, [byte[]]::new(0x00), $true) # Version
$this.EncodeIntegerBigEndian($innerWriter, $parameters.Modulus, $true)
$this.EncodeIntegerBigEndian($innerWriter, $parameters.Exponent, $true)
$this.EncodeIntegerBigEndian($innerWriter, $parameters.D, $true)
$this.EncodeIntegerBigEndian($innerWriter, $parameters.P, $true)
$this.EncodeIntegerBigEndian($innerWriter, $parameters.Q, $true)
$this.EncodeIntegerBigEndian($innerWriter, $parameters.DP, $true)
$this.EncodeIntegerBigEndian($innerWriter, $parameters.DQ, $true)
$this.EncodeIntegerBigEndian($innerWriter, $parameters.InverseQ, $true)
$length = [int]$innerStream.Length
$this.EncodeLength($writer, $length)
$writer.Write($innerStream.GetBuffer(), 0, $length)
$innerStream.Dispose()
$base64 = [System.Convert]::ToBase64String($stream.GetBuffer(), 0, [int]$stream.Length).ToCharArray();
$outputStream = [System.IO.StringWriter]::new()
$outputStream.WriteLine("-----BEGIN RSA PRIVATE KEY-----")
# Output as Base64 with lines chopped at 64 characters
for ($i = 0; $i -lt $base64.Length; $i += 64) {
$outputStream.WriteLine($base64, $i, [System.Math]::Min(64, $base64.Length - $i))
}
$outputStream.WriteLine("-----END RSA PRIVATE KEY-----")
return $outputStream.ToString()
$outputStream.Dispose()
$stream.Dispose()
}
# Export blobs to save or import directly into another CSP object
[byte[]] ToBlobs([bool] $includePrivateKey) {
return $this.csp.ExportCspBlob($includePrivateKey);
}
# Export blobs in base64 format to save or import directly into another CSP object
[string] ToB64Blob([bool] $includePrivateKey) {
return [System.Convert]::ToBase64String($this.ToBlobs($includePrivateKey));
}
# Export Csp as XML string. This XML contains both private key and public key (P and Q).
[string] ToXml() {
return $this.csp.ToXmlString($true);
}
# Returns the SSH public key in RFC4716 format
[string] ToRfcPublicKey([string] $comment) {
[byte[]] $sshrsaBytes = [System.Text.Encoding]::Default.GetBytes('ssh-rsa')
[byte[]] $n = $this.csp.ExportParameters($false).Modulus;
[byte[]] $e = $this.csp.ExportParameters($false).Exponent;
[string] $buffer64 = ''
[System.IO.MemoryStream] $ms = [System.IO.MemoryStream]::new()
$ms.Write($this.ToBytes($sshrsaBytes.Length), 0, 4)
$ms.Write($sshrsaBytes, 0, $sshrsaBytes.Length)
$ms.Write($this.ToBytes($e.Length), 0, 4)
$ms.Write($e, 0, $e.Length)
$ms.Write($this.ToBytes($n.Length + 1), 0, 4) # Remove the +1 if not Emulating Putty Gen
#$ms.Write(new byte[] {0}, 0, 1) # Add a 0 to Emulate PuttyGen
$ms.Write( [byte[]]$(0, 0, 1) ) # Add a 0 to Emulate PuttyGen
$ms.Write($n, 0, $n.Length)
$ms.Flush()
$buffer64 = [System.Convert]::ToBase64String($ms.ToArray())
$ms.Dispose()
return "ssh-rsa $buffer64 $comment"
}
# Returns the SSH public key in RFC4716 format
[string] ToRfcPublicKey() {
return $this.ToRfcPublicKey("generated-key")
}
# Returns the SSH public key in x509 format
[string] ToPublicKey() {
$parameters = $this.csp.ExportParameters($false)
$stream = [System.IO.MemoryStream]::new()
$writer = [System.IO.BinaryWriter]::new($stream)
$writer.Write([byte]0x30) # SEQUENCE
$innerStream = [System.IO.MemoryStream]::new()
$innerWriter = [System.IO.BinaryWriter]::new($innerStream)
$innerWriter.Write([byte]0x30) # SEQUENCE
$this.EncodeLength($innerWriter, 13)
$innerWriter.Write([byte]0x06) # OBJECT IDENTIFIER
$rsaEncryptionOid = [byte[]]$(0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01)
$this.EncodeLength($innerWriter, $rsaEncryptionOid.Length)
$innerWriter.Write($rsaEncryptionOid)
$innerWriter.Write([byte]0x05) # NULL
$this.EncodeLength($innerWriter, 0)
$innerWriter.Write([byte] 0x03) # BIT STRING
$bitStringStream = [System.IO.MemoryStream]::new()
$bitStringWriter = [System.IO.BinaryWriter]::new($bitStringStream)
$bitStringWriter.Write([byte]0x00) # of unused bits
$bitStringWriter.Write([byte]0x30) # SEQUENCE
$paramsStream = [System.IO.MemoryStream]::new()
$paramsWriter = [System.IO.BinaryWriter]::new($paramsStream)
$this.EncodeIntegerBigEndian($paramsWriter, $parameters.Modulus, $true) # Modulus
$this.EncodeIntegerBigEndian($paramsWriter, $parameters.Exponent, $true) # Exponent
$paramsLength = [int]$paramsStream.Length
$this.EncodeLength($bitStringWriter, $paramsLength)
$bitStringWriter.Write($paramsStream.GetBuffer(), 0, $paramsLength)
$paramsStream.Dispose()
$bitStringLength = [int]$bitStringStream.Length
$this.EncodeLength($innerWriter, $bitStringLength)
$innerWriter.Write($bitStringStream.GetBuffer(), 0, $bitStringLength)
$bitStringStream.Dispose()
$length = [int]$innerStream.Length
$this.EncodeLength($writer, $length)
$writer.Write($innerStream.GetBuffer(), 0, $length);
$innerStream.Dispose()
$base64 = [System.Convert]::ToBase64String($stream.GetBuffer(), 0, [int]$stream.Length).ToCharArray()
$outputStream = [System.IO.StringWriter]::new()
$outputStream.WriteLine("-----BEGIN PUBLIC KEY-----")
for ($i = 0; $i -lt $base64.Length; $i += 64) {
$outputStream.WriteLine($base64, $i, [System.Math]::Min(64, $base64.Length - $i))
}
$outputStream.WriteLine("-----END PUBLIC KEY-----")
return $outputStream.ToString()
$outputStream.Dispose()
$stream.Dispose()
}
hidden [void] EncodeLength([System.IO.BinaryWriter]$stream, [int]$length) {
if ($length -lt 0) {
#throw new ArgumentOutOfRangeException(nameof(length), "Length must be non-negative")
throw [System.ArgumentOutOfRangeException]::new("Length must be non-negative")
}
if ($length -lt 0x80) {
# Short form
$stream.Write([byte] $length)
} else {
# Long form
$temp = $length
$bytesRequired = 0
while ($temp > 0) {
$temp -shr 8
$bytesRequired++
}
$stream.Write([byte] ($bytesRequired -bor 0x80))
for ($i = $bytesRequired - 1; $i -ge 0; $i--) {
$stream.Write([byte] ($length -shr (8 * $i) -band 0xff))
}
}
}
hidden [void] EncodeIntegerBigEndian([System.IO.BinaryWriter] $stream, [byte[]] $value, [bool] $forceUnsigned = $true) {
$stream.Write([byte] 0x02) # INTEGER
$prefixZeros = 0
for ($i = 0; $i -lt $value.Length; $i++) {
if ($value[$i] -ne 0) { break }
$prefixZeros++
}
if ($value.Length - $prefixZeros -eq 0) {
$this.EncodeLength($stream, 1)
$stream.Write([byte] 0)
} else {
if ($forceUnsigned -and $value[$prefixZeros] -gt 0x7f) {
# Add a prefix zero to force unsigned if the MSB is 1
$this.EncodeLength($stream, $value.Length - $prefixZeros + 1)
$stream.Write([byte]0)
} else {
$this.EncodeLength($stream, $value.Length - $prefixZeros)
}
for ($i = $prefixZeros; $i -lt $value.Length; $i++) {
$stream.Write($value[$i])
}
}
}
hidden [byte[]] ToBytes([int] $i) {
[byte[]] $bts = [System.BitConverter]::GetBytes($i)
if ([System.BitConverter]::IsLittleEndian) {
[Array]::Reverse($bts)
}
return $bts
}
[void] Dispose() {
$this.Dispose($true);
[System.GC]::SuppressFinalize($this)
}
hidden [void] Dispose([bool]$disposing) {
if ($disposing) {
$this.csp?.Dispose()
}
}
}
A funcão que escrevi para fazer as chamadas a classe ficou da seguinte forma:
function New-SSHRSAKey {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0, HelpMessage = "The size of the key to generate, 1024, 2048 or 4096")]
[ValidateNotNull()]
[ValidateNotNullOrEmpty()]
[ValidateSet("1024", "2048", "4096")]
[int]$KeySize,
[Parameter(Mandatory = $false, Position = 1, HelpMessage = "The Comment to use for the public key, defaults to ssh-key")]
[string]$Comment = 'ssh-key',
[Parameter(Mandatory = $false, Position = 2, HelpMessage = "Output Type of the command, defaults to Object")]
[ValidateSet('File', 'XML', 'Base64', 'Blob', 'Object')]
[string]$OutputType = 'Object'
)
DynamicParam {
if ($OutputType -eq 'File') {
$SegmentAttribute = New-Object -TypeName System.Management.Automation.ParameterAttribute -Property @{
Position = 3
Mandatory = $false
HelpMessage = ('The complete File Path without extension to the file to , defaults to {0}\.ssh\id_rsa - {1}' -f $HOME, $Comment)
}
#create an attributecollection object for the attribute we just created.
$attributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute]
#add our custom attribute
$attributeCollection.Add($SegmentAttribute)
#add our paramater specifying the attribute collection
$SegmentParameter = New-Object System.Management.Automation.RuntimeDefinedParameter('FilePath', [String], $attributeCollection)
#expose the name of our parameter
$paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$paramDictionary.Add('FilePath', $SegmentParameter)
return $paramDictionary
}
}
begin {
class SshKeyGenerator : System.IDisposable {
hidden [System.Security.Cryptography.RSACryptoServiceProvider]$csp
SshKeyGenerator([int] $keySize) {
$this.csp =[System.Security.Cryptography.RSACryptoServiceProvider]::new($keySize)
}
# Returns the private key in x509 format
[string] ToPrivateKey() {
if ($this.csp.PublicOnly) {
#throw new ArgumentException("CSP does not contain a private key", nameof($this.csp));
}
$parameters = $this.csp.ExportParameters($true);
$stream = [System.IO.MemoryStream]::new()
$writer = [System.IO.BinaryWriter]::new($stream)
$writer.Write([byte]0x30)
$innerStream = [System.IO.MemoryStream]::new()
$innerWriter = [System.IO.BinaryWriter]::new($innerStream)
$this.EncodeIntegerBigEndian($innerWriter, [byte[]]::new(0x00), $true) # Version
$this.EncodeIntegerBigEndian($innerWriter, $parameters.Modulus, $true)
$this.EncodeIntegerBigEndian($innerWriter, $parameters.Exponent, $true)
$this.EncodeIntegerBigEndian($innerWriter, $parameters.D, $true)
$this.EncodeIntegerBigEndian($innerWriter, $parameters.P, $true)
$this.EncodeIntegerBigEndian($innerWriter, $parameters.Q, $true)
$this.EncodeIntegerBigEndian($innerWriter, $parameters.DP, $true)
$this.EncodeIntegerBigEndian($innerWriter, $parameters.DQ, $true)
$this.EncodeIntegerBigEndian($innerWriter, $parameters.InverseQ, $true)
$length = [int]$innerStream.Length
$this.EncodeLength($writer, $length)
$writer.Write($innerStream.GetBuffer(), 0, $length)
$innerStream.Dispose()
$base64 = [System.Convert]::ToBase64String($stream.GetBuffer(), 0, [int]$stream.Length).ToCharArray();
$outputStream = [System.IO.StringWriter]::new()
$outputStream.WriteLine("-----BEGIN RSA PRIVATE KEY-----")
# Output as Base64 with lines chopped at 64 characters
for ($i = 0; $i -lt $base64.Length; $i += 64) {
$outputStream.WriteLine($base64, $i, [System.Math]::Min(64, $base64.Length - $i))
}
$outputStream.WriteLine("-----END RSA PRIVATE KEY-----")
return $outputStream.ToString()
$outputStream.Dispose()
$stream.Dispose()
}
# Export blobs to save or import directly into another CSP object
[byte[]] ToBlobs([bool] $includePrivateKey) {
return $this.csp.ExportCspBlob($includePrivateKey);
}
# Export blobs in base64 format to save or import directly into another CSP object
[string] ToB64Blob([bool] $includePrivateKey) {
return [System.Convert]::ToBase64String($this.ToBlobs($includePrivateKey));
}
# Export Csp as XML string. This XML contains both private key and public key (P and Q).
[string] ToXml() {
return $this.csp.ToXmlString($true);
}
# Returns the SSH public key in RFC4716 format
[string] ToRfcPublicKey([string] $comment) {
[byte[]] $sshrsaBytes = [System.Text.Encoding]::Default.GetBytes('ssh-rsa')
[byte[]] $n = $this.csp.ExportParameters($false).Modulus;
[byte[]] $e = $this.csp.ExportParameters($false).Exponent;
[string] $buffer64 = ''
[System.IO.MemoryStream] $ms = [System.IO.MemoryStream]::new()
$ms.Write($this.ToBytes($sshrsaBytes.Length), 0, 4)
$ms.Write($sshrsaBytes, 0, $sshrsaBytes.Length)
$ms.Write($this.ToBytes($e.Length), 0, 4)
$ms.Write($e, 0, $e.Length)
$ms.Write($this.ToBytes($n.Length + 1), 0, 4) # Remove the +1 if not Emulating Putty Gen
#$ms.Write(new byte[] {0}, 0, 1) # Add a 0 to Emulate PuttyGen
$ms.Write( [byte[]]$(0, 0, 1) ) # Add a 0 to Emulate PuttyGen
$ms.Write($n, 0, $n.Length)
$ms.Flush()
$buffer64 = [System.Convert]::ToBase64String($ms.ToArray())
$ms.Dispose()
return "ssh-rsa $buffer64 $comment"
}
# Returns the SSH public key in RFC4716 format
[string] ToRfcPublicKey() {
return $this.ToRfcPublicKey("generated-key")
}
# Returns the SSH public key in x509 format
[string] ToPublicKey() {
$parameters = $this.csp.ExportParameters($false)
$stream = [System.IO.MemoryStream]::new()
$writer = [System.IO.BinaryWriter]::new($stream)
$writer.Write([byte]0x30) # SEQUENCE
$innerStream = [System.IO.MemoryStream]::new()
$innerWriter = [System.IO.BinaryWriter]::new($innerStream)
$innerWriter.Write([byte]0x30) # SEQUENCE
$this.EncodeLength($innerWriter, 13)
$innerWriter.Write([byte]0x06) # OBJECT IDENTIFIER
$rsaEncryptionOid = [byte[]]$(0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01)
$this.EncodeLength($innerWriter, $rsaEncryptionOid.Length)
$innerWriter.Write($rsaEncryptionOid)
$innerWriter.Write([byte]0x05) # NULL
$this.EncodeLength($innerWriter, 0)
$innerWriter.Write([byte] 0x03) # BIT STRING
$bitStringStream = [System.IO.MemoryStream]::new()
$bitStringWriter = [System.IO.BinaryWriter]::new($bitStringStream)
$bitStringWriter.Write([byte]0x00) # of unused bits
$bitStringWriter.Write([byte]0x30) # SEQUENCE
$paramsStream = [System.IO.MemoryStream]::new()
$paramsWriter = [System.IO.BinaryWriter]::new($paramsStream)
$this.EncodeIntegerBigEndian($paramsWriter, $parameters.Modulus, $true) # Modulus
$this.EncodeIntegerBigEndian($paramsWriter, $parameters.Exponent, $true) # Exponent
$paramsLength = [int]$paramsStream.Length
$this.EncodeLength($bitStringWriter, $paramsLength)
$bitStringWriter.Write($paramsStream.GetBuffer(), 0, $paramsLength)
$paramsStream.Dispose()
$bitStringLength = [int]$bitStringStream.Length
$this.EncodeLength($innerWriter, $bitStringLength)
$innerWriter.Write($bitStringStream.GetBuffer(), 0, $bitStringLength)
$bitStringStream.Dispose()
$length = [int]$innerStream.Length
$this.EncodeLength($writer, $length)
$writer.Write($innerStream.GetBuffer(), 0, $length);
$innerStream.Dispose()
$base64 = [System.Convert]::ToBase64String($stream.GetBuffer(), 0, [int]$stream.Length).ToCharArray()
$outputStream = [System.IO.StringWriter]::new()
$outputStream.WriteLine("-----BEGIN PUBLIC KEY-----")
for ($i = 0; $i -lt $base64.Length; $i += 64) {
$outputStream.WriteLine($base64, $i, [System.Math]::Min(64, $base64.Length - $i))
}
$outputStream.WriteLine("-----END PUBLIC KEY-----")
return $outputStream.ToString()
$outputStream.Dispose()
$stream.Dispose()
}
hidden [void] EncodeLength([System.IO.BinaryWriter]$stream, [int]$length) {
if ($length -lt 0) {
#throw new ArgumentOutOfRangeException(nameof(length), "Length must be non-negative")
throw [System.ArgumentOutOfRangeException]::new("Length must be non-negative")
}
if ($length -lt 0x80) {
# Short form
$stream.Write([byte] $length)
} else {
# Long form
$temp = $length
$bytesRequired = 0
while ($temp > 0) {
$temp -shr 8
$bytesRequired++
}
$stream.Write([byte] ($bytesRequired -bor 0x80))
for ($i = $bytesRequired - 1; $i -ge 0; $i--) {
$stream.Write([byte] ($length -shr (8 * $i) -band 0xff))
}
}
}
hidden [void] EncodeIntegerBigEndian([System.IO.BinaryWriter] $stream, [byte[]] $value, [bool] $forceUnsigned = $true) {
$stream.Write([byte] 0x02) # INTEGER
$prefixZeros = 0
for ($i = 0; $i -lt $value.Length; $i++) {
if ($value[$i] -ne 0) { break }
$prefixZeros++
}
if ($value.Length - $prefixZeros -eq 0) {
$this.EncodeLength($stream, 1)
$stream.Write([byte] 0)
} else {
if ($forceUnsigned -and $value[$prefixZeros] -gt 0x7f) {
# Add a prefix zero to force unsigned if the MSB is 1
$this.EncodeLength($stream, $value.Length - $prefixZeros + 1)
$stream.Write([byte]0)
} else {
$this.EncodeLength($stream, $value.Length - $prefixZeros)
}
for ($i = $prefixZeros; $i -lt $value.Length; $i++) {
$stream.Write($value[$i])
}
}
}
hidden [byte[]] ToBytes([int] $i) {
[byte[]] $bts = [System.BitConverter]::GetBytes($i)
if ([System.BitConverter]::IsLittleEndian) {
[Array]::Reverse($bts)
}
return $bts
}
[void] Dispose() {
$this.Dispose($true);
[System.GC]::SuppressFinalize($this)
}
hidden [void] Dispose([bool]$disposing) {
if ($disposing) {
$this.csp?.Dispose()
}
}
}
$sshkey = [SshKeyGenerator]::new($KeySize)
}
process {
switch ($OutputType) {
'File' {
if ($FilePath) {
[void](New-Item -Path $FilePath -ItemType File -Value $sshkey.ToPrivateKey())
[void](New-Item -Path "$FilePath.pub" -ItemType File -Value $sshkey.ToRfcPublicKey($Comment))
} else {
[void](New-Item -Path (Join-Path -Path $HOME -ChildPath .ssh) -Name "id_rsa - $Comment" -ItemType File -Value $sshkey.ToPrivateKey())
[void](New-Item -Path (Join-Path -Path $HOME -ChildPath .ssh) -Name "id_rsa - $Comment.pub" -ItemType File -Value $sshkey.ToRfcPublicKey($Comment))
}
return $sshkey.ToRfcPublicKey($Comment)
}
'XML' {
return $sshkey.ToXml()
}
'Base64' {
return $sshkey.ToB64Blob($true)
}
'Blob' {
return $sshkey.ToBlobs($true)
}
Default {
$obj = [pscustomobject][ordered]@{
PublicKey = $sshkey.ToPublicKey()
RfcPublicKey = $sshkey.ToRfcPublicKey($Comment)
PrivateKey = $sshkey.ToPrivateKey()
XML = $sshkey.ToXml()
Base64 = $sshkey.ToB64Blob($true)
Blob = $sshkey.ToBlobs($true)
}
return $obj
}
}
}
end {
$sshkey.Dispose()
}
}
Como a classe suporta varios tipos de tentei dar essa flexibilidade ao comando de forma simplificada.
Dúvidas? Sugestões? Comente!
Até a próxima!