Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PrivateKeyCredential: support loading key from char array. #224

Merged
merged 2 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,20 @@ abstract class Credential
{ }
class PrivateKeyCredential : Credential
{
PrivateKeyCredential(string path, string? password = null);
PrivateKeyCredential(string path, Func<string?> passwordPrompt);
PrivateKeyCredential(string path, string? password = null, string? identifier ??= path);
PrivateKeyCredential(string path, Func<string?> passwordPrompt, string? identifier ??= path);

PrivateKeyCredential(char[] rawKey, string? password = null, string identifier = "[raw key]");
PrivateKeyCredential(char[] rawKey, Func<string?> passwordPrompt, string identifier = "[raw key]");

// Enable using private keys from other sources.
protected PrivateKeyCredential(Func<CancellationToken, ValueTask<Key>> loadKey, string identifier);
protected struct Key
{
Key(RSA rsa);
Key(ECDsa ecdsa);
Key(ReadOnlyMemory<char> rawKey, Func<string?>? passwordPrompt = null);
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these APIs were added in #223, but I forgot to update the README in that PR.

}
class PasswordCredential : Credential
{
Expand Down
47 changes: 41 additions & 6 deletions src/Tmds.Ssh/PrivateKeyCredential.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,55 @@ public PrivateKeyCredential(string path, Func<string?> passwordPrompt, string? i
this(LoadKeyFromFile(path ?? throw new ArgumentNullException(nameof(path)), passwordPrompt), identifier ?? path)
{ }

public PrivateKeyCredential(char[] rawKey, string? password = null, string identifier = "[raw key]") :
this(rawKey, () => password, identifier)
{ }

public PrivateKeyCredential(char[] rawKey, Func<string?> passwordPrompt, string identifier = "[raw key]") :
this(LoadRawKey(ValidateRawKeyArgument(rawKey), passwordPrompt), identifier)
{ }

// Allows the user to implement derived classes that represent a private key.
protected PrivateKeyCredential(Func<CancellationToken, ValueTask<Key>> loadKey, string identifier)
{
ArgumentNullException.ThrowIfNull(identifier);
ArgumentNullException.ThrowIfNull(loadKey);

LoadKey = loadKey;
Identifier = identifier;
}

private static char[] ValidateRawKeyArgument(char[] rawKey)
{
ArgumentNullException.ThrowIfNull(rawKey);
return rawKey;
}

private static Func<CancellationToken, ValueTask<Key>> LoadRawKey(char[] rawKey, Func<string?>? passwordPrompt)
=> (CancellationToken cancellationToken) =>
{
Key key = new Key(rawKey.AsMemory(), passwordPrompt);

return ValueTask.FromResult(key);
};

private static Func<CancellationToken, ValueTask<Key>> LoadKeyFromFile(string path, Func<string?> passwordPrompt)
=> (CancellationToken cancellationToken) =>
{
if (PrivateKeyParser.TryParsePrivateKeyFile(path, passwordPrompt, out PrivateKey? privateKey, out Exception? error))
string rawKey;
try
{
return ValueTask.FromResult(new Key(privateKey));
// We read the file so we get UnauthorizedAccessException in case it is not accessible
rawKey = File.ReadAllText(path);
}

if (error is FileNotFoundException or DirectoryNotFoundException)
catch (Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException)
{
return ValueTask.FromResult(default(Key));
return ValueTask.FromResult(default(Key)); // not found.
}

throw error;
Key key = new Key(rawKey.AsMemory(), passwordPrompt);

return ValueTask.FromResult(key);
};

// This is a type we expose to our derive types to avoid having to expose PrivateKey and a bunch of other internals.
Expand All @@ -52,6 +80,13 @@ public Key(RSA rsa)
PrivateKey = new RsaPrivateKey(rsa);
}

public Key(ReadOnlyMemory<char> rawKey, Func<string?>? passwordPrompt = null)
{
passwordPrompt ??= delegate { return null; };

PrivateKey = PrivateKeyParser.ParsePrivateKey(rawKey, passwordPrompt);
}

public Key(ECDsa ecdsa)
{
ECParameters parameters = ecdsa.ExportParameters(includePrivateParameters: false);
Expand Down
94 changes: 28 additions & 66 deletions src/Tmds.Ssh/PrivateKeyParser.OpenSsh.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,10 @@ partial class PrivateKeyParser
/// Parses an OpenSSH PEM formatted key. This is a new key format used by
/// OpenSSH for private keys.
/// </summary>
internal static bool TryParseOpenSshKey(
internal static PrivateKey ParseOpenSshKey(
byte[] keyData,
Func<string?> passwordPrompt,
[NotNullWhen(true)] out PrivateKey? privateKey,
[NotNullWhen(false)] out Exception? error)
Func<string?> passwordPrompt)
{
privateKey = null;

// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
/*
byte[] AUTH_MAGIC
Expand All @@ -39,8 +35,7 @@ string publickeyN
ReadOnlySpan<byte> AUTH_MAGIC = "openssh-key-v1\0"u8;
if (!keyData.AsSpan().StartsWith(AUTH_MAGIC))
{
error = new FormatException($"Unknown OpenSSH key format.");
return false;
throw new FormatException($"Unknown OpenSSH key format.");
}
ReadOnlySequence<byte> ros = new ReadOnlySequence<byte>(keyData);
ros = ros.Slice(AUTH_MAGIC.Length);
Expand All @@ -51,8 +46,7 @@ string publickeyN
uint nrOfKeys = reader.ReadUInt32();
if (nrOfKeys != 1)
{
error = new FormatException($"The data contains multiple keys.");
return false; // Multiple keys are not supported.
throw new FormatException($"The data contains multiple keys.");
}
reader.SkipString(); // skip the public key
ReadOnlySequence<byte> privateKeyList;
Expand All @@ -65,15 +59,11 @@ string publickeyN
string? password = passwordPrompt();
if (password is null)
{
error = new FormatException("Key was encrypted but no password was provided.");
return false;
throw new FormatException("Key was encrypted but no password was provided.");
}

byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
if (!TryDecryptOpenSshPrivateKey(reader, cipherName, kdfName, kdfOptions, passwordBytes, out var decryptedKey, out error))
{
return false;
}
byte[] decryptedKey = DecryptOpenSshPrivateKey(reader, cipherName, kdfName, kdfOptions, passwordBytes);
privateKeyList = new ReadOnlySequence<byte>(decryptedKey);
}

Expand All @@ -98,45 +88,38 @@ byte padlen % 255
uint checkint2 = reader.ReadUInt32();
if (checkInt1 != checkint2)
{
error = new FormatException($"The checkints mismatch. The key is invalid or the password is wrong.");
return false;
throw new FormatException($"The checkints mismatch. The key is invalid or the password is wrong.");
}

Name keyType = reader.ReadName();
if (keyType == AlgorithmNames.SshRsa)
{
return TryParseOpenSshRsaKey(reader, out privateKey, out error);
return ParseOpenSshRsaKey(reader);
}
else if (keyType.ToString().StartsWith("ecdsa-sha2-"))
{
return TryParseOpenSshEcdsaKey(keyType, reader, out privateKey, out error);
return ParseOpenSshEcdsaKey(keyType, reader);
}
else if (keyType == AlgorithmNames.SshEd25519)
{
return TryParseOpenSshEd25519Key(reader, out privateKey, out error);
return ParseOpenSshEd25519Key(reader);
}
else
{
error = new NotSupportedException($"The key type is unsupported: '{keyType}'.");
return false;
throw new NotSupportedException($"The key type is unsupported: '{keyType}'.");
}
}

private static bool TryDecryptOpenSshPrivateKey(
private static byte[] DecryptOpenSshPrivateKey(
SequenceReader reader,
Name cipher,
Name kdf,
ReadOnlySequence<byte> kdfOptions,
ReadOnlySpan<byte> password,
[NotNullWhen(true)] out byte[]? privateKey,
[NotNullWhen(false)] out Exception? error)
ReadOnlySpan<byte> password)
{
privateKey = null;

if (kdf != AlgorithmNames.BCrypt)
{
error = new NotSupportedException($"Unsupported KDF: '{kdf}'.");
return false;
throw new NotSupportedException($"Unsupported KDF: '{kdf}'.");
}

/*
Expand All @@ -149,8 +132,7 @@ uint32 rounds

if (!OpenSshKeyCipher.TryGetCipher(cipher, out var keyCipher))
{
error = new NotSupportedException($"Unsupported Cipher: '{cipher}'.");
return false;
throw new NotSupportedException($"Unsupported Cipher: '{cipher}'.");
}

try
Expand All @@ -168,30 +150,24 @@ uint32 rounds
{
if (!reader.TryRead(keyCipher.TagLength, out tag))
{
error = new FormatException($"Failed to read {cipher} encryption tag for encrypted OpenSSH key.");
return false;
throw new FormatException($"Failed to read {cipher} encryption tag for encrypted OpenSSH key.");
}
}

privateKey = keyCipher.Decrypt(
return keyCipher.Decrypt(
derivedKey.AsSpan(0, keyCipher.KeyLength),
derivedKey.AsSpan(keyCipher.KeyLength, keyCipher.IVLength),
encryptedKey.IsSingleSegment ? encryptedKey.FirstSpan : encryptedKey.ToArray(),
tag.IsSingleSegment ? tag.FirstSpan : tag.ToArray());
error = null;
return true;
}
catch (Exception ex)
{
error = new FormatException($"Failed to decrypt OpenSSH key with cipher {cipher}.", ex);
return false;
throw new FormatException($"Failed to decrypt OpenSSH key with cipher {cipher}.", ex);
}
}

private static bool TryParseOpenSshRsaKey(SequenceReader reader, [NotNullWhen(true)] out PrivateKey? privateKey, [NotNullWhen(false)] out Exception? error)
private static PrivateKey ParseOpenSshRsaKey(SequenceReader reader)
{
privateKey = null;

byte[] modulus = reader.ReadMPIntAsByteArray(isUnsigned: true);
byte[] exponent = reader.ReadMPIntAsByteArray(isUnsigned: true);
BigInteger d = reader.ReadMPInt();
Expand All @@ -217,22 +193,17 @@ private static bool TryParseOpenSshRsaKey(SequenceReader reader, [NotNullWhen(tr
try
{
rsa.ImportParameters(parameters);
privateKey = new RsaPrivateKey(rsa);
error = null;
return true;
return new RsaPrivateKey(rsa);
}
catch (Exception ex)
{
error = new FormatException($"The data can not be parsed into an RSA key.", ex);
rsa.Dispose();
return false;
throw new FormatException($"The data can not be parsed into an RSA key.", ex);
}
}

private static bool TryParseOpenSshEcdsaKey(Name keyIdentifier, SequenceReader reader, [NotNullWhen(true)] out PrivateKey? privateKey, [NotNullWhen(false)] out Exception? error)
private static PrivateKey ParseOpenSshEcdsaKey(Name keyIdentifier, SequenceReader reader)
{
privateKey = null;

Name curveName = reader.ReadName();

HashAlgorithmName allowedHashAlgo;
Expand All @@ -254,8 +225,7 @@ private static bool TryParseOpenSshEcdsaKey(Name keyIdentifier, SequenceReader r
}
else
{
error = new NotSupportedException($"ECDSA curve '{curveName}' is unsupported.");
return false;
throw new NotSupportedException($"ECDSA curve '{curveName}' is unsupported.");
}

ECPoint q = reader.ReadStringAsECPoint();
Expand All @@ -272,22 +242,17 @@ private static bool TryParseOpenSshEcdsaKey(Name keyIdentifier, SequenceReader r
};

ecdsa.ImportParameters(parameters);
privateKey = new ECDsaPrivateKey(ecdsa, keyIdentifier, curveName, allowedHashAlgo);
error = null;
return true;
return new ECDsaPrivateKey(ecdsa, keyIdentifier, curveName, allowedHashAlgo);
}
catch (Exception ex)
{
error = new FormatException($"The data can not be parsed into an ECDSA key.", ex);
ecdsa.Dispose();
return false;
throw new FormatException($"The data can not be parsed into an ECDSA key.", ex);
}
}

private static bool TryParseOpenSshEd25519Key(SequenceReader reader, [NotNullWhen(true)] out PrivateKey? privateKey, [NotNullWhen(false)] out Exception? error)
private static PrivateKey ParseOpenSshEd25519Key(SequenceReader reader)
{
privateKey = null;

// https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent-14#section-3.2.3
/*
string ENC(A)
Expand All @@ -303,16 +268,13 @@ concatenation of the private key k and the public ENC(A) key. Why it is
ReadOnlySequence<byte> publicKey = reader.ReadStringAsBytes();
ReadOnlySequence<byte> keyData = reader.ReadStringAsBytes();

privateKey = new Ed25519PrivateKey(
return new Ed25519PrivateKey(
keyData.Slice(0, keyData.Length - publicKey.Length).ToArray(),
publicKey.ToArray());
error = null;
return true;
}
catch (Exception ex)
{
error = new FormatException($"The data can not be parsed into an ED25519 key.", ex);
return false;
throw new FormatException($"The data can not be parsed into an ED25519 key.", ex);
}
}
}
20 changes: 6 additions & 14 deletions src/Tmds.Ssh/PrivateKeyParser.RsaPkcs1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,30 @@ partial class PrivateKeyParser
/// Parses an RSA PKCS#1 PEM formatted key. This is a legacy format used
/// by older ssh-keygen and openssl versions for RSA based keys.
/// </summary>
internal static bool TryParseRsaPkcs1PemKey(
internal static PrivateKey ParseRsaPkcs1PemKey(
ReadOnlySpan<byte> keyData,
Dictionary<string, string> metadata,
[NotNullWhen(true)] out PrivateKey? privateKey,
[NotNullWhen(false)] out Exception? error)
Dictionary<string, string> metadata)
{
privateKey = null;
RSA? rsa = RSA.Create();
try
{
if (metadata.TryGetValue("DEK-Info", out var dekInfo))
{
error = new NotImplementedException($"PKCS#1 key decryption is not implemented.");
return false;
throw new NotImplementedException($"PKCS#1 key decryption is not implemented.");
}

rsa.ImportRSAPrivateKey(keyData, out int bytesRead);
if (bytesRead != keyData.Length)
{
rsa.Dispose();
error = new FormatException($"There is additional data after the RSA key.");
return false;
throw new FormatException($"There is additional data after the RSA key.");
}
privateKey = new RsaPrivateKey(rsa);
error = null;
return true;
return new RsaPrivateKey(rsa);
}
catch (Exception ex)
{
rsa?.Dispose();
error = new FormatException($"The data can not be parsed into an RSA key.", ex);
return false;
throw new FormatException($"The data can not be parsed into an RSA key.", ex);
}
}
}
Loading
Loading