Skip to content

fix: Fixed bug importing PIV private key in legacy classes #231

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

Merged
merged 9 commits into from
May 26, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ private static byte[] EncodeECKey(
writer.PopSequence();

// PrivateKey as OCTET STRING
writer.WriteOctetString(ecPrivateKeyHandle.Data);
writer.WriteOctetString(ecPrivateKeyHandle.Data.Span);
writer.PopSequence();

return writer.Encode();
Expand Down Expand Up @@ -237,7 +237,7 @@ private static byte[] EncodeCurve25519Key(ReadOnlySpan<byte> privateKey, string
privateKeyWriter.WriteOctetString(privateKey);

using var privateKeyBytesHandle = new ZeroingMemoryHandle(privateKeyWriter.Encode());
writer.WriteOctetString(privateKeyBytesHandle.Data);
writer.WriteOctetString(privateKeyBytesHandle.Data.Span);

// End PrivateKeyInfo SEQUENCE
writer.PopSequence();
Expand Down
19 changes: 18 additions & 1 deletion Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/EcdsaVerify.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ public EcdsaVerify(ECDsa ecdsa)
/// <exception cref="ArgumentException">
/// The key is not for a supported algorithm or curve, or is malformed.
/// </exception>
[Obsolete("Usage of PivEccPublic/PivEccPrivateKey PivRsaPublic/PivRsaPrivateKey is deprecated. Use implementations of ECPublicKey, ECPrivateKey and RSAPublicKey, RSAPrivateKey instead", false)]
public EcdsaVerify(PivPublicKey pivPublicKey)
{
if (pivPublicKey is null)
Expand All @@ -171,6 +172,21 @@ public EcdsaVerify(PivPublicKey pivPublicKey)

ECDsa = ConvertPublicKey(publicPointSpan.ToArray());
}

public EcdsaVerify(ECPublicKey publicKey)
{
if (publicKey is null)
{
throw new ArgumentNullException(nameof(publicKey));
}

if (!publicKey.KeyType.IsECDsa())
{
throw new ArgumentException("Invalid key type", nameof(publicKey));
}

ECDsa = ConvertPublicKey(publicKey.PublicPoint);
}

/// <summary>
/// Create an instance of the <see cref="EcdsaVerify"/> class using the
Expand Down Expand Up @@ -331,7 +347,8 @@ public bool VerifyDigestedData(

private static ECDsa ConvertPublicKey(ReadOnlyMemory<byte> encodedEccPoint)
{
int minEncodedPointLength = (KeyDefinitions.P256.LengthInBytes * 2) + 1; // This is the minimum length for an encoded point on P-256 (0x04 || x || y)
// This is the minimum length for an encoded point on P-256 (0x04 || x || y)
int minEncodedPointLength = (KeyDefinitions.P256.LengthInBytes * 2) + 1;
if (encodedEccPoint.Length < minEncodedPointLength || encodedEccPoint.Span[0] != EncodedPointTag)
{
throw new ArgumentException(ExceptionMessages.UnsupportedAlgorithm);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,76 +13,34 @@
// limitations under the License.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography;

namespace Yubico.YubiKey.Cryptography;

#pragma warning disable CA1710
internal class ZeroingMemoryHandle : IDisposable, IReadOnlyCollection<byte>, IEnumerable<byte>
#pragma warning restore CA1710
internal class ZeroingMemoryHandle : IDisposable
{
private readonly byte[] _data;
private readonly Memory<byte> _data;
private bool _disposed;

public int Length => _disposed ? 0 : _data.Length;
public int Count => Length; // For IReadOnlyCollection

public byte this[int index] => _disposed
? throw new ObjectDisposedException(nameof(ZeroingMemoryHandle))
: _data[index];
public int Count => Length;

public ZeroingMemoryHandle(byte[] data)
public ZeroingMemoryHandle(Memory<byte> data)
{
_data = data ?? throw new ArgumentNullException(nameof(data));
_data = data;
}

public ReadOnlySpan<byte> AsSpan() => _disposed
? ReadOnlySpan<byte>.Empty
: _data.AsSpan();

public byte[] Data => _disposed
public Memory<byte> Data => _disposed
? throw new ObjectDisposedException(nameof(ZeroingMemoryHandle))
: _data;

public void CopyTo(byte[] destination, int destinationIndex = 0)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(ZeroingMemoryHandle));
}

Buffer.BlockCopy(_data, 0, destination, destinationIndex, _data.Length);
}

public ReadOnlySpan<byte> Slice(int start, int length) => _disposed
? ReadOnlySpan<byte>.Empty
: _data.AsSpan(start, length);

public IEnumerator<byte> GetEnumerator()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(ZeroingMemoryHandle));
}

for (int i = 0; i < _data.Length; i++)
{
yield return _data[i];
}
}

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();


