Skip to content

Commit b169aa2

Browse files
authored
feat: Implement reading of VersionQualifier into YubikeyDeviceInfo (#240)
* feat: Implement reading of VersionQualifier into YubikeyDeviceInfo * chore: use long to capture uint32 value
2 parents 2e45407 + f15f0e4 commit b169aa2

File tree

6 files changed

+403
-28
lines changed

6 files changed

+403
-28
lines changed

Yubico.YubiKey/src/Yubico/YubiKey/FirmwareVersion.cs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public FirmwareVersion(byte major, byte minor = 0, byte patch = 0)
6161
Minor = minor;
6262
Patch = patch;
6363
}
64-
64+
6565
/// <summary>
6666
/// Parse a string of the form "major.minor.patch"
6767
/// </summary>
@@ -75,23 +75,44 @@ public static FirmwareVersion Parse(string versionString)
7575
{
7676
throw new ArgumentNullException(nameof(versionString));
7777
}
78-
78+
7979
string[] parts = versionString.Split('.');
8080
if (parts.Length != 3)
8181
{
8282
throw new ArgumentException("Must include major.minor.patch", nameof(versionString));
8383
}
84-
84+
8585
if (!byte.TryParse(parts[0], out byte major) ||
86-
!byte.TryParse(parts[1], out byte minor) ||
86+
!byte.TryParse(parts[1], out byte minor) ||
8787
!byte.TryParse(parts[2], out byte patch))
8888
{
8989
throw new ArgumentException("Major, minor and patch must be valid numbers", nameof(versionString));
9090
}
91-
91+
9292
return new FirmwareVersion(major, minor, patch);
9393
}
9494

