-
-
Notifications
You must be signed in to change notification settings - Fork 940
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Private Key] Add support for PuTTY private key file format (V3 and V…
…2) (#1543) * [Private Key] Add support for PuTTY private key * add negative test for mac --------- Co-authored-by: Rob Hague <[email protected]>
- Loading branch information
Showing
20 changed files
with
678 additions
and
15 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
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,273 @@ | ||
#nullable enable | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.Linq; | ||
using System.Security.Cryptography; | ||
using System.Text; | ||
|
||
using Org.BouncyCastle.Crypto.Generators; | ||
using Org.BouncyCastle.Crypto.Parameters; | ||
|
||
using Renci.SshNet.Abstractions; | ||
using Renci.SshNet.Common; | ||
using Renci.SshNet.Security; | ||
using Renci.SshNet.Security.Cryptography.Ciphers; | ||
|
||
namespace Renci.SshNet | ||
{ | ||
public partial class PrivateKeyFile | ||
{ | ||
private sealed class PuTTY : IPrivateKeyParser | ||
{ | ||
private readonly string _version; | ||
private readonly string _algorithmName; | ||
private readonly string _encryptionType; | ||
private readonly string _comment; | ||
private readonly byte[] _publicKey; | ||
private readonly string? _argon2Type; | ||
private readonly string? _argon2Salt; | ||
private readonly string? _argon2Iterations; | ||
private readonly string? _argon2Memory; | ||
private readonly string? _argon2Parallelism; | ||
private readonly byte[] _data; | ||
private readonly string _mac; | ||
private readonly string? _passPhrase; | ||
|
||
public PuTTY(string version, string algorithmName, string encryptionType, string comment, byte[] publicKey, string? argon2Type, string? argon2Salt, string? argon2Iterations, string? argon2Memory, string? argon2Parallelism, byte[] data, string mac, string? passPhrase) | ||
{ | ||
_version = version; | ||
_algorithmName = algorithmName; | ||
_encryptionType = encryptionType; | ||
_comment = comment; | ||
_publicKey = publicKey; | ||
_argon2Type = argon2Type; | ||
_argon2Salt = argon2Salt; | ||
_argon2Iterations = argon2Iterations; | ||
_argon2Memory = argon2Memory; | ||
_argon2Parallelism = argon2Parallelism; | ||
_data = data; | ||
_mac = mac; | ||
_passPhrase = passPhrase; | ||
} | ||
|
||
/// <summary> | ||
/// Parses an PuTTY PPK key file. | ||
/// <see href="https://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixC.html"/>. | ||
/// </summary> | ||
public Key Parse() | ||
{ | ||
byte[] privateKey; | ||
HMAC hmac; | ||
switch (_encryptionType) | ||
{ | ||
case "aes256-cbc": | ||
if (string.IsNullOrEmpty(_passPhrase)) | ||
{ | ||
throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty."); | ||
} | ||
|
||
byte[] cipherKey; | ||
byte[] cipherIV; | ||
switch (_version) | ||
{ | ||
case "3": | ||
ThrowHelper.ThrowIfNullOrEmpty(_argon2Type); | ||
ThrowHelper.ThrowIfNullOrEmpty(_argon2Iterations); | ||
ThrowHelper.ThrowIfNullOrEmpty(_argon2Memory); | ||
ThrowHelper.ThrowIfNullOrEmpty(_argon2Parallelism); | ||
ThrowHelper.ThrowIfNullOrEmpty(_argon2Salt); | ||
|
||
var keyData = Argon2( | ||
_argon2Type, | ||
Convert.ToInt32(_argon2Iterations), | ||
Convert.ToInt32(_argon2Memory), | ||
Convert.ToInt32(_argon2Parallelism), | ||
#if NET | ||
Convert.FromHexString(_argon2Salt), | ||
#else | ||
Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_argon2Salt), | ||
#endif | ||
_passPhrase); | ||
|
||
cipherKey = keyData.Take(32); | ||
cipherIV = keyData.Take(32, 16); | ||
|
||
var macKey = keyData.Take(48, 32); | ||
hmac = new HMACSHA256(macKey); | ||
|
||
break; | ||
case "2": | ||
keyData = V2KDF(_passPhrase); | ||
|
||
cipherKey = keyData.Take(32); | ||
cipherIV = new byte[16]; | ||
|
||
macKey = CryptoAbstraction.HashSHA1(Encoding.UTF8.GetBytes("putty-private-key-file-mac-key" + _passPhrase)).Take(20); | ||
hmac = new HMACSHA1(macKey); | ||
|
||
break; | ||
default: | ||
throw new SshException("PuTTY key file version " + _version + " is not supported"); | ||
} | ||
|
||
using (var cipher = new AesCipher(cipherKey, cipherIV, AesCipherMode.CBC, pkcs7Padding: false)) | ||
{ | ||
privateKey = cipher.Decrypt(_data); | ||
} | ||
|
||
break; | ||
case "none": | ||
switch (_version) | ||
{ | ||
case "3": | ||
hmac = new HMACSHA256(Array.Empty<byte>()); | ||
break; | ||
case "2": | ||
var macKey = CryptoAbstraction.HashSHA1(Encoding.UTF8.GetBytes("putty-private-key-file-mac-key")); | ||
hmac = new HMACSHA1(macKey); | ||
break; | ||
default: | ||
throw new SshException("PuTTY key file version " + _version + " is not supported"); | ||
} | ||
|
||
privateKey = _data; | ||
break; | ||
default: | ||
throw new SshException("Encryption " + _encryptionType + " is not supported for PuTTY key file"); | ||
} | ||
|
||
byte[] macData; | ||
using (var macStream = new SshDataStream(256)) | ||
{ | ||
macStream.Write(_algorithmName, Encoding.UTF8); | ||
macStream.Write(_encryptionType, Encoding.UTF8); | ||
macStream.Write(_comment, Encoding.UTF8); | ||
macStream.WriteBinary(_publicKey); | ||
macStream.WriteBinary(privateKey); | ||
macData = macStream.ToArray(); | ||
} | ||
|
||
byte[] macValue; | ||
using (hmac) | ||
{ | ||
macValue = hmac.ComputeHash(macData); | ||
} | ||
#if NET | ||
var reference = Convert.FromHexString(_mac); | ||
#else | ||
var reference = Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_mac); | ||
#endif | ||
if (!macValue.SequenceEqual(reference)) | ||
{ | ||
throw new SshException("MAC verification failed for PuTTY key file"); | ||
} | ||
|
||
var publicKeyReader = new SshDataReader(_publicKey); | ||
var keyType = publicKeyReader.ReadString(Encoding.UTF8); | ||
Debug.Assert(keyType == _algorithmName, $"{nameof(keyType)} is not the same as {nameof(_algorithmName)}"); | ||
|
||
var privateKeyReader = new SshDataReader(privateKey); | ||
|
||
Key parsedKey; | ||
|
||
switch (keyType) | ||
{ | ||
case "ssh-ed25519": | ||
parsedKey = new ED25519Key(privateKeyReader.ReadBignum2()); | ||
break; | ||
case "ecdsa-sha2-nistp256": | ||
case "ecdsa-sha2-nistp384": | ||
case "ecdsa-sha2-nistp521": | ||
var curve = publicKeyReader.ReadString(Encoding.ASCII); | ||
var pub = publicKeyReader.ReadBignum2(); | ||
var prv = privateKeyReader.ReadBignum2(); | ||
parsedKey = new EcdsaKey(curve, pub, prv); | ||
break; | ||
case "ssh-dss": | ||
var p = publicKeyReader.ReadBignum(); | ||
var q = publicKeyReader.ReadBignum(); | ||
var g = publicKeyReader.ReadBignum(); | ||
var y = publicKeyReader.ReadBignum(); | ||
var x = privateKeyReader.ReadBignum(); | ||
parsedKey = new DsaKey(p, q, g, y, x); | ||
break; | ||
case "ssh-rsa": | ||
var exponent = publicKeyReader.ReadBignum(); // e | ||
var modulus = publicKeyReader.ReadBignum(); // n | ||
var d = privateKeyReader.ReadBignum(); // d | ||
p = privateKeyReader.ReadBignum(); // p | ||
q = privateKeyReader.ReadBignum(); // q | ||
var inverseQ = privateKeyReader.ReadBignum(); // iqmp | ||
parsedKey = new RsaKey(modulus, exponent, d, p, q, inverseQ); | ||
break; | ||
default: | ||
throw new SshException("Key type " + keyType + " is not supported for PuTTY key file"); | ||
} | ||
|
||
parsedKey.Comment = _comment; | ||
return parsedKey; | ||
} | ||
|
||
private static byte[] Argon2(string type, int iterations, int memory, int parallelism, byte[] salt, string passPhrase) | ||
{ | ||
int param; | ||
switch (type) | ||
{ | ||
case "Argon2i": | ||
param = Argon2Parameters.Argon2i; | ||
break; | ||
case "Argon2d": | ||
param = Argon2Parameters.Argon2d; | ||
break; | ||
case "Argon2id": | ||
param = Argon2Parameters.Argon2id; | ||
break; | ||
default: | ||
throw new SshException("KDF " + type + " is not supported for PuTTY key file"); | ||
} | ||
|
||
var a2p = new Argon2Parameters.Builder(param) | ||
.WithVersion(Argon2Parameters.Version13) | ||
.WithIterations(iterations) | ||
.WithMemoryAsKB(memory) | ||
.WithParallelism(parallelism) | ||
.WithSalt(salt).Build(); | ||
|
||
var generator = new Argon2BytesGenerator(); | ||
|
||
generator.Init(a2p); | ||
|
||
var output = new byte[80]; | ||
var bytes = generator.GenerateBytes(passPhrase.ToCharArray(), output); | ||
|
||
if (bytes != output.Length) | ||
{ | ||
throw new SshException("Failed to generate key via Argon2"); | ||
} | ||
|
||
return output; | ||
} | ||
|
||
private static byte[] V2KDF(string passPhrase) | ||
{ | ||
var cipherKey = new List<byte>(); | ||
|
||
var passPhraseBytes = Encoding.UTF8.GetBytes(passPhrase); | ||
for (var sequenceNumber = 0; sequenceNumber < 2; sequenceNumber++) | ||
{ | ||
using (var sha1 = SHA1.Create()) | ||
{ | ||
var sequence = new byte[] { 0, 0, 0, (byte)sequenceNumber }; | ||
_ = sha1.TransformBlock(sequence, 0, 4, outputBuffer: null, 0); | ||
_ = sha1.TransformFinalBlock(passPhraseBytes, 0, passPhraseBytes.Length); | ||
Debug.Assert(sha1.Hash != null, "Hash is null"); | ||
cipherKey.AddRange(sha1.Hash); | ||
} | ||
} | ||
|
||
return cipherKey.ToArray(); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.