Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
31 changes: 31 additions & 0 deletions .github/workflows/build-nativeshims.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ jobs:
if %FAILED%==1 exit /b 1
echo All Windows builds verified: no VC++ Redistributable required
exit /b 0
- name: Verify export tables match canonical symbol list
shell: pwsh
run: |
# Set up VC++ environment so dumpbin is on PATH for arm64 inspection
& "${env:ProgramFiles}\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\Launch-VsDevShell.ps1" -Arch amd64
$script = "$PWD\Yubico.NativeShims\tests\check_exports.ps1"
$failed = $false
foreach ($arch in @('win-x64', 'win-x86', 'win-arm64')) {
Write-Host "=== Checking $arch\Yubico.NativeShims.dll ==="
& $script "$PWD\Yubico.NativeShims\$arch\Yubico.NativeShims.dll"
if ($LASTEXITCODE -ne 0) { $failed = $true }
}
if ($failed) { exit 1 }
Write-Host "All Windows export tables match canonical symbol list."
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: win-x64
Expand Down Expand Up @@ -253,6 +267,10 @@ jobs:
readelf -V *.so | grep GLIBC_2 | sort -u
echo "✅ Binary compatible with Debian 10 (glibc 2.28)"
'
- name: Verify export table matches canonical symbol list
working-directory: Yubico.NativeShims
run: |
bash tests/check_exports.sh linux-x64/libYubico.NativeShims.so
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: linux-x64
Expand Down Expand Up @@ -414,6 +432,11 @@ jobs:
readelf -V *.so | grep GLIBC_2 | sort -u
echo "✅ ARM64 binary compatible with Debian 10 (glibc 2.28)"
'
- name: Verify export table matches canonical symbol list
working-directory: Yubico.NativeShims
run: |
# nm reads ELF metadata regardless of target arch — works on x86_64 host inspecting aarch64 .so
bash tests/check_exports.sh linux-arm64/libYubico.NativeShims.so
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: linux-arm64
Expand All @@ -440,6 +463,14 @@ jobs:
else
sh ./build-macOS.sh
fi
- name: Verify export tables match canonical symbol list
working-directory: Yubico.NativeShims
run: |
set -e
for arch in osx-x64 osx-arm64; do
echo "=== Checking $arch/libYubico.NativeShims.dylib ==="
bash tests/check_exports.sh "$arch/libYubico.NativeShims.dylib"
done
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: osx-x64
Expand Down
2 changes: 1 addition & 1 deletion Yubico.Core/src/Yubico.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ limitations under the License. -->
</PackageReference>
<PackageReference Include="System.Memory" Version="4.6.3" />
<PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" />
<PackageReference Include="Yubico.NativeShims" Version="1.16.0" />
<PackageReference Include="Yubico.NativeShims" Version="1.16.1-prerelease.20260428.1" />
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm aware. Will be updated later

</ItemGroup>
Comment thread
DennisDyallo marked this conversation as resolved.

<ItemGroup Label="Expose internal test hooks to Unit Test projects">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,24 @@
// limitations under the License.

using System;
using System.Security.Cryptography;

namespace Yubico.YubiKey.Cryptography;
namespace Yubico.Core.Cryptography;

internal static class HkdfUtilities
public static class HkdfUtilities
{
private const int Sha256HashByteLength = 32; // SHA-256 hash length in bytes

/// <summary>
/// Derives a key using the HKDF (HMAC-based Key Derivation Function)
/// as specified in RFC 5869 using SHA-256.
/// </summary>
/// <remarks>
/// Uses BCL HMACSHA256 directly. The .ToArray() calls on Span inputs are
/// required by the BCL HMAC.Key setter and ComputeHash API — they only
/// accept byte[], not Span. The intermediate pseudo-random key (PRK) is
/// zeroed via CryptographicOperations.ZeroMemory after use.
/// </remarks>
/// <param name="inputKeyMaterial">The input key material (IKM).</param>
/// <param name="salt">Optional salt value. If not provided, a zero-length
/// salt will be used.</param>
Expand All @@ -47,30 +54,37 @@ public static Memory<byte> DeriveKey(
throw new ArgumentOutOfRangeException(nameof(length), "Length exceeds maximum output size.");
}

var pseudoRandomKey = HkdfExtract(inputKeyMaterial, salt);
return HkdfExpand(pseudoRandomKey, contextInfo, length);
byte[] pseudoRandomKey = HkdfExtract(inputKeyMaterial, salt);
try
{
return HkdfExpand(pseudoRandomKey, contextInfo, length);
}
finally
{
CryptographicOperations.ZeroMemory(pseudoRandomKey);
}
}