95+
/// <summary>
96+
/// Creates a <see cref="FirmwareVersion"/> from a byte array.
97+
/// The byte array must contain exactly three bytes, representing the major, minor, and patch versions.
98+
/// </summary>
99+
/// <param name="bytes">A byte array containing the version information.</param>
100+
/// <returns>A <see cref="FirmwareVersion"/> instance.</returns>
101+
/// <exception cref="ArgumentException">Thrown if the byte array does not contain exactly three bytes.</exception>
102+
/// <remarks>
103+
/// The first byte represents the major version, the second byte represents the minor version,
104+
/// and the third byte represents the patch version.
105+
/// </remarks>
106+
public static FirmwareVersion FromBytes(ReadOnlySpan<byte> bytes)
107+
{
108+
if (bytes.Length != 3)
109+
{
110+
throw new ArgumentException("Invalid length of data");
111+
}
112+
113+
return new FirmwareVersion(bytes[0], bytes[1], bytes[2]);
114+
}
115+
95116
public static bool operator >(FirmwareVersion left, FirmwareVersion right)
96117
{
97118
// CA1065, these operators shouldn't throw exceptions.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright 2025 Yubico AB
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License").
4+
// You may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
17+
namespace Yubico.YubiKey;
18+
19+
/// <summary>
20+
/// Represents the type of version qualifier for a firmware version.
21+
/// The version qualifier type indicates whether the version is an Alpha, Beta, or Final release.
22+
/// </summary>
23+
internal enum VersionQualifierType : byte
24+
{
25+
Alpha = 0x00,
26+
Beta = 0x01,
27+
Final = 0x02
28+
}
29+
30+
/// <summary>
31+
/// Represents a version qualifier for a firmware version.
32+
/// A version qualifier typically includes the firmware version, a type (such as Alpha, Beta, or Final),
33+
/// and an iteration number.
34+
/// </summary>
35+
internal class VersionQualifier
36+
{
37+
/// <summary>
38+
/// Represents the firmware version associated with this qualifier.
39+
/// </summary>
40+
public FirmwareVersion FirmwareVersion { get; }
41+
/// <summary>
42+
/// Represents the type of version qualifier, such as Alpha, Beta, or Final.
43+
/// </summary>
44+
public VersionQualifierType Type { get; }
45+
46+
/// <summary>
47+
/// Represents the iteration number of the version qualifier.
48+
/// </summary>
49+
public long Iteration { get; }
50+
51+
/// <summary>
52+
/// Initializes a new instance of the <see cref="VersionQualifier"/> class.
53+
/// This constructor allows you to specify the firmware version, type, and iteration.
54+
/// The iteration must be a non-negative value and less than or equal to int.MaxValue.
55+
/// If the firmware version is null, an <see cref="ArgumentNullException"/> will be thrown.
56+
/// If the iteration is negative or greater than int.MaxValue, an <see cref="ArgumentOutOfRangeException"/> will be thrown.
57+
/// </summary>
58+
/// <param name="firmwareVersion">The firmware version associated with this qualifier.</param>
59+
/// <param name="type">The type of version qualifier (Alpha, Beta, Final).</param>
60+
/// <param name="iteration">The iteration number of the version qualifier, must be a non-negative value and less than or equal to int.MaxValue.</param>
61+
/// <exception cref="ArgumentOutOfRangeException"></exception>
62+
/// <exception cref="ArgumentNullException"></exception>
63+
public VersionQualifier(FirmwareVersion firmwareVersion, VersionQualifierType type, long iteration)
64+
{
65+
if (iteration < 0 || iteration > uint.MaxValue)
66+
{
67+
throw new ArgumentOutOfRangeException(nameof(iteration),
68+
$"Iteration must be between 0 and {uint.MaxValue}.");
69+
}
70+
71+
FirmwareVersion = firmwareVersion ?? throw new ArgumentNullException(nameof(firmwareVersion));
72+
Type = type;
73+
Iteration = iteration;
74+
}
75+
76+
/// <summary>
77+
/// Initializes a new instance of the <see cref="VersionQualifier"/> class with default values.
78+
/// The default firmware version is set to a new instance of <see cref="FirmwareVersion"/>,
79+
/// the type is set to <see cref="VersionQualifierType.Final"/>, and the iteration is set to 0.
80+
/// </summary>
81+
public VersionQualifier()
82+
{
83+
FirmwareVersion = new FirmwareVersion();
84+
Type = VersionQualifierType.Final;
85+
Iteration = 0;
86+
}
87+
88+
public override string ToString() => $"{FirmwareVersion}.{Type.ToString().ToLowerInvariant()}.{Iteration}";
89+
public override bool Equals(object obj) => obj is VersionQualifier other &&
90+
FirmwareVersion.Equals(other.FirmwareVersion) &&
91+
Type == other.Type &&
92+
Iteration == other.Iteration;
93+
public override int GetHashCode() => HashCode.Combine(FirmwareVersion, Type, Iteration);
94+
}

Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceInfo.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
using System.Diagnostics;
1919
using System.Diagnostics.CodeAnalysis;
2020
using System.Text;
21+
using Microsoft.Extensions.Logging;
22+
using Yubico.Core.Tlv;
2123

2224
namespace Yubico.YubiKey
2325
{
@@ -73,9 +75,16 @@ public class YubiKeyDeviceInfo : IYubiKeyDeviceInfo
7375
/// <inheritdoc />
7476
public FormFactor FormFactor { get; set; }
7577

78+
public string VersionName => VersionQualifier.Type == VersionQualifierType.Final
79+
? FirmwareVersion.ToString()
80+
: VersionQualifier.ToString();
81+
7682
/// <inheritdoc />
7783
public FirmwareVersion FirmwareVersion { get; set; }
7884

85+
/// <inheritdoc />
86+
internal VersionQualifier VersionQualifier { get; set; }
87+
7988
/// <inheritdoc />
8089
public TemplateStorageVersion? TemplateStorageVersion { get; set; }
8190

@@ -109,6 +118,7 @@ public class YubiKeyDeviceInfo : IYubiKeyDeviceInfo
109118
public YubiKeyDeviceInfo()
110119
{
111120
FirmwareVersion = new FirmwareVersion();
121+
VersionQualifier = new VersionQualifier(FirmwareVersion, VersionQualifierType.Final, 0);
112122
}
113123

114124
/// <summary>
@@ -191,6 +201,10 @@ internal static YubiKeyDeviceInfo CreateFromResponseData(Dictionary<int, ReadOnl
191201
};
192202

193203
break;
204+
205+
case YubikeyDeviceManagementTags.VersionQualifierTag:
206+
// This tag is handled later in the method.
207+
break;
194208
case YubikeyDeviceManagementTags.AutoEjectTimeoutTag:
195209
deviceInfo.AutoEjectTimeout = BinaryPrimitives.ReadUInt16BigEndian(value);
196210
break;
@@ -261,6 +275,55 @@ internal static YubiKeyDeviceInfo CreateFromResponseData(Dictionary<int, ReadOnl
261275

262276
deviceInfo.IsSkySeries |= skySeriesFlag;
263277

278+
if (!responseApduData.TryGetValue(YubikeyDeviceManagementTags.VersionQualifierTag, out var versionQualifierBytes))
279+
{
280+
deviceInfo.VersionQualifier = new VersionQualifier(deviceInfo.FirmwareVersion, VersionQualifierType.Final, 0);
281+
}
282+
else
283+
{
284+
if (versionQualifierBytes.Length != 0x0E)
285+
{
286+
throw new ArgumentException("Invalid data length.");
287+
}
288+
289+
const byte TAG_VERSION = 0x01;
290+
const byte TAG_TYPE = 0x02;
291+
const byte TAG_ITERATION = 0x03;
292+
293+
var data = TlvObjects.DecodeDictionary(versionQualifierBytes.Span);
294+
295+
if (!data.TryGetValue(TAG_VERSION, out var firmwareVersionBytes))
296+
{
297+
throw new ArgumentException("Missing TLV field: TAG_VERSION.");
298+
}
299+
if (!data.TryGetValue(TAG_TYPE, out var versionTypeBytes))
300+
{
301+
throw new ArgumentException("Missing TLV field: TAG_TYPE.");
302+
}
303+
if (!data.TryGetValue(TAG_ITERATION, out var iterationBytes))
304+
{
305+
throw new ArgumentException("Missing TLV field: TAG_ITERATION.");
306+
}
307+
308+
var qualifierVersion = FirmwareVersion.FromBytes(firmwareVersionBytes.Span);
309+
var versionType = (VersionQualifierType)versionTypeBytes.Span[0];
310+
long iteration = BinaryPrimitives.ReadUInt32BigEndian(iterationBytes.Span);
311+
312+
deviceInfo.VersionQualifier = new VersionQualifier(
313+
qualifierVersion,
314+
versionType,
315+
iteration);
316+
}
317+
318+
bool isFinalVersion = deviceInfo.VersionQualifier.Type == VersionQualifierType.Final;
319+
if (!isFinalVersion)
320+
{
321+
var Logger = Core.Logging.Log.GetLogger<YubiKeyDeviceInfo>();
322+
Logger.LogDebug("Overriding behavioral version with {FirmwareString}", deviceInfo.VersionQualifier.FirmwareVersion);
323+
}
324+
325+
var computedVersion = isFinalVersion ? deviceInfo.FirmwareVersion : deviceInfo.VersionQualifier.FirmwareVersion;
326+
deviceInfo.FirmwareVersion = computedVersion;
264327
return deviceInfo;
265328
}
266329

@@ -309,6 +372,10 @@ internal YubiKeyDeviceInfo Merge(YubiKeyDeviceInfo? second)
309372
? FirmwareVersion
310373
: second.FirmwareVersion,
311374

375+
VersionQualifier = VersionQualifier != new VersionQualifier()
376+
? VersionQualifier
377+
: second.VersionQualifier,
378+
312379
AutoEjectTimeout = DeviceFlags.HasFlag(DeviceFlags.TouchEject)
313380
? AutoEjectTimeout
314381
: second.DeviceFlags.HasFlag(DeviceFlags.TouchEject)

Yubico.YubiKey/src/Yubico/YubiKey/YubikeyDeviceManagementTags.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ internal static class YubikeyDeviceManagementTags
4242
internal const byte PinComplexityTag = 0x16;
4343
internal const byte NfcRestrictedTag = 0x17;
4444
internal const byte ResetBlockedTag = 0x18;
45+
internal const byte VersionQualifierTag = 0x19;
4546
internal const byte TemplateStorageVersionTag = 0x20; // FPS version tag
4647
internal const byte ImageProcessorVersionTag = 0x21; // STM version tag
4748
internal const byte TempTouchThresholdTag = 0x85;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright 2024 Yubico AB
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License").
4+
// You may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using Xunit;
16+
17+
namespace Yubico.YubiKey
18+
{
19+
public class VersionQualifierTests
20+
{
21+
[Fact]
22+
public void TestVersion()
23+
{
24+
var version = new FirmwareVersion(5, 7, 2);
25+
Assert.Equal(
26+
version, new VersionQualifier(version, VersionQualifierType.Alpha, 1).FirmwareVersion);
27+
}
28+
29+
[Fact]
30+
public void TestType()
31+
{
32+
Assert.Equal(
33+
VersionQualifierType.Alpha,
34+
new VersionQualifier(new FirmwareVersion(5, 7, 2), VersionQualifierType.Alpha, 1).Type);
35+
Assert.Equal(
36+
VersionQualifierType.Beta,
37+
new VersionQualifier(new FirmwareVersion(5, 7, 2), VersionQualifierType.Beta, 1).Type);
38+
Assert.Equal(
39+
VersionQualifierType.Final,
40+
new VersionQualifier(new FirmwareVersion(5, 7, 2), VersionQualifierType.Final, 1).Type);
41+
}
42+
43+
[Fact]
44+
public void TestIteration()
45+
{
46+
var version = new FirmwareVersion(5, 7, 2);
47+
var type = VersionQualifierType.Alpha;
48+
Assert.Equal(0, new VersionQualifier(version, type, 0).Iteration);
49+
Assert.Equal(128, new VersionQualifier(version, type, 128).Iteration);
50+
Assert.Equal(255, new VersionQualifier(version, type, 255).Iteration);
51+
}
52+
53+
[Fact]
54+
public void TestToString()
55+
{
56+
Assert.Equal(
57+
"5.7.2.alpha.0",
58+
new VersionQualifier(new FirmwareVersion(5, 7, 2), VersionQualifierType.Alpha, 0).ToString());
59+
Assert.Equal(
60+
"5.6.6.beta.16384",
61+
new VersionQualifier(new FirmwareVersion(5, 6, 6), VersionQualifierType.Beta, 16384).ToString());
62+
Assert.Equal(
63+
"3.4.0.final.2147483648",
64+
new VersionQualifier(new FirmwareVersion(3, 4, 0), VersionQualifierType.Final, 0x80000000).ToString());
65+
Assert.Equal(
66+
"3.4.0.final.2147483647",
67+
new VersionQualifier(new FirmwareVersion(3, 4, 0), VersionQualifierType.Final, 0x7fffffff).ToString());
68+
}
69+
70+
[Fact]
71+
public void TestEqualsAndHashCode()
72+
{
73+
var version1 = new FirmwareVersion(1, 0, 0);
74+
var version2 = new FirmwareVersion(1, 0, 0);
75+
var qualifier1 = new VersionQualifier(version1, VersionQualifierType.Alpha, 1);
76+
var qualifier2 = new VersionQualifier(version2, VersionQualifierType.Alpha, 1);
77+
var qualifier3 = new VersionQualifier(version1, VersionQualifierType.Beta, 2);
78+
79+
Assert.Equal(qualifier1, qualifier2);
80+
Assert.Equal(qualifier1.GetHashCode(), qualifier2.GetHashCode());
81+
Assert.NotEqual(qualifier1, qualifier3);
82+
}
83+
84+
[Fact]
85+
public void TestTypeFromValue()
86+
{
87+
Assert.Equal(VersionQualifierType.Alpha, (VersionQualifierType)0);
88+
Assert.Equal(VersionQualifierType.Beta, (VersionQualifierType)1);
89+
Assert.Equal(VersionQualifierType.Final, (VersionQualifierType)2);
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)