Skip to content

Commit 0e287a6

Browse files
authored
Merge pull request #398 from Yubico/fix/issue-395-digest-data-regression
fix(piv): Fix YubiKeySignatureGenerator.DigestData regression in Sample App
2 parents 72fe466 + fb4d208 commit 0e287a6

File tree

3 files changed

+257
-22
lines changed

3 files changed

+257
-22
lines changed

.devcontainer/devcontainer.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
{
22
"image": "mcr.microsoft.com/devcontainers/dotnet:9.0",
33

4+
"features": {
5+
"ghcr.io/devcontainers/features/dotnet:2": {
6+
"version": "none",
7+
"additionalVersions": "8.0,10.0"
8+
}
9+
},
10+
411
"customizations": {
512
"vscode": {
613
"extensions": [

Yubico.YubiKey/examples/PivSampleCode/CertificateOperations/YubiKeySignatureGenerator.cs

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -127,41 +127,41 @@ public override byte[] SignData(byte[] data, HashAlgorithmName hashAlgorithm)
127127
}
128128

129129
// Compute the message digest of the data using the given hashAlgorithm.
130-
private byte[] DigestData(byte[] data, HashAlgorithmName hashAlgorithm)
130+
// For RSA keys, returns the raw digest (PadRsa handles signature padding).
131+
// For ECC keys, pads the digest to key size with leading zeros if needed.
132+
public byte[] DigestData(byte[] data, HashAlgorithmName hashAlgorithm)
131133
{
132-
using HashAlgorithm digester = hashAlgorithm.Name switch
134+
byte[] digest = MessageDigestOperations.ComputeMessageDigest(data, hashAlgorithm);
135+
136+
// For RSA, return the raw digest - PadRsa handles the signature padding
137+
if (_algorithm.IsRSA())
133138
{
134-
"SHA1" => CryptographyProviders.Sha1Creator(),
135-
"SHA256" => CryptographyProviders.Sha256Creator(),
136-
"SHA384" => CryptographyProviders.Sha384Creator(),
137-
"SHA512" => CryptographyProviders.Sha512Creator(),
138-
_ => throw new ArgumentException(
139-
string.Format(
140-
CultureInfo.CurrentCulture,
141-
InvalidAlgorithmMessage)),
142-
};
139+
return digest;
140+
}
143141

144-
// If the algorithm is P-256, then make sure the digest is exactly 32
145-
// bytes. If it's P-384, the digest must be exactly 48 bytes.
146-
// We'll prepend 00 bytes if necessary.
147-
int bufferSize = _algorithm.GetKeySizeBytes();
142+
// For ECC, the digest must match the key size (e.g., 32 bytes for P-256)
143+
// Pad with leading zeros if necessary
144+
int keySizeBytes = _algorithm.GetKeySizeBytes();
148145

149-
byte[] digest = new byte[bufferSize];
150-
int offset = bufferSize - (digester.HashSize / 8);
146+
if (digest.Length == keySizeBytes)
147+
{
148+
return digest;
149+
}
151150

152-
// If offset < 0, that means the digest is too big.
153-
if (offset < 0)
151+
if (digest.Length > keySizeBytes)
154152
{
155153
throw new ArgumentException(
156154
string.Format(
157155
CultureInfo.CurrentCulture,
158156
InvalidAlgorithmMessage));
159157
}
160158

161-
_ = digester.TransformFinalBlock(data, 0, data.Length);
162-
Array.Copy(digester.Hash, 0, digest, offset, digest.Length);
159+
// Pad with leading zeros
160+
byte[] paddedDigest = new byte[keySizeBytes];
161+
int offset = keySizeBytes - digest.Length;
162+
Array.Copy(digest, 0, paddedDigest, offset, digest.Length);
163163

164-
return digest;
164+
return paddedDigest;
165165
}
166166

167167
// Create a block of data that is the data to sign padded following the
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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+
using System.Security.Cryptography;
17+
using Xunit;
18+
using Yubico.YubiKey.Cryptography;
19+
20+
namespace Yubico.YubiKey.Sample
21+
{
22+
/// <summary>
23+
/// Unit tests for YubiKeySignatureGenerator.DigestData logic.
24+
/// These tests verify the fix for the regression introduced in commit 01d2a667
25+
/// where _algorithm.GetKeySizeBytes() was incorrectly used instead of the hash digest size.
26+
///
27+
/// Since YubiKeySignatureGenerator is in an example project that cannot be referenced
28+
/// from unit tests (strong naming issues), these tests verify the digest computation
29+
/// logic directly using the same approach the fixed code uses.
30+
/// </summary>
31+
public class YubiKeySignatureGeneratorDigestDataTests
32+
{
33+
private static readonly byte[] TestData = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
34+
35+
/// <summary>
36+
/// Computes a digest using the same logic as the fixed YubiKeySignatureGenerator.DigestData method.
37+
/// For RSA: returns the raw digest.
38+
/// For ECC: pads the digest to key size with leading zeros if needed.
39+
/// </summary>
40+
private static byte[] ComputeDigestData(byte[] data, HashAlgorithmName hashAlgorithm, KeyType keyType)
41+
{
42+
byte[] digest = ComputeMessageDigest(data, hashAlgorithm);
43+
44+
// For RSA, return the raw digest - PadRsa handles the signature padding
45+
if (keyType.IsRSA())
46+
{
47+
return digest;
48+
}
49+
50+
// For ECC, the digest must match the key size (e.g., 32 bytes for P-256)
51+
// Pad with leading zeros if necessary
52+
int keySizeBytes = keyType.GetKeySizeBytes();
53+
54+
if (digest.Length == keySizeBytes)
55+
{
56+
return digest;
57+
}
58+
59+
if (digest.Length > keySizeBytes)
60+
{
61+
throw new ArgumentException("Digest is larger than key size");
62+
}
63+
64+
// Pad with leading zeros
65+
byte[] paddedDigest = new byte[keySizeBytes];
66+
int offset = keySizeBytes - digest.Length;
67+
Array.Copy(digest, 0, paddedDigest, offset, digest.Length);
68+
69+
return paddedDigest;
70+
}
71+
72+
/// <summary>
73+
/// Computes a message digest using the same logic as MessageDigestOperations.ComputeMessageDigest.
74+
/// </summary>
75+
private static byte[] ComputeMessageDigest(byte[] dataToDigest, HashAlgorithmName hashAlgorithm)
76+
{
77+
using HashAlgorithm digester = hashAlgorithm.Name switch
78+
{
79+
"SHA1" => CryptographyProviders.Sha1Creator(),
80+
"SHA256" => CryptographyProviders.Sha256Creator(),
81+
"SHA384" => CryptographyProviders.Sha384Creator(),
82+
"SHA512" => CryptographyProviders.Sha512Creator(),
83+
_ => throw new ArgumentException("Unsupported algorithm"),
84+
};
85+
86+
byte[] digest = new byte[digester.HashSize / 8];
87+
88+
_ = digester.TransformFinalBlock(dataToDigest, 0, dataToDigest.Length);
89+
Array.Copy(digester.Hash!, 0, digest, 0, digest.Length);
90+
91+
return digest;
92+
}
93+
94+
/// <summary>
95+
/// Computes a digest using the OLD BUGGY logic that was in YubiKeySignatureGenerator.DigestData.
96+
/// This is used to verify that the bug would cause failures.
97+
/// </summary>
98+
private static byte[] ComputeDigestDataBuggy(byte[] data, HashAlgorithmName hashAlgorithm, KeyType keyType)
99+
{
100+
using HashAlgorithm digester = hashAlgorithm.Name switch
101+
{
102+
"SHA1" => CryptographyProviders.Sha1Creator(),
103+
"SHA256" => CryptographyProviders.Sha256Creator(),
104+
"SHA384" => CryptographyProviders.Sha384Creator(),
105+
"SHA512" => CryptographyProviders.Sha512Creator(),
106+
_ => throw new ArgumentException("Unsupported algorithm"),
107+
};
108+
109+
// BUG: This uses key size (256 bytes for RSA2048) instead of digest size (32 bytes for SHA256)
110+
int bufferSize = keyType.GetKeySizeBytes();
111+
112+
byte[] digest = new byte[bufferSize];
113+
int offset = bufferSize - (digester.HashSize / 8);
114+
115+
if (offset < 0)
116+
{
117+
throw new ArgumentException("Digest too big");
118+
}
119+
120+
_ = digester.TransformFinalBlock(data, 0, data.Length);
121+
// BUG: This tries to copy digest.Length (256) bytes from a 32-byte Hash array
122+
Array.Copy(digester.Hash!, 0, digest, offset, digest.Length);
123+
124+
return digest;
125+
}
126+
127+
[Theory]
128+
[InlineData(KeyType.RSA2048, "SHA256", 32)]
129+
[InlineData(KeyType.RSA2048, "SHA384", 48)]
130+
[InlineData(KeyType.RSA2048, "SHA512", 64)]
131+
[InlineData(KeyType.RSA1024, "SHA256", 32)]
132+
[InlineData(KeyType.RSA3072, "SHA256", 32)]
133+
[InlineData(KeyType.RSA4096, "SHA256", 32)]
134+
public void DigestData_RSA_ReturnsCorrectDigestSize(KeyType keyType, string hashName, int expectedSize)
135+
{
136+
// Arrange
137+
var hashAlgorithm = new HashAlgorithmName(hashName);
138+
139+
// Act
140+
byte[] digest = ComputeDigestData(TestData, hashAlgorithm, keyType);
141+
142+
// Assert
143+
Assert.Equal(expectedSize, digest.Length);
144+
}
145+
146+
[Fact]
147+
public void DigestData_RSA2048_SHA256_FixedVersion_DoesNotThrow()
148+
{
149+
// This is the specific scenario from the bug report:
150+
// RSA2048 with SHA256 - the fixed version should not throw
151+
var exception = Record.Exception(() =>
152+
ComputeDigestData(TestData, HashAlgorithmName.SHA256, KeyType.RSA2048));
153+
154+
Assert.Null(exception);
155+
}
156+
157+
[Fact]
158+
public void DigestData_RSA2048_SHA256_BuggyVersion_Throws()
159+
{
160+
// This demonstrates the bug: RSA2048 with SHA256 was throwing because
161+
// it tried to copy 256 bytes (key size) from a 32-byte array (hash size)
162+
Assert.Throws<ArgumentException>(() =>
163+
ComputeDigestDataBuggy(TestData, HashAlgorithmName.SHA256, KeyType.RSA2048));
164+
}
165+
166+
[Theory]
167+
[InlineData(KeyType.ECP256, "SHA256", 32)] // Digest matches key size
168+
[InlineData(KeyType.ECP384, "SHA256", 48)] // Digest (32) padded to key size (48)
169+
[InlineData(KeyType.ECP384, "SHA384", 48)] // Digest matches key size
170+
[InlineData(KeyType.ECP521, "SHA256", 66)] // Digest (32) padded to key size (66)
171+
[InlineData(KeyType.ECP521, "SHA384", 66)] // Digest (48) padded to key size (66)
172+
[InlineData(KeyType.ECP521, "SHA512", 66)] // Digest (64) padded to key size (66)
173+
public void DigestData_ECC_ReturnsCorrectDigestSize(KeyType keyType, string hashName, int expectedSize)
174+
{
175+
// Arrange
176+
var hashAlgorithm = new HashAlgorithmName(hashName);
177+
178+
// Act
179+
byte[] digest = ComputeDigestData(TestData, hashAlgorithm, keyType);
180+
181+
// Assert
182+
Assert.Equal(expectedSize, digest.Length);
183+
}
184+
185+
[Theory]
186+
[InlineData(KeyType.ECP256, "SHA384")] // SHA384 (48 bytes) > P-256 key size (32 bytes)
187+
[InlineData(KeyType.ECP256, "SHA512")] // SHA512 (64 bytes) > P-256 key size (32 bytes)
188+
public void DigestData_ECC_ThrowsWhenDigestLargerThanKeySize(KeyType keyType, string hashName)
189+
{
190+
// Arrange
191+
var hashAlgorithm = new HashAlgorithmName(hashName);
192+
193+
// Act & Assert
194+
Assert.Throws<ArgumentException>(() => ComputeDigestData(TestData, hashAlgorithm, keyType));
195+
}
196+
197+
[Theory]
198+
[InlineData(KeyType.ECP384, "SHA256", 16)] // P-384 (48) - SHA256 (32) = 16 bytes padding
199+
[InlineData(KeyType.ECP521, "SHA256", 34)] // P-521 (66) - SHA256 (32) = 34 bytes padding
200+
[InlineData(KeyType.ECP521, "SHA384", 18)] // P-521 (66) - SHA384 (48) = 18 bytes padding
201+
public void DigestData_ECC_PadsWithLeadingZeros(KeyType keyType, string hashName, int expectedPadding)
202+
{
203+
// Arrange
204+
var hashAlgorithm = new HashAlgorithmName(hashName);
205+
206+
// Act
207+
byte[] digest = ComputeDigestData(TestData, hashAlgorithm, keyType);
208+
209+
// Assert - first bytes should be zeros (padding)
210+
for (int i = 0; i < expectedPadding; i++)
211+
{
212+
Assert.Equal(0, digest[i]);
213+
}
214+
215+
// The non-zero hash data should start after the padding
216+
bool hasNonZeroData = false;
217+
for (int i = expectedPadding; i < digest.Length; i++)
218+
{
219+
if (digest[i] != 0)
220+
{
221+
hasNonZeroData = true;
222+
break;
223+
}
224+
}
225+
Assert.True(hasNonZeroData, "Expected non-zero hash data after padding");
226+
}
227+
}
228+
}

0 commit comments

Comments
 (0)