public void Dispose()
{
if (_disposed)
{
return;
}

CryptographicOperations.ZeroMemory(_data);
CryptographicOperations.ZeroMemory(_data.Span);
_disposed = true;
GC.SuppressFinalize(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ namespace Yubico.YubiKey.Piv.Commands
/// PivPublicKey pubKey = generateKeyPairResponse.GetData();
/// </code>
/// </remarks>
#pragma warning disable CS0618 // Type or member is obsolete
public class GenerateKeyPairResponse : PivResponse, IYubiKeyResponseWithData<PivPublicKey>
#pragma warning restore CS0618 // Type or member is obsolete
{
Comment on lines +90 to 91
Copy link
Preview

Copilot AI May 26, 2025

Choose a reason for hiding this comment

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

The warning disable only wraps the class declaration, but the GetData() method still emits CS0618 warnings on the return type. Consider extending the pragma to cover GetData() or annotate it with [Obsolete] to fully suppress warnings.

Suggested change
#pragma warning restore CS0618 // Type or member is obsolete
{
{
#pragma warning disable CS0618 // Type or member is obsolete

Copilot uses AI. Check for mistakes.

private byte _slotNumber;
private PivAlgorithm _algorithm;
Expand Down Expand Up @@ -190,11 +192,14 @@ public GenerateKeyPairResponse(
/// <exception cref="InvalidOperationException">
/// Thrown when <see cref="YubiKeyResponse.Status"/> is not <see cref="ResponseStatus.Success"/>.
/// </exception>
#pragma warning disable CS0618 // Type or member is obsolete
public PivPublicKey GetData() =>
Status switch
{
ResponseStatus.Success => PivPublicKey.Create(ResponseApdu.Data, Algorithm),
_ => throw new InvalidOperationException(StatusMessage),
};
#pragma warning restore CS0618 // Type or member is obsolete

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ private ImportAsymmetricKeyCommand()
/// <exception cref="ArgumentException">
/// The <c>privateKey</c> argument does not contain a key.
/// </exception>
[Obsolete("Usage of PivEccPublic/PivEccPrivateKey is deprecated. Use IPublicKey, IPrivateKey instead", false)]
[Obsolete("Usage of PivEccPublic/PivEccPrivateKey, PivRsaPublic/PivRsaPrivateKey is deprecated. Use implementations of ECPublicKey, ECPrivateKey and RSAPublicKey, RSAPrivateKey instead", false)]

public ImportAsymmetricKeyCommand(
PivPrivateKey privateKey,
byte slotNumber,
Expand Down Expand Up @@ -274,7 +275,8 @@ public ImportAsymmetricKeyCommand(
/// <exception cref="ArgumentException">
/// The <c>privateKey</c> argument does not contain a key.
/// </exception>
[Obsolete("Usage of PivEccPublic/PivEccPrivateKey is deprecated. Use IPublicKey, IPrivateKey instead", false)]
[Obsolete("Usage of PivEccPublic/PivEccPrivateKey, PivRsaPublic/PivRsaPrivateKey is deprecated. Use implementations of ECPublicKey, ECPrivateKey and RSAPublicKey, RSAPrivateKey instead", false)]

public ImportAsymmetricKeyCommand(PivPrivateKey privateKey)
{
if (privateKey is null)
Expand Down
29 changes: 18 additions & 11 deletions Yubico.YubiKey/src/Yubico/YubiKey/Piv/Converters/KeyExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,31 +33,38 @@ public static class KeyExtensions
/// <param name="parameters">The public key parameters to encode.</param>
/// <returns>A BER encoded byte array containing the encoded public key.</returns>
/// <exception cref="ArgumentException">Thrown when the key type is not supported.</exception>
public static Memory<byte> EncodeAsPiv(this IPublicKey parameters)
{
return parameters switch
public static Memory<byte> EncodeAsPiv(this IPublicKey parameters) =>
parameters switch
{
ECPublicKey p => PivKeyEncoder.EncodeECPublicKey(p),
RSAPublicKey p => PivKeyEncoder.EncodeRSAPublicKey(p),
Curve25519PublicKey p => PivKeyEncoder.EncodeCurve25519PublicKey(p),
_ => throw new ArgumentException("The type conversion for the specified key type is not supported", nameof(parameters))

#pragma warning disable CS0618 // Type or member is obsolete
PivPublicKey p => p.PivEncodedPublicKey.ToArray(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the bugfix

#pragma warning restore CS0618 // Type or member is obsolete
_ => throw new ArgumentException(
"The type conversion for the specified key type is not supported", nameof(parameters))
};
}


/// <summary>
/// Encodes a private key into the PIV format.
/// </summary>
/// <param name="parameters">The private key parameters to encode.</param>
/// <returns>A BER encoded byte array containing the encoded private key.</returns>
/// <exception cref="ArgumentException">Thrown when the key type is not supported or when RSA key components have invalid lengths.</exception>
public static Memory<byte> EncodeAsPiv(this IPrivateKey parameters)
{
return parameters switch
/// <remarks>This method returns a newly allocated array containing sensitive information.</remarks>
public static Memory<byte> EncodeAsPiv(this IPrivateKey parameters) =>
parameters switch
{
ECPrivateKey p => PivKeyEncoder.EncodeECPrivateKey(p),
RSAPrivateKey p => PivKeyEncoder.EncodeRSAPrivateKey(p),
Curve25519PrivateKey p => PivKeyEncoder.EncodeCurve25519PrivateKey(p),
_ => throw new ArgumentException("The type conversion for the specified key type is not supported", nameof(parameters))

#pragma warning disable CS0618 // Type or member is obsolete
PivPrivateKey p => p.EncodedPrivateKey.ToArray(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the bugfix

#pragma warning restore CS0618 // Type or member is obsolete
_ => throw new ArgumentException(
"The type conversion for the specified key type is not supported", nameof(parameters))
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
namespace Yubico.YubiKey.Piv.Converters;

/// <summary>
/// This class converts from a Piv Encoded Key to either instances of the common IPublicKey and IPrivateKey
/// or concrete the concrete types that inherit these interfaces.
/// This class converts from IPublicKey, IPrivateKey and implementations of either to a PIV-encoded key.
/// </summary>
internal class PivKeyEncoder
internal static class PivKeyEncoder
{
[Obsolete("KeyExtensions instead", false)]
public static Memory<byte> EncodePublicKey(IPublicKey publicKey)
{
return publicKey switch
Expand All @@ -35,7 +35,7 @@ public static Memory<byte> EncodePublicKey(IPublicKey publicKey)
_ => throw new ArgumentException("Unsupported public key type."),
};
}

public static Memory<byte> EncodeRSAPublicKey(RSAPublicKey publicKey)
{
var rsaParameters = publicKey.Parameters;
Expand Down Expand Up @@ -70,17 +70,17 @@ public static Memory<byte> EncodeECPublicKey(ECPublicKey publicKey)

return tlvWriter.Encode();
}
public static Memory<byte> EncodePrivateKey(IPrivateKey publicKey)
{
return publicKey switch

[Obsolete("KeyExtensions instead", false)]
public static Memory<byte> EncodePrivateKey(IPrivateKey publicKey) =>
publicKey switch
{
Curve25519PrivateKey curve25519PrivateKey => EncodeCurve25519PrivateKey(curve25519PrivateKey),
ECPrivateKey ecPrivateKey => EncodeECPrivateKey(ecPrivateKey),
RSAPrivateKey rsaPrivateKey => EncodeRSAPrivateKey(rsaPrivateKey),

_ => throw new ArgumentException("Unsupported public key type."),
};
}

public static Memory<byte> EncodeECPrivateKey(ECPrivateKey privateKey)
{
Expand Down
56 changes: 37 additions & 19 deletions Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivEccPrivateKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
// limitations under the License.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using Yubico.Core.Tlv;
using Yubico.YubiKey.Cryptography;

namespace Yubico.YubiKey.Piv
{
Expand All @@ -27,11 +30,11 @@ namespace Yubico.YubiKey.Piv
/// of a point on the curve. So for ECC P-256, each coordinate is 32 bytes
/// (256 bits), so the private value will be 32 bytes.
/// </remarks>
[Obsolete(
"Usage of PivEccPublic/PivEccPrivateKey, PivRsaPublic/PivRsaPrivateKey is deprecated. Use implementations of ECPublicKey, ECPrivateKey and RSAPublicKey, RSAPrivateKey instead",
false)]
public sealed class PivEccPrivateKey : PivPrivateKey
{
private const int EccP256PrivateKeySize = 32;
private const int EccP384PrivateKeySize = 48;

private Memory<byte> _privateValue;

// <summary>
Expand Down Expand Up @@ -66,29 +69,19 @@ private PivEccPrivateKey()
/// <exception cref="ArgumentException">
/// The size of the private value is not supported by the YubiKey.
/// </exception>
[Obsolete("Usage of PivEccPublic/PivEccPrivateKey is deprecated. Use ECPublicKey, ECPrivateKey instead", false)]
public PivEccPrivateKey(
ReadOnlySpan<byte> privateValue,
ReadOnlySpan<byte> privateValue,
PivAlgorithm? algorithm = null)
{
if (algorithm.HasValue)
{
int expectedSize = algorithm.Value.GetPivKeyDefinition().KeyDefinition.LengthInBytes;
if(privateValue.Length != expectedSize)
{
throw new ArgumentException(
string.Format(
CultureInfo.CurrentCulture, ExceptionMessages.InvalidPrivateKeyData));
}
ValidateSize(privateValue, algorithm.Value);
Algorithm = algorithm.Value;
}
else
{
Algorithm = privateValue.Length switch
{
EccP256PrivateKeySize => PivAlgorithm.EccP256,
EccP384PrivateKeySize => PivAlgorithm.EccP384,
_ => throw new ArgumentException(string.Format(
CultureInfo.CurrentCulture, ExceptionMessages.InvalidPrivateKeyData))
};
Algorithm = GetBySize(privateValue);
}

int tag = Algorithm switch
Expand All @@ -97,13 +90,38 @@ public PivEccPrivateKey(
PivAlgorithm.EccX25519 => PivConstants.PrivateECX25519Tag,
_ => PivConstants.PrivateECDsaTag
};

var tlvWriter = new TlvWriter();
tlvWriter.WriteValue(tag, privateValue);
EncodedKey = tlvWriter.Encode();
_privateValue = new Memory<byte>(privateValue.ToArray());
}

private static void ValidateSize(ReadOnlySpan<byte> privateValue, PivAlgorithm algorithm)
{
int expectedSize = algorithm.GetPivKeyDefinition().KeyDefinition.LengthInBytes;
if (privateValue.Length != expectedSize)
{
throw new ArgumentException(
string.Format(
CultureInfo.CurrentCulture, ExceptionMessages.InvalidPrivateKeyData));
}
}

private static PivAlgorithm GetBySize(ReadOnlySpan<byte> privateValue)
{
int privateValueSize = privateValue.Length;
var allowed = new List<KeyDefinition> { KeyDefinitions.P256, KeyDefinitions.P384 };
if (allowed.SingleOrDefault(kd => kd.LengthInBytes == privateValueSize) is { } keyDefinition)
{
return keyDefinition.GetPivKeyDefinition().Algorithm;
}

throw new ArgumentException(
string.Format(
CultureInfo.CurrentCulture, ExceptionMessages.InvalidPrivateKeyData));
}

/// <summary>
/// Create a new instance of an ECC private key object based on the
/// encoding.
Expand Down
Loading
Loading