Skip to content

Feature: Supports enumerating Ed25519 and P521 credentials within FIDO #186

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 31 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2b8bd7e
misc: map from coseCurve to keydefinition.cosedefinition.cosecurve (int)
DennisDyallo Jan 11, 2025
795da5a
misc: consolidating key definitions
DennisDyallo Jan 11, 2025
de90dde
misc
DennisDyallo Jan 11, 2025
df3b32c
feat: enhanced FIDO curve support
DennisDyallo Jan 11, 2025
a769e22
tests: reuse constant
DennisDyallo Jan 15, 2025
f42e76b
tests: rewrite tests for CoseEcPublicKey
DennisDyallo Jan 15, 2025
3614777
misc: work on Cose keys
DennisDyallo Jan 15, 2025
d8e8bfe
fix: set correct length on P521
DennisDyallo Jan 15, 2025
a12fa54
misc: adding reusable keydefinitions as static members
DennisDyallo Jan 27, 2025
37bdfe6
feat: added support for P521
DennisDyallo Jan 27, 2025
7c0a144
misc: removed need for property (KeyDefinitions)
DennisDyallo Jan 27, 2025
03e14cf
docs: fix reference
DennisDyallo Jan 27, 2025
35a20dc
tests: added dynamic tests of verify P384, P521
DennisDyallo Jan 27, 2025
86984a7
refactor: update KeyOids references to remove 'Oid' prefix for consis…
DennisDyallo Jan 28, 2025
9d943be
docs: add IANA references to COSE algorithm, elliptic curve, and key …
DennisDyallo Jan 28, 2025
78f19d3
refactor: remove unused default constructor from CoseEdDsaPublicKey a…
DennisDyallo Jan 28, 2025
c8f0a94
refactor: reorganize fields and improve setup message in IntegrationT…
DennisDyallo Jan 28, 2025
c536f54
refactor: change _allDefinitions to a mutable Dictionary
DennisDyallo Jan 28, 2025
85dde40
refactor: remove TODO comment regarding CoseEdDsaPublicKey support in…
DennisDyallo Jan 28, 2025
b0a8b13
refactor: move GetOidByAlgorithm method to a more appropriate locatio…
DennisDyallo Jan 28, 2025
85c5e34
refactor: rename test methods for clarity and consistency, and extrac…
DennisDyallo Jan 28, 2025
e2f7241
refactor: correct SHA algorithm mapping and rename test method for cl…
DennisDyallo Jan 28, 2025
1e5923a
tests: rename test methods for clarity and fixed tests
DennisDyallo Jan 28, 2025
3565df7
misc: throw NotSupportedException for unsupported algorithms in GetOi…
DennisDyallo Jan 28, 2025
9af41dd
refactor: rename GetByCoseCurveType method to GetByCoseCurve for cons…
DennisDyallo Jan 28, 2025
d8e52f7
docs: enhance XML documentation for key OIDs and COSE enumerations
DennisDyallo Jan 28, 2025
5217bef
misc: update algorithm entry in CoseEcPublicKey and clean up CoseEdDs…
DennisDyallo Jan 29, 2025
05d7dda
tests: add unit tests for CoseEdDsaPublicKey functionality and valida…
DennisDyallo Jan 29, 2025
af3c5ae
Update Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/EcdsaVer…
DennisDyallo Jan 29, 2025
a0b38be
Update Yubico.YubiKey/tests/unit/Yubico/YubiKey/Cryptography/EcdsaVer…
DennisDyallo Jan 29, 2025
9d37340
fix: update algorithm entry in CoseEcPublicKey to use dynamic value i…
DennisDyallo Feb 5, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ namespace Yubico.YubiKey.Cryptography
/// contains the necessary private key data.
/// It extends the base <see cref="ECKeyParameters"/> class with additional validation for private key components.
/// </remarks>

