Skip to content
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];
uint 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
97 changes: 97 additions & 0 deletions Yubico.YubiKey/tests/unit/Yubico/YubiKey/VersionQualifierTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// 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);
// Hash codes are not guaranteed to be different for non-equal objects,
// but for these specific inputs, they likely will be.
// If this assertion fails, it might not indicate a bug in GetHashCode itself,
// as long as the Equals contract is maintained.
// Assert.NotEqual(qualifier1.GetHashCode(), qualifier3.GetHashCode());
}

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