Skip to content

Commit 3465c0a

Browse files
committed
refactor: PivSession
- use AsnReader to validate x509 cert - use negating operator in boolean expression - removed parts about specific slots for attestation
1 parent 279fd05 commit 3465c0a

File tree

2 files changed

+71
-132
lines changed

2 files changed

+71
-132
lines changed

Yubico.YubiKey/src/Yubico/YubiKey/Piv/Commands/CreateAttestationStatementCommand.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public byte SlotNumber
7676
get => _slotNumber;
7777
set
7878
{
79-
if (PivSlot.IsValidSlotNumberForSigning(value) == false)
79+
if (!PivSlot.IsValidSlotNumberForSigning(value))
8080
{
8181
throw new ArgumentException(
8282
string.Format(
@@ -152,7 +152,7 @@ public CreateAttestationStatementCommand()
152152
/// </exception>
153153
public CommandApdu CreateCommandApdu()
154154
{
155-
if (PivSlot.IsValidSlotNumberForSigning(_slotNumber) == false)
155+
if (!PivSlot.IsValidSlotNumberForSigning(_slotNumber))
156156
{
157157
throw new InvalidOperationException(
158158
string.Format(
@@ -161,7 +161,7 @@ public CommandApdu CreateCommandApdu()
161161
_slotNumber));
162162
}
163163

164-
return new CommandApdu()
164+
return new CommandApdu
165165
{
166166
Ins = AttestInstruction,
167167
P1 = SlotNumber,

Yubico.YubiKey/src/Yubico/YubiKey/Piv/PivSession.Attestation.cs

Lines changed: 68 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
// limitations under the License.
1414

1515
using System;
16+
using System.Formats.Asn1;
1617
using System.Globalization;
18+
using System.Numerics;
1719
using System.Security;
1820
using System.Security.Cryptography.X509Certificates;
1921
using Yubico.Core.Tlv;
@@ -35,11 +37,6 @@ public sealed partial class PivSession : IDisposable
3537

3638
/// <summary>
3739
/// Create an attestation statement for the private key in the given slot.
38-
/// &gt; [!NOTE]
39-
/// &gt; In version 1.0.0 of the SDK, it was not possible to get an
40-
/// &gt; attestation statement for keys in slots 82 - 95 (retired key
41-
/// &gt; slots). However, beginning with SDK 1.0.1, it is possible to get
42-
/// &gt; attestation statements for keys in those slots.
4340
/// </summary>
4441
/// <remarks>
4542
/// See the User's Manual entry on
@@ -55,16 +52,7 @@ public sealed partial class PivSession : IDisposable
5552
/// </para>
5653
/// <para>
5754
/// It is possible to create attestation statements only for keys
58-
/// generated on a YubiKey, and only for keys in the following slots:
59-
/// <code>
60-
/// PivSlot.Authentication = 9A
61-
/// PivSlot.Signing = 9C
62-
/// PivSlot.KeyManagement = 9D
63-
/// PivSlot.CardAuthentication = 9E
64-
/// PivSlot.Retired1 = 82
65-
/// through
66-
/// PivSlot.Retired20 = 95
67-
/// </code>
55+
/// generated on a YubiKey.
6856
/// If the <c>slotNumber</c> argument is for any other slot, or if there
6957
/// is no key in the slot, or if the key in the slot was imported and not
7058
/// generated by the YubiKey, this method will throw an exception.
@@ -115,21 +103,22 @@ public sealed partial class PivSession : IDisposable
115103
/// </exception>
116104
public X509Certificate2 CreateAttestationStatement(byte slotNumber)
117105
{
118-
if (YubiKey.HasFeature(YubiKeyFeature.PivAttestation))
106+
if (!YubiKey.HasFeature(YubiKeyFeature.PivAttestation))
119107
{
120-
// This call will throw an exception if the slot number is incorrect.
121-
var command = new CreateAttestationStatementCommand(slotNumber);
122-
var response = Connection.SendCommand(command);
123-
124-
// This call will throw an exception if there was a problem with
125-
// attestation (imported, invalid cert, etc.).
126-
return response.GetData();
108+
throw new NotSupportedException(
109+
string.Format(
110+
CultureInfo.CurrentCulture,
111+
ExceptionMessages.NotSupportedByYubiKeyVersion));
127112
}
128113

129-
throw new NotSupportedException(
130-
string.Format(
131-
CultureInfo.CurrentCulture,
132-
ExceptionMessages.NotSupportedByYubiKeyVersion));
114+
// This call will throw an exception if the slot number is incorrect.
115+
var command = new CreateAttestationStatementCommand(slotNumber);
116+
var response = Connection.SendCommand(command);
117+
118+
// This call will throw an exception if there was a problem with
119+
// attestation (imported, invalid cert, etc.).
120+
return response.GetData();
121+
133122
}
134123

135124
/// <summary>
@@ -201,19 +190,20 @@ public X509Certificate2 GetAttestationCertificate()
201190
ExceptionMessages.NotSupportedByYubiKeyVersion));
202191
}
203192

204-
205193
[Obsolete("Usage of PivEccPublic/PivEccPrivateKey is deprecated. Use IPublicKey, IPrivateKey instead", false)]
206194
public void ReplaceAttestationKeyAndCertificate(PivPrivateKey privateKey, X509Certificate2 certificate)
207195
{
208-
byte[] certDer = CheckVersionKeyAndCertRequirements(privateKey.Algorithm.GetKeyType(), certificate);
196+
CheckVersionKeyAndCertRequirements(privateKey.Algorithm.GetKeyType(), certificate);
209197

198+
byte[] certDer = certificate.GetRawCertData();
210199
var tlvWriter = new TlvWriter();
211200
using (tlvWriter.WriteNestedTlv(0x53))
212201
{
213202
tlvWriter.WriteValue(0x70, certDer);
214203
tlvWriter.WriteByte(0x71, 0);
215204
tlvWriter.WriteValue(0xfe, null);
216205
}
206+
217207
byte[] encodedCert = tlvWriter.Encode();
218208

219209
ImportPrivateKey(PivSlot.Attestation, privateKey);
@@ -229,8 +219,8 @@ public void ReplaceAttestationKeyAndCertificate(PivPrivateKey privateKey, X509Ce
229219
response.StatusWord.ToString("X4", CultureInfo.InvariantCulture)));
230220
}
231221
}
232-
233-
/// <summary>
222+
223+
/// <summary>
234224
/// Replace the attestation key and certificate.
235225
/// </summary>
236226
/// <remarks>
@@ -363,21 +353,23 @@ public void ReplaceAttestationKeyAndCertificate(IPrivateKey privateKey, X509Cert
363353
{
364354
throw new ArgumentNullException(nameof(privateKey));
365355
}
366-
356+
367357
if (certificate is null)
368358
{
369359
throw new ArgumentNullException(nameof(certificate));
370360
}
371-
372-
byte[] certDer = CheckVersionKeyAndCertRequirements(privateKey.KeyType, certificate);
373361

362+
CheckVersionKeyAndCertRequirements(privateKey.KeyType, certificate);
363+
364+
byte[] certDer = certificate.GetRawCertData();
374365
var tlvWriter = new TlvWriter();
375366
using (tlvWriter.WriteNestedTlv(0x53))
376367
{
377368
tlvWriter.WriteValue(0x70, certDer);
378369
tlvWriter.WriteByte(0x71, 0);
379370
tlvWriter.WriteValue(0xfe, null);
380371
}
372+
381373
byte[] encodedCert = tlvWriter.Encode();
382374

383375
ImportPrivateKey(PivSlot.Attestation, privateKey);
@@ -402,7 +394,7 @@ public void ReplaceAttestationKeyAndCertificate(IPrivateKey privateKey, X509Cert
402394
// This will throw an exception if a check fails, or if one or both
403395
// arguments are null, or the algorithm is unsupported.
404396
// Return the DER encoding of the certificate.
405-
private byte[] CheckVersionKeyAndCertRequirements(KeyType keyType, X509Certificate2 certificate)
397+
private void CheckVersionKeyAndCertRequirements(KeyType keyType, X509Certificate2 certificate)
406398
{
407399
if (!YubiKey.HasFeature(YubiKeyFeature.PivAttestation))
408400
{
@@ -412,126 +404,73 @@ private byte[] CheckVersionKeyAndCertRequirements(KeyType keyType, X509Certifica
412404
ExceptionMessages.NotSupportedByYubiKeyVersion));
413405
}
414406

415-
var keyDefinition = KeyDefinitions.GetByKeyType(keyType);
416-
int keySize = keyDefinition.LengthInBytes;
417-
418-
bool isValidCert = IsCert(certificate, out byte[] certDer);
419-
isValidCert = IsCertSameAlgorithm(isValidCert, certificate, keySize, keyType);
420-
isValidCert = IsCertNameAndValidity(isValidCert, certDer);
421-
422-
if (isValidCert == false)
407+
bool isValidCert = IsSupportedCert(certificate, keyType);
408+
if (!isValidCert)
423409
{
424410
throw new ArgumentException(
425411
string.Format(
426412
CultureInfo.CurrentCulture,
427413
ExceptionMessages.UnsupportedAttestationCert));
428414
}
429-
430-
return certDer;
431415
}
432416

433417
// Is there really a cert in this variable? Is it > version 1?
434418
// If so, set certDer to the DER encoding of the cert.
435-
private static bool IsCert(X509Certificate2 certificate, out byte[] certDer)
419+
private static bool IsSupportedCert(X509Certificate2 certificate, KeyType keyType)
436420
{
437-
certDer = Array.Empty<byte>();
421+
byte[] certDer = certificate.GetRawCertData();
438422

439-
if (certificate is null)
440-
{
441-
throw new ArgumentNullException(nameof(certificate));
442-
}
423+
return certificate.Handle != IntPtr.Zero &&
424+
certificate.Version > 1 &&
425+
certDer.Length is > 0 and < MaximumCertDerLength &&
426+
certificate.PublicKey.Oid.Value == keyType.GetAlgorithmOid() &&
427+
IsValidCertNameAndValidity(certDer);
428+
}
443429

444-
if (certificate.Handle != IntPtr.Zero)
445-
{
446-
if (certificate.Version > 1)
447-
{
448-
certDer = certificate.GetRawCertData();
449-
}
450-
}
430+
private static bool IsValidCertNameAndValidity(byte[] certDer)
431+
{
432+
var reader = new AsnReader(certDer, AsnEncodingRules.BER);
433+
var seqCert = reader.ReadSequence();
434+
var seqTbsCert = seqCert.ReadSequence();
435+
_ = ReadVersion(seqTbsCert);
451436

452-
return certDer.Length > 0 && certDer.Length < MaximumCertDerLength;
437+
// Read serial number integer, signature and issuer sequence
438+
_ = seqTbsCert.ReadInteger();
439+
_ = seqTbsCert.ReadSequence();
440+
_ = seqTbsCert.ReadSequence();
441+
442+
var validity = seqTbsCert.ReadSequence();
443+
int validityValue = GetSequenceLength(validity);
444+
445+
var subject = seqTbsCert.ReadSequence();
446+
int subjectValue = GetSequenceLength(subject);
447+
448+
return validityValue < MaximumValidityValueLength && subjectValue < MaximumNameValueLength;
453449
}
454450

455-
// Does the cert in the object share the algorithm and key size?
456-
// If the input arg isValidCert is false, don't check, just return false;
457-
private static bool IsCertSameAlgorithm(bool isValidCert, X509Certificate2 certificate, int keySize, KeyType keyType)
451+
private static BigInteger? ReadVersion(AsnReader seqTbsCert)
458452
{
459-
bool returnValue = false;
460-
461-
if (isValidCert)
453+
if (!seqTbsCert.HasData ||
454+
seqTbsCert.PeekTag().TagClass != TagClass.ContextSpecific ||
455+
seqTbsCert.PeekTag().TagValue != 0)
462456
{
463-
// For ECC, the Certificate class's PublicKey property does not
464-
// have a Key. But the public key is in the EncodedKeyValue
465-
// property. An encoded ECC public key is a point:
466-
// 04 || x-coord || y-coord fixed length
467-
// The length of each coordinate is the key size. Because keySize
468-
// is given in bits, and the Length of the encoded key is given
469-
// in bytes, compare ( 2 * (keySize / 8)) + 1.
470-
// That's why the comparison is to (keySize / 4) + 1.
471-
if (certificate.PublicKey.Oid.Value == keyType.GetAlgorithmOid())
472-
{
473-
returnValue = keySize switch
474-
{
475-
256 => certificate.PublicKey.EncodedKeyValue.RawData.Length == (keySize / 4) + 1,
476-
384 => certificate.PublicKey.EncodedKeyValue.RawData.Length == (keySize / 4) + 1,
477-
2048 => certificate.PublicKey.Key.KeySize == keySize,
478-
_ => false,
479-
};
480-
}
457+
return null;
481458
}
482459

483-
return returnValue;
460+
var versionReader = seqTbsCert.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 0));
461+
return versionReader.ReadInteger();
484462
}
485463