public class ECPrivateKeyParameters : ECKeyParameters
{
/// <summary>
Expand Down
177 changes: 80 additions & 97 deletions Yubico.YubiKey/src/Yubico/YubiKey/Cryptography/EcdsaVerify.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,6 @@ namespace Yubico.YubiKey.Cryptography
/// BER encoding.
/// </para>
/// <para>
/// This class can verify signatures for P-256 and P-384 only.
/// </para>
/// <para>
/// Each of the verify methods will return a boolean, indicating whether the
/// signature verifies or not. If a signature does not verify, that is not an
/// error. The methods will throw exceptions if they encounter bad data, such
Expand All @@ -103,15 +100,7 @@ namespace Yubico.YubiKey.Cryptography
/// </remarks>
public class EcdsaVerify : IDisposable
{
private const int P256EncodedPointLength = 65;
private const int P384EncodedPointLength = 97;
private const int MinEncodedPointLength = 65;
private const int P256KeySize = 256;
private const int P384KeySize = 384;
private const string OidP256 = "1.2.840.10045.3.1.7";
private const string OidP384 = "1.3.132.0.34";

private const byte EncodedPointTag = 4;
private const byte EncodedPointTag = 0x04;
private const int SequenceTag = 0x30;
private const int IntegerTag = 0x02;

Expand All @@ -121,7 +110,7 @@ public class EcdsaVerify : IDisposable
/// The object built that will perform the verification operation.
/// </summary>
/// <remarks>
/// This must be P-256 or P-384, and contain valid coordinates.
/// This must be P-256, P-384 or P-521, and contain valid coordinates.
/// </remarks>
public ECDsa ECDsa { get; private set; }

Expand All @@ -136,9 +125,6 @@ private EcdsaVerify()
/// Create an instance of the <see cref="EcdsaVerify"/> class using the
/// ECDsa object that contains the public key.
/// </summary>
/// <remarks>
/// This supports only NIST P-256 and P-384 curves.
/// </remarks>
/// <param name="ecdsa">
/// The public key to use to verify. This constructor will copy a
/// reference to this object.
Expand All @@ -163,9 +149,6 @@ public EcdsaVerify(ECDsa ecdsa)
/// Create an instance of the <see cref="EcdsaVerify"/> class using the
/// PIV ECC public key.
/// </summary>
/// <remarks>
/// This supports only NIST P-256 and P-384 curves.
/// </remarks>
/// <param name="pivPublicKey">
/// The public key to use to verify.
/// </param>
Expand All @@ -183,7 +166,7 @@ public EcdsaVerify(PivPublicKey pivPublicKey)
}

var publicPointSpan = pivPublicKey is PivEccPublicKey eccKey
? eccKey.PublicPoint
? eccKey.PublicPoint
: ReadOnlySpan<byte>.Empty;

ECDsa = ConvertPublicKey(publicPointSpan.ToArray());
Expand All @@ -193,9 +176,6 @@ public EcdsaVerify(PivPublicKey pivPublicKey)
/// Create an instance of the <see cref="EcdsaVerify"/> class using the
/// COSE EC public key.
/// </summary>
/// <remarks>
/// This supports only NIST P-256 and P-384 curves.
/// </remarks>
/// <param name="coseKey">
/// The public key to use to verify.
/// </param>
Expand All @@ -212,13 +192,7 @@ public EcdsaVerify(CoseKey coseKey)
throw new ArgumentNullException(nameof(coseKey));
}

string oid = coseKey.Algorithm switch
{
CoseAlgorithmIdentifier.ES256 => OidP256,
CoseAlgorithmIdentifier.ECDHwHKDF256 => OidP256,
CoseAlgorithmIdentifier.ES384 => OidP384,
_ => "",
};
string oid = GetOidByAlgorithm(coseKey.Algorithm);

byte[] xCoordinate = Array.Empty<byte>();
byte[] yCoordinate = Array.Empty<byte>();
Expand All @@ -236,10 +210,9 @@ public EcdsaVerify(CoseKey coseKey)
/// encoded point.
/// </summary>
/// <remarks>
/// This supports only NIST P-256 and P-384 curves and only supports the
/// uncompressed encoded point: <c>04||x-coordinate||y-coordinate</c>
/// This supports the uncompressed encoded point: <c>04||x-coordinate||y-coordinate</c>
/// where both coordinates are the curve size (each coordinate is 32 bytes
/// for P-256 and 48 bytes for P-384), prepended with 00 bytes if
/// for P-256, 48 bytes for P-384 and 66 bytes for P-521), prepended with 00 bytes if
/// necessary.
/// </remarks>
/// <param name="encodedEccPoint">
Expand All @@ -260,9 +233,6 @@ public EcdsaVerify(ReadOnlyMemory<byte> encodedEccPoint)
/// Create an instance of the <see cref="EcdsaVerify"/> class using the
/// given certificate.
/// </summary>
/// <remarks>
/// This supports only NIST P-256 and P-384 curves.
/// </remarks>
/// <param name="certificate">
/// The certificate containing the public key to use to verify.
/// </param>
Expand All @@ -279,9 +249,8 @@ public EcdsaVerify(X509Certificate2 certificate)

