Skip to content

feat: Implement reading of VersionQualifier into YubikeyDeviceInfo #240

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 8 commits into from
Jun 10, 2025
Merged
31 changes: 26 additions & 5 deletions Yubico.YubiKey/src/Yubico/YubiKey/FirmwareVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public FirmwareVersion(byte major, byte minor = 0, byte patch = 0)
Minor = minor;
Patch = patch;
}

/// <summary>
/// Parse a string of the form "major.minor.patch"
/// </summary>
Expand All @@ -75,23 +75,44 @@ public static FirmwareVersion Parse(string versionString)
{
throw new ArgumentNullException(nameof(versionString));
}

string[] parts = versionString.Split('.');
if (parts.Length != 3)
{
throw new ArgumentException("Must include major.minor.patch", nameof(versionString));
}

if (!byte.TryParse(parts[0], out byte major) ||
!byte.TryParse(parts[1], out byte minor) ||
!byte.TryParse(parts[1], out byte minor) ||
!byte.TryParse(parts[2], out byte patch))
{
throw new ArgumentException("Major, minor and patch must be valid numbers", nameof(versionString));
}

return new FirmwareVersion(major, minor, patch);
}

/// <summary>
/// Creates a <see cref="FirmwareVersion"/> from a byte array.
/// The byte array must contain exactly three bytes, representing the major, minor, and patch versions.
/// </summary>
/// <param name="bytes">A byte array containing the version information.</param>
/// <returns>A <see cref="FirmwareVersion"/> instance.</returns>
/// <exception cref="ArgumentException">Thrown if the byte array does not contain exactly three bytes.</exception>
/// <remarks>
/// The first byte represents the major version, the second byte represents the minor version,
/// and the third byte represents the patch version.
/// </remarks>
public static FirmwareVersion FromBytes(ReadOnlySpan<byte> bytes)
{
if (bytes.Length != 3)
{
throw new ArgumentException("Invalid length of data");
}

return new FirmwareVersion(bytes[0], bytes[1], bytes[2]);
}

