Skip to content

Commit

Permalink
Enable importing PEM-formatted keys into AsymmetricAlgorithm values
Browse files Browse the repository at this point in the history
  • Loading branch information
vcsjones committed Apr 9, 2020
1 parent 755b548 commit 29df078
Show file tree
Hide file tree
Showing 22 changed files with 2,491 additions and 3 deletions.
1 change: 1 addition & 0 deletions .config/CredScanSuppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/EC/ECKeyFileTests.cs",
"/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/EC/ECKeyFileTests.LimitedPrivate.cs",
"/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/RSAKeyFileTests.cs",
"/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/RSAKeyPemTests.cs",
"/src/libraries/System.Data.Common/tests/System/Data/Common/DbConnectionStringBuilderTest.cs",
"/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs",
"/src/libraries/System.DirectoryServices.AccountManagement/src/System/DirectoryServices/AccountManagement/constants.cs",
Expand Down
170 changes: 170 additions & 0 deletions src/libraries/Common/src/Internal/Cryptography/PemKeyImportHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable
using System;
using System.Diagnostics;
using System.Security.Cryptography;

namespace Internal.Cryptography
{
internal static class PemKeyImportHelpers
{
public delegate void ImportKeyAction(ReadOnlySpan<byte> source, out int bytesRead);
public delegate ImportKeyAction? FindImportActionFunc(ReadOnlySpan<char> label);
public delegate void ImportEncryptedKeyAction<TPass>(
ReadOnlySpan<TPass> password,
ReadOnlySpan<byte> source,
out int bytesRead);

public static void ImportEncryptedPem<TPass>(
ReadOnlySpan<char> input,
ReadOnlySpan<TPass> password,
ImportEncryptedKeyAction<TPass> importAction)
{
bool foundEncryptedPem = false;
PemFields foundFields = default;
ReadOnlySpan<char> foundSlice = default;

ReadOnlySpan<char> pem = input;
while (PemEncoding.TryFind(pem, out PemFields fields))
{
ReadOnlySpan<char> label = pem[fields.Label];

if (label.SequenceEqual(PemLabels.EncryptedPkcs8PrivateKey))
{
if (foundEncryptedPem)
{
throw new ArgumentException(SR.Argument_PemImport_AmbiguousPem, nameof(input));
}

foundEncryptedPem = true;
foundFields = fields;
foundSlice = pem;
}

Index offset = fields.Location.End;
pem = pem[offset..];
}

if (!foundEncryptedPem)
{
throw new ArgumentException(SR.Argument_PemImport_NoPemFound, nameof(input));
}

ReadOnlySpan<char> base64Contents = foundSlice[foundFields.Base64Data];
int base64size = foundFields.DecodedDataLength;
byte[] decodeBuffer = CryptoPool.Rent(base64size);
int bytesWritten = 0;

try
{
if (!Convert.TryFromBase64Chars(base64Contents, decodeBuffer, out bytesWritten))
{
// Couldn't decode base64. We shouldn't get here since the
// contents are pre-validated.
Debug.Fail("Base64 decoding failed on already validated contents.");
throw new ArgumentException();
}

Debug.Assert(bytesWritten == base64size);
Span<byte> decodedBase64 = decodeBuffer.AsSpan(0, bytesWritten);

// Don't need to check the bytesRead here. We're already operating
// on an input which is already a parsed subset of the input.
importAction(password, decodedBase64, out _);
}
finally
{
CryptoPool.Return(decodeBuffer, clearSize: bytesWritten);
}
}

public static void ImportPem(ReadOnlySpan<char> input, FindImportActionFunc callback)
{
ImportKeyAction? importAction = null;
PemFields foundFields = default;
ReadOnlySpan<char> foundSlice = default;
bool containsEncryptedPem = false;

ReadOnlySpan<char> pem = input;
while (PemEncoding.TryFind(pem, out PemFields fields))
{
ReadOnlySpan<char> label = pem[fields.Label];
ImportKeyAction? action = callback(label);

// Caller knows how to handle this PEM by label.
if (action != null)
{
// There was a previous PEM that could have been handled,
// which means this is ambiguous and contains multiple
// importable keys. Or, this contained an encrypted PEM.
// For purposes of encrypted PKCS8 with another actionable
// PEM, we will throw a duplicate exception.
if (importAction != null || containsEncryptedPem)
{
throw new ArgumentException(SR.Argument_PemImport_AmbiguousPem, nameof(input));
}

importAction = action;
foundFields = fields;
foundSlice = pem;
}
else if (label.SequenceEqual(PemLabels.EncryptedPkcs8PrivateKey))
{
if (importAction != null || containsEncryptedPem)
{
throw new ArgumentException(SR.Argument_PemImport_AmbiguousPem, nameof(input));
}

containsEncryptedPem = true;
}

Index offset = fields.Location.End;
pem = pem[offset..];
}

// The only PEM found that could potentially be used is encrypted PKCS8,
// but we won't try to import it with a null or blank password, so
// throw.
if (containsEncryptedPem)
{
throw new ArgumentException(SR.Argument_PemImport_EncryptedPem, nameof(input));
}

// We went through the PEM and found nothing that could be handled.
if (importAction is null)
{
throw new ArgumentException(SR.Argument_PemImport_NoPemFound, nameof(input));
}

ReadOnlySpan<char> base64Contents = foundSlice[foundFields.Base64Data];
int base64size = foundFields.DecodedDataLength;
byte[] decodeBuffer = CryptoPool.Rent(base64size);
int bytesWritten = 0;

try
{
if (!Convert.TryFromBase64Chars(base64Contents, decodeBuffer, out bytesWritten))
{
// Couldn't decode base64. We shouldn't get here since the
// contents are pre-validated.
Debug.Fail("Base64 decoding failed on already validated contents.");
throw new ArgumentException();
}

Debug.Assert(bytesWritten == base64size);
Span<byte> decodedBase64 = decodeBuffer.AsSpan(0, bytesWritten);

// Don't need to check the bytesRead here. We're already operating
// on an input which is already a parsed subset of the input.
importAction(decodedBase64, out _);
}
finally
{
CryptoPool.Return(decodeBuffer, clearSize: bytesWritten);
}
}
}
}
16 changes: 16 additions & 0 deletions src/libraries/Common/src/System/Security/Cryptography/PemLabels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace System.Security.Cryptography
{
internal static class PemLabels
{
internal const string Pkcs8PrivateKey = "PRIVATE KEY";
internal const string EncryptedPkcs8PrivateKey = "ENCRYPTED PRIVATE KEY";
internal const string SpkiPublicKey = "PUBLIC KEY";
internal const string RsaPublicKey = "RSA PUBLIC KEY";
internal const string RsaPrivateKey = "RSA PRIVATE KEY";
internal const string EcPrivateKey = "EC PRIVATE KEY";
}
}
Loading

0 comments on commit 29df078

Please sign in to comment.