486-
// Does the cert in the object have Validity and IssuerName encodings
487-
// that are not too long?
488-
// If the input arg isValidCert is false, don't check, just return false;
489-
private static bool IsCertNameAndValidity(bool isValidCert, byte[] certDer)
464+
private static int GetSequenceLength(AsnReader validity)
490465
{
491-
bool returnValue = false;
492-
493-
if (isValidCert)
466+
var clone = validity.Clone();
467+
int length = 0;
468+
while (clone.HasData)
494469
{
495-
// Get some of the elements of a cert. We just want to verify
496-
// their lengths.
497-
// The cert is the DER encoding of
498-
// SEQ {
499-
// SEQ {
500-
// [0] EXPLICIT INTEGER OPTIONAL,
501-
// INTEGER,
502-
// AlgId (a SEQ)
503-
// IssuerName (a SEQ)
504-
// Validity (a SEQ)
505-
// SubjectName (a SEQ)
506-
// We just want to know how long the SubjectName and Validity are,
507-
// so "decode" them as full elements (don't decode the contents
508-
// of the IssuerName and Validity SEQUENCEs).
509-
var reader = new TlvReader(certDer);
510-
if (reader.TryReadNestedTlv(out reader, 0x30))
511-
{
512-
if (reader.TryReadNestedTlv(out reader, 0x30))
513-
{
514-
byte[] tags = new byte[] { 0xA0, 0x02, 0x30, 0x30, 0x30, 0x30 };
515-
var value = new ReadOnlyMemory<byte>[tags.Length];
516-
int index = 0;
517-
for (; index < tags.Length; index++)
518-
{
519-
if (reader.TryReadValue(out value[index], tags[index]) == false)
520-
{
521-
break;
522-
}
523-
}
524-
525-
if (index >= tags.Length)
526-
{
527-
returnValue = value[4].Length < MaximumValidityValueLength &&
528-
value[5].Length < MaximumNameValueLength;
529-
}
530-
}
531-
}
470+
length += clone.ReadEncodedValue().Length;
532471
}
533472

534-
return returnValue;
473+
return length;
535474
}
536475
}
537476
}

0 commit comments

Comments
 (0)