/// <summary>
/// Verify the <c>signature</c> using the <c>dataToVerify</c>. This
/// method will digest the <c>dataToVerify</c> using SHA-256 if the
/// public key is P-256 and SHA-384 if the public key is P-384, and then
/// verify the signature using the digest.
/// method will digest the <c>dataToVerify</c> using SHA-256, SHA-384 or SHA-512,
/// depending on the public key's curve, and then verify the signature using the digest.
/// </summary>
/// <remarks>
/// If the signature is the standard BER encoding, then pass <c>true</c>
Expand All @@ -293,7 +262,7 @@ public EcdsaVerify(X509Certificate2 certificate)
/// </remarks>
/// <param name="dataToVerify">
/// The data data to verify. To verify an ECDSA signature, this method
/// will digest the data using SHA-256 or SHA-384, depending on the
/// will digest the data using SHA-256, SHA-384 or SHA-512, depending on the
/// public key's curve.
/// </param>
/// <param name="signature">
Expand All @@ -315,12 +284,14 @@ public bool VerifyData(
{
HashAlgorithm digester = ECDsa.KeySize switch
{
P256KeySize => CryptographyProviders.Sha256Creator(),
P384KeySize => CryptographyProviders.Sha384Creator(),
256 => CryptographyProviders.Sha256Creator(),
384 => CryptographyProviders.Sha384Creator(),
521 => CryptographyProviders.Sha512Creator(),
_ => throw new ArgumentException(ExceptionMessages.UnsupportedAlgorithm),
};

return VerifyDigestedData(digester.ComputeHash(dataToVerify), signature, isStandardSignature);
byte[] digestToVerify = digester.ComputeHash(dataToVerify);
return VerifyDigestedData(digestToVerify, signature, isStandardSignature);
}

/// <summary>
Expand All @@ -341,7 +312,7 @@ public bool VerifyData(
/// The signature to verify.
/// </param>
/// <param name="isStandardSignature">
/// <c>true</c> if the signature is formatted as the BER encoding
/// <c>true</c> if the signature is formatted as the BER encoding (DSASignatureFormat.Rfc3279DerSequence)
/// specified by most standards, or <c>false</c> if the signature is
/// formatted as the concatenation of <c>r</c> and <c>s</c>.
/// </param>
Expand All @@ -360,89 +331,89 @@ public bool VerifyDigestedData(

private static ECDsa ConvertPublicKey(ReadOnlyMemory<byte> encodedEccPoint)
{
string oid = "";
byte[] xCoordinate = Array.Empty<byte>();
byte[] yCoordinate = Array.Empty<byte>();

if (encodedEccPoint.Length >= MinEncodedPointLength && encodedEccPoint.Span[0] == EncodedPointTag)
int minEncodedPointLength = (KeyDefinitions.P256.LengthInBytes * 2) + 1; // This is the minimum length for an encoded point on P-256 (0x04 || x || y)
if (encodedEccPoint.Length < minEncodedPointLength || encodedEccPoint.Span[0] != EncodedPointTag)
{
int coordLength = (encodedEccPoint.Length - 1) / 2;
xCoordinate = encodedEccPoint.Slice(1, coordLength).ToArray();
yCoordinate = encodedEccPoint.Slice(1 + coordLength, coordLength).ToArray();

oid = encodedEccPoint.Length switch
{
P256EncodedPointLength => OidP256,
P384EncodedPointLength => OidP384,
_ => "",
};
throw new ArgumentException(ExceptionMessages.UnsupportedAlgorithm);
}

int coordLength = (encodedEccPoint.Length - 1) / 2;
byte[] xCoordinate = encodedEccPoint.Slice(1, coordLength).ToArray();
byte[] yCoordinate = encodedEccPoint.Slice(1 + coordLength, coordLength).ToArray();

string oid = GetOidByLength(encodedEccPoint.Length);
return ConvertPublicKey(oid, xCoordinate, yCoordinate);
}

private static ECDsa ConvertPublicKey(string oid, byte[] xCoordinate, byte[] yCoordinate)
private static string GetOidByLength(int encodedPointLength)
{
if (!string.IsNullOrEmpty(oid))
if (encodedPointLength == (KeyDefinitions.P256.LengthInBytes * 2) + 1)
{
var eccCurve = ECCurve.CreateFromValue(oid);
var eccParams = new ECParameters
{
Curve = eccCurve
};

eccParams.Q.X = xCoordinate;
eccParams.Q.Y = yCoordinate;

return CheckECDsa(ECDsa.Create(eccParams));
return KeyDefinitions.KeyOids.P256;
}
if (encodedPointLength == (KeyDefinitions.P384.LengthInBytes * 2) + 1)
{
return KeyDefinitions.KeyOids.P384;
}
if (encodedPointLength == (KeyDefinitions.P521.LengthInBytes * 2) + 1)
{
return KeyDefinitions.KeyOids.P521;
}

throw new ArgumentException(ExceptionMessages.UnsupportedAlgorithm);
}

private static ECDsa CheckECDsa(ECDsa toCheck)
private static ECDsa ConvertPublicKey(string oid, byte[] xCoordinate, byte[] yCoordinate)
{
var ecParameters = toCheck.ExportParameters(false);
if (string.IsNullOrEmpty(oid))
{
throw new ArgumentException(ExceptionMessages.UnsupportedAlgorithm);
}

int coordinateLength = ecParameters.Curve.Oid.Value switch
var eccCurve = ECCurve.CreateFromValue(oid);
var eccParams = new ECParameters
{
OidP256 => (P256EncodedPointLength - 1) / 2,
OidP384 => (P384EncodedPointLength - 1) / 2,
_ => -1,
Curve = eccCurve
};

if (ecParameters.Q.X.Length > 0 && ecParameters.Q.X.Length <= coordinateLength)
eccParams.Q.X = xCoordinate;
eccParams.Q.Y = yCoordinate;

var ecdsa = ECDsa.Create(eccParams);
return CheckECDsa(ecdsa);
}

private static ECDsa CheckECDsa(ECDsa toCheck)
{
var ecParameters = toCheck.ExportParameters(false);
var keyDefinition = KeyDefinitions.GetByOid(ecParameters.Curve.Oid.Value);
int coordinateLength = keyDefinition.LengthInBytes;

if (ecParameters.Q.X.Length == 0 ||
ecParameters.Q.X.Length > coordinateLength ||
ecParameters.Q.Y.Length == 0 ||
ecParameters.Q.Y.Length > coordinateLength)
{
if (ecParameters.Q.Y.Length > 0 && ecParameters.Q.Y.Length <= coordinateLength)
{
return toCheck;
}
throw new ArgumentException(ExceptionMessages.UnsupportedAlgorithm);
}

throw new ArgumentException(ExceptionMessages.UnsupportedAlgorithm);
return toCheck;
}


// Convert the signature from standard to the concatenation of r and s.
private static byte[] ConvertSignature(byte[] signature, int publicKeyBitSize)
{
int coordinateLength = publicKeyBitSize / 8;
byte[] convertedSignature = new byte[2 * coordinateLength];
var signatureMemory = new Memory<byte>(convertedSignature);
int bytesNeededForCurve = (publicKeyBitSize + 7) / 8; // Round up to nearest byte
Memory<byte> convertedSignatureBuffer = new byte[2 * bytesNeededForCurve];

var tlvReader = new TlvReader(signature);
if (tlvReader.TryReadNestedTlv(out tlvReader, SequenceTag))
if (!tlvReader.TryReadNestedTlv(out tlvReader, SequenceTag) ||
!TryCopyNextInteger(tlvReader, convertedSignatureBuffer, bytesNeededForCurve) ||
!TryCopyNextInteger(tlvReader, convertedSignatureBuffer[bytesNeededForCurve..], bytesNeededForCurve))
{
if (TryCopyNextInteger(tlvReader, signatureMemory, coordinateLength))
{
if (TryCopyNextInteger(tlvReader, signatureMemory[coordinateLength..], coordinateLength))
{
return convertedSignature;
}
}
throw new ArgumentException(ExceptionMessages.UnsupportedAlgorithm);
}

throw new ArgumentException(ExceptionMessages.UnsupportedAlgorithm);
return convertedSignatureBuffer.ToArray();
}

// Decode the next value in tlvReader, then copy the result into
Expand Down Expand Up @@ -483,6 +454,18 @@ private static bool TryCopyNextInteger(TlvReader tlvReader, Memory<byte> signatu
return false;
}

private static string GetOidByAlgorithm(CoseAlgorithmIdentifier algorithm)
{
return algorithm switch
{
CoseAlgorithmIdentifier.ES256 => KeyDefinitions.KeyOids.P256,
CoseAlgorithmIdentifier.ECDHwHKDF256 => KeyDefinitions.KeyOids.P256,
CoseAlgorithmIdentifier.ES384 => KeyDefinitions.KeyOids.P384,
CoseAlgorithmIdentifier.ES512 => KeyDefinitions.KeyOids.P521,
_ => throw new NotSupportedException(ExceptionMessages.UnsupportedAlgorithm)
};
}

/// <summary>
/// Clean up
/// </summary>
Expand Down
Loading
Loading