private static ReadOnlyMemory<byte> HkdfExtract(ReadOnlySpan<byte> inputKeyMaterial, ReadOnlySpan<byte> salt)
private static byte[] HkdfExtract(ReadOnlySpan<byte> inputKeyMaterial, ReadOnlySpan<byte> salt)
{
using var hmac = CryptographyProviders.HmacCreator("HMACSHA256");
hmac.Key = salt.IsEmpty ? new byte[Sha256HashByteLength] : salt.ToArray();
// BCL HMACSHA256 requires byte[] for key — .ToArray() is unavoidable here
byte[] saltBytes = salt.IsEmpty ? new byte[Sha256HashByteLength] : salt.ToArray();
using var hmac = new HMACSHA256(saltBytes);
return hmac.ComputeHash(inputKeyMaterial.ToArray());
}

private static Memory<byte> HkdfExpand(
ReadOnlyMemory<byte> pseudoRandomKey,
ReadOnlySpan<byte> pseudoRandomKey,
ReadOnlySpan<byte> contextInfo,
int length)
{
int numberOfBlocks = (length / Sha256HashByteLength) + (length % Sha256HashByteLength == 0 ? 0 : 1);
byte[] outputKeyMaterial = new byte[length];
Span<byte> previousBlock = Array.Empty<byte>();

using var hmac = CryptographyProviders.HmacCreator("HMACSHA256");
// BCL HMACSHA256 requires byte[] for key — .ToArray() is unavoidable here
using var hmac = new HMACSHA256(pseudoRandomKey.ToArray());

hmac.Key = pseudoRandomKey.ToArray();

for (byte index = 1; index <= numberOfBlocks; index++)
{
hmac.Initialize();
Expand All @@ -94,7 +108,7 @@ private static Memory<byte> HkdfExpand(
currentBlock
.AsSpan(0, bytesToCopy)
.CopyTo(outputKeyMaterial.AsSpan(blockOffset));

previousBlock = currentBlock;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,13 @@ public static int EcPointMul(
q,
m,
IntPtr.Zero);

// int EC_POINT_is_on_curve(const EC_GROUP* group, const EC_POINT* point, BN_CTX* ctx);
[DllImport(Libraries.NativeShims, EntryPoint = "Native_EC_POINT_is_on_curve", ExactSpelling = true, CharSet = CharSet.Ansi)]
[DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
private static extern int EcPointIsOnCurve(IntPtr group, IntPtr point, IntPtr ctx);

public static int EcPointIsOnCurve(SafeEcGroup group, SafeEcPoint point) =>
EcPointIsOnCurve(group.DangerousGetHandle(), point.DangerousGetHandle(), IntPtr.Zero);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
using System.Linq;
using System.Security.Cryptography;
using Xunit;
using Yubico.Core.Cryptography;

namespace Yubico.YubiKey.Cryptography;
namespace Yubico.Core.Cryptography;
public class HkdfUtilitiesTests
{
[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// 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.

// Purpose
// -------
// Direct P/Invoke functional tests for the OpenSSL BIGNUM marshaling layer
// exposed by Yubico.NativeShims (Native_BN_*). These wrappers move arbitrary-
// precision integers across the C#/C boundary; subtle bugs in length handling,
// padding, or leading-zero behavior surface as silent corruption in EC point
// coordinates and ARKG primitives that build on top.
//
// What this validates
// -------------------
// * bin -> BIGNUM -> bin round-trip preserves bytes for sizes 1, 16, 32, 256.
// * Native_BN_num_bytes returns the canonical length (leading zeros stripped,
// matching OpenSSL semantics).
// * Native_BN_bn2binpad left-pads to a fixed width without truncating.
// * Lifecycle: Native_BN_new, Native_BN_clear_free do not crash or leak under
// repeated allocate/free.
//
// References
// ----------
// * OpenSSL BN(3) man page — https://docs.openssl.org/master/man3/BN_new/
// (authoritative for BN_new, BN_bin2bn, BN_bn2bin, BN_bn2binpad,
// BN_num_bytes, BN_clear_free behavior).
// * No formal standards-track spec exists for the BIGNUM API; round-trip
// and boundary tests are self-consistent against the OpenSSL contract.

using System;
using System.Linq;
using Xunit;
using Yubico.PlatformInterop;

namespace Yubico.PlatformInterop.Cryptography
{
public class BigNumInteropTests
{
[Fact]
public void BnBinaryToBigNum_RoundTrip_SingleByte_ReturnsOriginal()
{
byte[] original = { 0x42 };

using SafeBigNum bn = NativeMethods.BnBinaryToBigNum(original);
byte[] buffer = new byte[1];
int written = NativeMethods.BnBigNumToBinary(bn, buffer);

Assert.Equal(1, written);
Assert.Equal(original, buffer);
}

[Fact]
public void BnBinaryToBigNum_RoundTrip_16Bytes_ReturnsOriginal()
{
byte[] original = Enumerable.Range(1, 16).Select(i => (byte)i).ToArray();

using SafeBigNum bn = NativeMethods.BnBinaryToBigNum(original);
byte[] buffer = new byte[16];
int written = NativeMethods.BnBigNumToBinary(bn, buffer);

Assert.Equal(16, written);
Assert.Equal(original, buffer);
}

[Fact]
public void BnBinaryToBigNum_RoundTrip_32Bytes_ReturnsOriginal()
{
byte[] original = Enumerable.Range(0, 32).Select(i => (byte)((i * 7) + 13)).ToArray();

using SafeBigNum bn = NativeMethods.BnBinaryToBigNum(original);
byte[] buffer = new byte[32];
int written = NativeMethods.BnBigNumToBinary(bn, buffer);

Assert.Equal(32, written);
Assert.Equal(original, buffer);
}

[Fact]
public void BnBinaryToBigNum_RoundTrip_256Bytes_ReturnsOriginal()
{
// Start with 0x01 to avoid leading-zero stripping
byte[] original = Enumerable.Range(1, 256).Select(i => (byte)i).ToArray();

using SafeBigNum bn = NativeMethods.BnBinaryToBigNum(original);
byte[] buffer = new byte[256];
int written = NativeMethods.BnBigNumToBinary(bn, buffer);

Assert.Equal(256, written);
Assert.Equal(original, buffer);
}

[Fact]
public void BnBinaryToBigNum_LeadingZero_StripsLeadingZeros()
{
// OpenSSL BIGNUMs strip leading zeros
byte[] original = { 0x00, 0x00, 0x01, 0x23, 0x45 };
byte[] expected = { 0x01, 0x23, 0x45 };

using SafeBigNum bn = NativeMethods.BnBinaryToBigNum(original);
byte[] buffer = new byte[5];
int written = NativeMethods.BnBigNumToBinary(bn, buffer);

Assert.Equal(3, written);
Assert.Equal(expected, buffer.Take(written).ToArray());
}

[Fact]
public void BnBinaryToBigNum_AllZeros_HandlesGracefully()
{
byte[] original = { 0x00, 0x00, 0x00 };

using SafeBigNum bn = NativeMethods.BnBinaryToBigNum(original);
byte[] buffer = new byte[3];
int written = NativeMethods.BnBigNumToBinary(bn, buffer);

// All zeros represents the number 0, which OpenSSL represents as zero bytes
Assert.Equal(0, written);
}

[Fact]
public void BnBigNumToBinaryWithPadding_PadsTo32Bytes()
{
byte[] original = { 0x12, 0x34 };

using SafeBigNum bn = NativeMethods.BnBinaryToBigNum(original);
byte[] buffer = new byte[32];
int written = NativeMethods.BnBigNumToBinaryWithPadding(bn, buffer);

Assert.Equal(32, written);
// Padding should be zero-bytes on the left (big-endian)
byte[] expected = new byte[32];
expected[30] = 0x12;
expected[31] = 0x34;
Assert.Equal(expected, buffer);
}

[Fact]
public void BnBigNumToBinaryWithPadding_PadsTo16Bytes()
{
byte[] original = { 0xAB };

using SafeBigNum bn = NativeMethods.BnBinaryToBigNum(original);
byte[] buffer = new byte[16];
int written = NativeMethods.BnBigNumToBinaryWithPadding(bn, buffer);

Assert.Equal(16, written);
byte[] expected = new byte[16];
expected[15] = 0xAB;
Assert.Equal(expected, buffer);
}

[Fact]
public void BnNew_CreatesValidHandle()
{
using SafeBigNum bn = NativeMethods.BnNew();

Assert.NotNull(bn);
Assert.False(bn.IsInvalid);
}

[Fact]
public void BnBinaryToBigNum_EmptyArray_HandlesGracefully()
{
byte[] original = Array.Empty<byte>();

using SafeBigNum bn = NativeMethods.BnBinaryToBigNum(original);
byte[] buffer = new byte[16];
int written = NativeMethods.BnBigNumToBinary(bn, buffer);

// Empty input = zero
Assert.Equal(0, written);
}
}
}
Loading
Loading