public static bool operator >(FirmwareVersion left, FirmwareVersion right)
{
// CA1065, these operators shouldn't throw exceptions.
Expand Down
94 changes: 94 additions & 0 deletions Yubico.YubiKey/src/Yubico/YubiKey/VersionQualifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2025 Yubico AB
//
// Licensed under the Apache License, Version 2.0 (the "License").
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;

namespace Yubico.YubiKey;

/// <summary>
/// Represents the type of version qualifier for a firmware version.
/// The version qualifier type indicates whether the version is an Alpha, Beta, or Final release.
/// </summary>
internal enum VersionQualifierType : byte
{
Alpha = 0x00,
Beta = 0x01,
Final = 0x02
}

/// <summary>
/// Represents a version qualifier for a firmware version.
/// A version qualifier typically includes the firmware version, a type (such as Alpha, Beta, or Final),
/// and an iteration number.
/// </summary>
internal class VersionQualifier
{
/// <summary>
/// Represents the firmware version associated with this qualifier.
/// </summary>
public FirmwareVersion FirmwareVersion { get; }
/// <summary>
/// Represents the type of version qualifier, such as Alpha, Beta, or Final.
/// </summary>
public VersionQualifierType Type { get; }

/// <summary>
/// Represents the iteration number of the version qualifier.
/// </summary>
public long Iteration { get; }

/// <summary>
/// Initializes a new instance of the <see cref="VersionQualifier"/> class.
/// This constructor allows you to specify the firmware version, type, and iteration.
/// The iteration must be a non-negative value and less than or equal to int.MaxValue.
/// If the firmware version is null, an <see cref="ArgumentNullException"/> will be thrown.
/// If the iteration is negative or greater than int.MaxValue, an <see cref="ArgumentOutOfRangeException"/> will be thrown.
/// </summary>
/// <param name="firmwareVersion">The firmware version associated with this qualifier.</param>
/// <param name="type">The type of version qualifier (Alpha, Beta, Final).</param>
/// <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>
/// <exception cref="ArgumentOutOfRangeException"></exception>
/// <exception cref="ArgumentNullException"></exception>
public VersionQualifier(FirmwareVersion firmwareVersion, VersionQualifierType type, long iteration)
{
if (iteration < 0 || iteration > uint.MaxValue)
{
throw new ArgumentOutOfRangeException(nameof(iteration),
$"Iteration must be between 0 and {uint.MaxValue}.");
}

FirmwareVersion = firmwareVersion ?? throw new ArgumentNullException(nameof(firmwareVersion));
Type = type;
Iteration = iteration;
}

/// <summary>
/// Initializes a new instance of the <see cref="VersionQualifier"/> class with default values.
/// The default firmware version is set to a new instance of <see cref="FirmwareVersion"/>,
/// the type is set to <see cref="VersionQualifierType.Final"/>, and the iteration is set to 0.
/// </summary>
public VersionQualifier()
{
FirmwareVersion = new FirmwareVersion();
Type = VersionQualifierType.Final;
Iteration = 0;
}

public override string ToString() => $"{FirmwareVersion}.{Type.ToString().ToLowerInvariant()}.{Iteration}";
public override bool Equals(object obj) => obj is VersionQualifier other &&
FirmwareVersion.Equals(other.FirmwareVersion) &&
Type == other.Type &&
Iteration == other.Iteration;
public override int GetHashCode() => HashCode.Combine(FirmwareVersion, Type, Iteration);
}
67 changes: 67 additions & 0 deletions Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Microsoft.Extensions.Logging;
using Yubico.Core.Tlv;

namespace Yubico.YubiKey
{
Expand Down Expand Up @@ -73,9 +75,16 @@ public class YubiKeyDeviceInfo : IYubiKeyDeviceInfo
/// <inheritdoc />
public FormFactor FormFactor { get; set; }

public string VersionName => VersionQualifier.Type == VersionQualifierType.Final
? FirmwareVersion.ToString()
: VersionQualifier.ToString();

/// <inheritdoc />
public FirmwareVersion FirmwareVersion { get; set; }

/// <inheritdoc />
internal VersionQualifier VersionQualifier { get; set; }

/// <inheritdoc />
public TemplateStorageVersion? TemplateStorageVersion { get; set; }

Expand Down Expand Up @@ -109,6 +118,7 @@ public class YubiKeyDeviceInfo : IYubiKeyDeviceInfo
public YubiKeyDeviceInfo()
{
FirmwareVersion = new FirmwareVersion();
VersionQualifier = new VersionQualifier(FirmwareVersion, VersionQualifierType.Final, 0);
}

/// <summary>
Expand Down Expand Up @@ -191,6 +201,10 @@ internal static YubiKeyDeviceInfo CreateFromResponseData(Dictionary<int, ReadOnl
};

break;

case YubikeyDeviceManagementTags.VersionQualifierTag:
// This tag is handled later in the method.
break;
case YubikeyDeviceManagementTags.AutoEjectTimeoutTag:
deviceInfo.AutoEjectTimeout = BinaryPrimitives.ReadUInt16BigEndian(value);
break;
Expand Down Expand Up @@ -261,6 +275,55 @@ internal static YubiKeyDeviceInfo CreateFromResponseData(Dictionary<int, ReadOnl

deviceInfo.IsSkySeries |= skySeriesFlag;

if (!responseApduData.TryGetValue(YubikeyDeviceManagementTags.VersionQualifierTag, out var versionQualifierBytes))
{
deviceInfo.VersionQualifier = new VersionQualifier(deviceInfo.FirmwareVersion, VersionQualifierType.Final, 0);
}
else
{
if (versionQualifierBytes.Length != 0x0E)
{
throw new ArgumentException("Invalid data length.");
}

const byte TAG_VERSION = 0x01;
const byte TAG_TYPE = 0x02;
const byte TAG_ITERATION = 0x03;

var data = TlvObjects.DecodeDictionary(versionQualifierBytes.Span);

if (!data.TryGetValue(TAG_VERSION, out var firmwareVersionBytes))
{
throw new ArgumentException("Missing TLV field: TAG_VERSION.");
}
if (!data.TryGetValue(TAG_TYPE, out var versionTypeBytes))
{
throw new ArgumentException("Missing TLV field: TAG_TYPE.");
}
if (!data.TryGetValue(TAG_ITERATION, out var iterationBytes))
{
throw new ArgumentException("Missing TLV field: TAG_ITERATION.");
}

var qualifierVersion = FirmwareVersion.FromBytes(firmwareVersionBytes.Span);
var versionType = (VersionQualifierType)versionTypeBytes.Span[0];
long iteration = BinaryPrimitives.ReadUInt32BigEndian(iterationBytes.Span);

deviceInfo.VersionQualifier = new VersionQualifier(
qualifierVersion,
versionType,
iteration);
}

bool isFinalVersion = deviceInfo.VersionQualifier.Type == VersionQualifierType.Final;
if (!isFinalVersion)
{
var Logger = Core.Logging.Log.GetLogger<YubiKeyDeviceInfo>();
Logger.LogDebug("Overriding behavioral version with {FirmwareString}", deviceInfo.VersionQualifier.FirmwareVersion);
}

var computedVersion = isFinalVersion ? deviceInfo.FirmwareVersion : deviceInfo.VersionQualifier.FirmwareVersion;
deviceInfo.FirmwareVersion = computedVersion;
return deviceInfo;
}

Expand Down Expand Up @@ -309,6 +372,10 @@ internal YubiKeyDeviceInfo Merge(YubiKeyDeviceInfo? second)
? FirmwareVersion
: second.FirmwareVersion,

VersionQualifier = VersionQualifier != new VersionQualifier()
? VersionQualifier
: second.VersionQualifier,

AutoEjectTimeout = DeviceFlags.HasFlag(DeviceFlags.TouchEject)
? AutoEjectTimeout
: second.DeviceFlags.HasFlag(DeviceFlags.TouchEject)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ internal static class YubikeyDeviceManagementTags
internal const byte PinComplexityTag = 0x16;
internal const byte NfcRestrictedTag = 0x17;
internal const byte ResetBlockedTag = 0x18;
internal const byte VersionQualifierTag = 0x19;
internal const byte TemplateStorageVersionTag = 0x20; // FPS version tag
internal const byte ImageProcessorVersionTag = 0x21; // STM version tag
internal const byte TempTouchThresholdTag = 0x85;
Expand Down
92 changes: 92 additions & 0 deletions Yubico.YubiKey/tests/unit/Yubico/YubiKey/VersionQualifierTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright 2024 Yubico AB
//
// Licensed under the Apache License, Version 2.0 (the "License").
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Xunit;

namespace Yubico.YubiKey
{
public class VersionQualifierTests
{
[Fact]
public void TestVersion()
{
var version = new FirmwareVersion(5, 7, 2);
Assert.Equal(
version, new VersionQualifier(version, VersionQualifierType.Alpha, 1).FirmwareVersion);
}

[Fact]
public void TestType()
{
Assert.Equal(
VersionQualifierType.Alpha,
new VersionQualifier(new FirmwareVersion(5, 7, 2), VersionQualifierType.Alpha, 1).Type);
Assert.Equal(
VersionQualifierType.Beta,
new VersionQualifier(new FirmwareVersion(5, 7, 2), VersionQualifierType.Beta, 1).Type);
Assert.Equal(
VersionQualifierType.Final,
new VersionQualifier(new FirmwareVersion(5, 7, 2), VersionQualifierType.Final, 1).Type);
}

[Fact]
public void TestIteration()
{
var version = new FirmwareVersion(5, 7, 2);
var type = VersionQualifierType.Alpha;
Assert.Equal(0, new VersionQualifier(version, type, 0).Iteration);
Assert.Equal(128, new VersionQualifier(version, type, 128).Iteration);
Assert.Equal(255, new VersionQualifier(version, type, 255).Iteration);
}

[Fact]
public void TestToString()
{
Assert.Equal(
"5.7.2.alpha.0",
new VersionQualifier(new FirmwareVersion(5, 7, 2), VersionQualifierType.Alpha, 0).ToString());
Assert.Equal(
"5.6.6.beta.16384",
new VersionQualifier(new FirmwareVersion(5, 6, 6), VersionQualifierType.Beta, 16384).ToString());
Assert.Equal(
"3.4.0.final.2147483648",
new VersionQualifier(new FirmwareVersion(3, 4, 0), VersionQualifierType.Final, 0x80000000).ToString());
Assert.Equal(
"3.4.0.final.2147483647",
new VersionQualifier(new FirmwareVersion(3, 4, 0), VersionQualifierType.Final, 0x7fffffff).ToString());
}

[Fact]
public void TestEqualsAndHashCode()
{
var version1 = new FirmwareVersion(1, 0, 0);
var version2 = new FirmwareVersion(1, 0, 0);
var qualifier1 = new VersionQualifier(version1, VersionQualifierType.Alpha, 1);
var qualifier2 = new VersionQualifier(version2, VersionQualifierType.Alpha, 1);
var qualifier3 = new VersionQualifier(version1, VersionQualifierType.Beta, 2);

Assert.Equal(qualifier1, qualifier2);
Assert.Equal(qualifier1.GetHashCode(), qualifier2.GetHashCode());
Assert.NotEqual(qualifier1, qualifier3);
}

[Fact]
public void TestTypeFromValue()
{
Assert.Equal(VersionQualifierType.Alpha, (VersionQualifierType)0);
Assert.Equal(VersionQualifierType.Beta, (VersionQualifierType)1);
Assert.Equal(VersionQualifierType.Final, (VersionQualifierType)2);
}
}
}
Loading
Loading