Skip to content

Commit 424f154

Browse files
committed
Zeroize a copy of the card user PIN in the YubiKey decryptor factories, not the provider's array
1 parent ab16374 commit 424f154

5 files changed

Lines changed: 332 additions & 4 deletions

File tree

‎pg/src/main/java/org/bouncycastle/openpgp/api/KeyPassphraseProvider.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public interface KeyPassphraseProvider
1414
* Return the passphrase for the given key.
1515
* This callback is only fired, if the key is locked and a passphrase is required to unlock it.
1616
* Returning null means, that the passphrase is not available.
17+
* The returned array remains owned by the provider; a caller that needs to zeroize the
18+
* passphrase after use must clone it first.
1719
*
1820
* @param key the locked (sub-)key.
1921
* @return passphrase or null

‎pgsc/src/main/java/org/bouncycastle/openpgp/smartcard/yubikey/operator/bc/BcYubikeyPublicKeyDataDecryptorFactory.java‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,13 +224,13 @@ private static boolean isSupportedCurve(String curveName)
224224
}
225225

226226
/**
227-
* Fetch the card's user PIN. The returned array is the caller's to zeroize once the card has
228-
* verified it.
227+
* Fetch the card's user PIN. A defensive copy of the provider's array is returned, so
228+
* the copy is the caller's to zeroize once the card has verified it.
229229
*/
230230
private char[] requireUserPin()
231231
throws KeyPassphraseException
232232
{
233-
char[] pin = userPinProvider.getKeyPassword(getSecretKey());
233+
char[] pin = Arrays.clone(userPinProvider.getKeyPassword(getSecretKey()));
234234
if (pin == null || pin.length == 0)
235235
{
236236
throw new KeyPassphraseException(getSecretKey(), new IllegalStateException("PIN required."));

‎pgsc/src/main/java/org/bouncycastle/openpgp/smartcard/yubikey/operator/jcajce/JceYubikeyPublicKeyDataDecryptorFactoryBuilder.java‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,15 @@ private static boolean isSupportedCurve(String curveName)
173173
|| "curve25519".equals(curveName);
174174
}
175175

176+
/**
177+
* Fetch the card's user PIN. A defensive copy of the provider's array is returned, so
178+
* the copy is the caller's to zeroize once the card has verified it.
179+
*/
176180
private char[] requireUserPin(KeyPassphraseProvider userPinProvider,
177181
OpenPGPKey.OpenPGPSecretKey key)
178182
throws KeyPassphraseException
179183
{
180-
char[] pin = userPinProvider.getKeyPassword(key);
184+
char[] pin = Arrays.clone(userPinProvider.getKeyPassword(key));
181185
if (pin == null || pin.length == 0)
182186
{
183187
throw new KeyPassphraseException(key, new IllegalStateException("PIN required."));

‎pgsc/src/test/java/org/bouncycastle/openpgp/smartcard/simulator/SimulatorTests.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.bouncycastle.openpgp.smartcard.test.AnonymousRecipientSmartCardDecryptionTest;
77
import org.bouncycastle.openpgp.smartcard.test.SmartCardMessageDecryptionTest;
88
import org.bouncycastle.openpgp.smartcard.test.SmartCardTestProperties;
9+
import org.bouncycastle.openpgp.smartcard.test.SmartCardUserPinOwnershipTest;
910
import org.bouncycastle.openpgp.smartcard.test.UnrelatedSmartCardMessageDecryptionTest;
1011
import org.bouncycastle.util.test.SimpleTestResult;
1112

@@ -25,6 +26,7 @@ public void testSimulatorSmartCard()
2526
new SmartCardMessageDecryptionTest(m, p),
2627
new AnonymousRecipientSmartCardDecryptionTest(m, p),
2728
new UnrelatedSmartCardMessageDecryptionTest(m, p),
29+
new SmartCardUserPinOwnershipTest(m, p),
2830
new SimulatorSmartCardTest(m, p)
2931
};
3032

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
package org.bouncycastle.openpgp.smartcard.test;
2+
3+
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
4+
import org.bouncycastle.bcpg.PublicKeyEncSessionPacket;
5+
import org.bouncycastle.crypto.InvalidCipherTextException;
6+
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
7+
import org.bouncycastle.openpgp.PGPException;
8+
import org.bouncycastle.openpgp.api.KeyPairGeneratorCallback;
9+
import org.bouncycastle.openpgp.api.KeyPassphraseProvider;
10+
import org.bouncycastle.openpgp.api.OpenPGPKey;
11+
import org.bouncycastle.openpgp.api.OpenPGPMessageInputStream;
12+
import org.bouncycastle.openpgp.api.OpenPGPMessageOutputStream;
13+
import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator;
14+
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
15+
import org.bouncycastle.openpgp.smartcard.OpenPGPSmartCard;
16+
import org.bouncycastle.openpgp.smartcard.OpenPGPSmartCardManager;
17+
import org.bouncycastle.openpgp.smartcard.card.CardException;
18+
import org.bouncycastle.openpgp.smartcard.simulator.SimulatorOpenPGPSmartCard;
19+
import org.bouncycastle.openpgp.smartcard.simulator.SimulatorSmartCardBackend;
20+
import org.bouncycastle.openpgp.smartcard.yubikey.YubikeySmartCardBackend;
21+
import org.bouncycastle.openpgp.smartcard.yubikey.YubikeyTestInstanceProvider;
22+
import org.bouncycastle.openpgp.smartcard.yubikey.YubikeyTestProperties;
23+
import org.bouncycastle.openpgp.smartcard.yubikey.operator.bc.BcYubikeyPublicKeyDataDecryptorFactory;
24+
import org.bouncycastle.openpgp.smartcard.yubikey.operator.jcajce.JceYubikeyPublicKeyDataDecryptorFactoryBuilder;
25+
import org.bouncycastle.util.Arrays;
26+
import org.bouncycastle.util.io.Streams;
27+
28+
import java.io.ByteArrayInputStream;
29+
import java.io.ByteArrayOutputStream;
30+
import java.io.IOException;
31+
import java.nio.charset.StandardCharsets;
32+
import java.util.ArrayList;
33+
import java.util.List;
34+
35+
/**
36+
* A card user PIN fetched from a {@link KeyPassphraseProvider} stays owned by the provider.
37+
* <p>
38+
* Both {@link KeyPassphraseProvider} implementations BC ships hand out the array the application
39+
* registered rather than a copy of it - {@code DefaultKeyPassphraseProvider} returns the
40+
* {@code char[]} held in its cache, and the anonymous provider inside {@code OpenPGPApi.editKey}
41+
* returns the caller's array verbatim - and every other consumer of
42+
* {@link KeyPassphraseProvider#getKeyPassword} borrows the array and leaves it alone. The two
43+
* YubiKey decryptor factories instead zeroized what they were handed, in a finally block after
44+
* every private-key operation, so the first decryption destroyed the application's PIN: the next
45+
* private-key operation presented an all-zero PIN to the card, which fails and costs a PIN retry.
46+
* They now clear a copy of their own, which is the rule {@code ECJPAKEParticipant} and
47+
* {@code JPAKEParticipant} state for a password they mean to clear.
48+
* <p>
49+
* The two factory cases need no token. The PIN handling brackets the card call - the PIN is fetched
50+
* before the session is opened and cleared in a finally block after it - so driving a factory with
51+
* no card exercises that handling in full, and only the card operation in between fails. Each case
52+
* asserts the PIN really was fetched, so it cannot pass by failing ahead of the fetch.
53+
* <p>
54+
* The simulator case then locks the borrowing convention on the backend that runs with no hardware
55+
* present: it unlocks the key it holds through the {@link KeyPassphraseProvider} it is handed, so a
56+
* PIN is presented on every private-key operation as it is on a real card.
57+
*/
58+
public class SmartCardUserPinOwnershipTest
59+
extends AbstractOpenPGPSmartCardTest
60+
{
61+
public SmartCardUserPinOwnershipTest(OpenPGPSmartCardManager manager,
62+
SmartCardTestProperties properties)
63+
{
64+
super(manager, properties);
65+
}
66+
67+
@Override
68+
public String getName()
69+
{
70+
return "SmartCardUserPinOwnershipTest";
71+
}
72+
73+
@Override
74+
public void performTest()
75+
throws Exception
76+
{
77+
testPinSurvivesTwoConsecutiveDecryptions();
78+
testBcYubikeyFactoryBorrowsProvidersPin();
79+
testJceYubikeyFactoryBorrowsProvidersPin();
80+
}
81+
82+
/**
83+
* Decrypt two messages in a row against one cached PIN array, the shape an application gets from
84+
* {@code OpenPGPMessageProcessor} for free: the PIN it registers is cached and handed to the
85+
* card backend on every private-key operation, so a backend that zeroized it would destroy the
86+
* application's PIN during the first message and present zeros during the second.
87+
*/
88+
private void testPinSurvivesTwoConsecutiveDecryptions()
89+
throws PGPException, IOException, CardException
90+
{
91+
OpenPGPSmartCard card = manager.findSmartCard(properties.getSerialNumber());
92+
// -DM System.out.println
93+
System.out.println("Test user PIN ownership over two messages on " + card.getCardType() + " " + card.getVersion() + " (" + card.getBackend().getName() + ")");
94+
95+
char[] expectedPin = properties.getUserPin();
96+
char[] applicationPin = properties.getUserPin();
97+
98+
OpenPGPKey softwareKey = keyOnCard(card, expectedPin);
99+
OpenPGPKey externalKey = toExternalKey(softwareKey, null);
100+
101+
isTrue("the stripped key must be marked external",
102+
externalKey.getSecretKey(externalKey.getEncryptionKeys().get(0))
103+
.getPGPSecretKey().isExternalKey());
104+
105+
RecordingPinProvider pinProvider = new RecordingPinProvider(applicationPin);
106+
107+
for (int i = 1; i <= 2; i++)
108+
{
109+
byte[] plaintext = ("Message " + i + " to a card-held key.\n").getBytes(StandardCharsets.UTF_8);
110+
111+
isTrue("message " + i + ": decrypted plaintext mismatch",
112+
Arrays.areEqual(plaintext, decrypt(externalKey, pinProvider, encrypt(softwareKey, plaintext))));
113+
isTrue("message " + i + ": the application's PIN buffer must be intact after decryption",
114+
Arrays.areEqual(expectedPin, applicationPin));
115+
}
116+
117+
isEquals("the card must be presented the PIN once per message", 2, pinProvider.presentations.size());
118+
for (int i = 0; i != pinProvider.presentations.size(); i++)
119+
{
120+
isTrue("presentation " + (i + 1) + " must carry the real PIN rather than a cleared buffer",
121+
Arrays.areEqual(expectedPin, pinProvider.presentations.get(i)));
122+
}
123+
}
124+
125+
private void testBcYubikeyFactoryBorrowsProvidersPin()
126+
throws PGPException, InvalidCipherTextException
127+
{
128+
// -DM System.out.println
129+
System.out.println("Test BcYubikeyPublicKeyDataDecryptorFactory borrows the PIN it is given");
130+
131+
char[] expectedPin = properties.getUserPin();
132+
char[] applicationPin = properties.getUserPin();
133+
RecordingPinProvider pinProvider = new RecordingPinProvider(applicationPin);
134+
135+
BcYubikeyPublicKeyDataDecryptorFactory factory =
136+
new BcYubikeyPublicKeyDataDecryptorFactory(externalDecryptionKey(), null, pinProvider);
137+
138+
try
139+
{
140+
factory.getExternalKeyCryptoCallback().decryptRSA(
141+
PublicKeyAlgorithmTags.RSA_GENERAL, new byte[]{(byte)0xAA}, (AsymmetricKeyParameter)null);
142+
fail("a private-key operation with no card present must fail");
143+
}
144+
catch (RuntimeException e)
145+
{
146+
// expected: there is no card to open a session on
147+
}
148+
149+
implTestPinBorrowed("BcYubikeyPublicKeyDataDecryptorFactory", pinProvider, expectedPin, applicationPin);
150+
}
151+
152+
private void testJceYubikeyFactoryBorrowsProvidersPin()
153+
throws PGPException
154+
{
155+
// -DM System.out.println
156+
System.out.println("Test JceYubikeyPublicKeyDataDecryptorFactoryBuilder borrows the PIN it is given");
157+
158+
char[] expectedPin = properties.getUserPin();
159+
char[] applicationPin = properties.getUserPin();
160+
RecordingPinProvider pinProvider = new RecordingPinProvider(applicationPin);
161+
162+
PublicKeyDataDecryptorFactory factory =
163+
new JceYubikeyPublicKeyDataDecryptorFactoryBuilder(null, pinProvider).build(externalDecryptionKey());
164+
165+
try
166+
{
167+
factory.recoverSessionData(PublicKeyAlgorithmTags.RSA_GENERAL,
168+
new byte[][]{new byte[]{0, 8, (byte)0xAA}}, PublicKeyEncSessionPacket.VERSION_3);
169+
fail("a private-key operation with no card present must fail");
170+
}
171+
catch (RuntimeException e)
172+
{
173+
// expected: there is no card to open a session on
174+
}
175+
176+
implTestPinBorrowed("JceYubikeyPublicKeyDataDecryptorFactoryBuilder", pinProvider, expectedPin, applicationPin);
177+
}
178+
179+
private void implTestPinBorrowed(String label,
180+
RecordingPinProvider pinProvider,
181+
char[] expectedPin,
182+
char[] applicationPin)
183+
{
184+
isEquals(label + ": the PIN must be fetched from the provider exactly once",
185+
1, pinProvider.presentations.size());
186+
isTrue(label + ": the PIN handed to the card must be the registered one",
187+
Arrays.areEqual(expectedPin, pinProvider.presentations.get(0)));
188+
isTrue(label + ": the provider's PIN array must survive the operation",
189+
Arrays.areEqual(expectedPin, applicationPin));
190+
}
191+
192+
/**
193+
* Generate a key protected with the card's user PIN, move its decryption key onto the card and
194+
* return the software copy the message is encrypted to.
195+
*/
196+
private OpenPGPKey keyOnCard(OpenPGPSmartCard card, char[] userPin)
197+
throws PGPException, CardException
198+
{
199+
card.reset();
200+
201+
// build(char[]) clears the array it is given, so it gets a copy
202+
OpenPGPKey softwareKey = api.generateKey(4)
203+
.withPrimaryKey((KeyPairGeneratorCallback)PGPKeyPairGenerator::generateEd25519KeyPair)
204+
.addEncryptionSubkey((KeyPairGeneratorCallback)PGPKeyPairGenerator::generateX25519KeyPair)
205+
.build(Arrays.clone(userPin));
206+
207+
OpenPGPKey.OpenPGPSecretKey decryptionKey =
208+
softwareKey.getSecretKey(softwareKey.getEncryptionKeys().get(0));
209+
card.uploadDecryptionKey(decryptionKey.unlock(Arrays.clone(userPin)), properties.getAdminPin());
210+
211+
return softwareKey;
212+
}
213+
214+
/**
215+
* An externally-backed decryption key, which is all either factory needs to be built. No card is
216+
* involved: the private key material is simply absent.
217+
*/
218+
private OpenPGPKey.OpenPGPSecretKey externalDecryptionKey()
219+
throws PGPException
220+
{
221+
OpenPGPKey softwareKey = api.generateKey(4)
222+
.withPrimaryKey((KeyPairGeneratorCallback)PGPKeyPairGenerator::generateEd25519KeyPair)
223+
.addEncryptionSubkey((KeyPairGeneratorCallback)PGPKeyPairGenerator::generateX25519KeyPair)
224+
.build();
225+
OpenPGPKey externalKey = toExternalKey(softwareKey, null);
226+
227+
return externalKey.getSecretKey(externalKey.getEncryptionKeys().get(0));
228+
}
229+
230+
private byte[] encrypt(OpenPGPKey recipient, byte[] plaintext)
231+
throws PGPException, IOException
232+
{
233+
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
234+
OpenPGPMessageOutputStream mOut = api.signAndOrEncryptMessage()
235+
.addEncryptionCertificate(recipient.toCertificate())
236+
.open(bOut);
237+
mOut.write(plaintext);
238+
mOut.close();
239+
240+
return bOut.toByteArray();
241+
}
242+
243+
private byte[] decrypt(OpenPGPKey externalKey, KeyPassphraseProvider pinProvider, byte[] message)
244+
throws PGPException, IOException
245+
{
246+
OpenPGPMessageInputStream mIn = api.decryptAndOrVerifyMessage()
247+
.addDecryptionKey(externalKey)
248+
.setMissingOpenPGPKeyPassphraseProvider(pinProvider)
249+
.addPublicKeyDataDecryptorFactoryProvider(manager)
250+
.process(new ByteArrayInputStream(message));
251+
ByteArrayOutputStream plainOut = new ByteArrayOutputStream();
252+
Streams.pipeAll(mIn, plainOut);
253+
mIn.close();
254+
255+
return plainOut.toByteArray();
256+
}
257+
258+
/**
259+
* Hands out one cached array on every call, as
260+
* {@code KeyPassphraseProvider.DefaultKeyPassphraseProvider} does, and records a snapshot of what
261+
* each call returned so the PIN the card was presented can be checked afterwards.
262+
*/
263+
private static class RecordingPinProvider
264+
implements KeyPassphraseProvider
265+
{
266+
final char[] pin;
267+
final List<char[]> presentations = new ArrayList<char[]>();
268+
269+
RecordingPinProvider(char[] pin)
270+
{
271+
this.pin = pin;
272+
}
273+
274+
public char[] getKeyPassword(OpenPGPKey.OpenPGPSecretKey key)
275+
{
276+
presentations.add(Arrays.clone(pin));
277+
return pin;
278+
}
279+
}
280+
281+
public static void main(String[] args)
282+
throws CardException
283+
{
284+
SmartCardTestProperties p;
285+
OpenPGPSmartCardManager m;
286+
287+
// BCYK
288+
try
289+
{
290+
p = new YubikeyTestProperties();
291+
m = YubikeyTestInstanceProvider.prepareOneYubikeySmartCardManager(p, YubikeySmartCardBackend.bcImpl());
292+
runTest(new SmartCardUserPinOwnershipTest(m, p));
293+
}
294+
catch (YubikeyTestInstanceProvider.YubikeySetupException e)
295+
{
296+
// -DM System.out.println
297+
System.out.println("Skipping run of SmartCardUserPinOwnershipTest on BC Yubikey.");
298+
}
299+
300+
// JCYK
301+
try
302+
{
303+
p = new YubikeyTestProperties();
304+
m = YubikeyTestInstanceProvider.prepareOneYubikeySmartCardManager(p, YubikeySmartCardBackend.jceImpl());
305+
runTest(new SmartCardUserPinOwnershipTest(m, p));
306+
}
307+
catch (YubikeyTestInstanceProvider.YubikeySetupException e)
308+
{
309+
// -DM System.out.println
310+
System.out.println("Skipping run of SmartCardUserPinOwnershipTest on JCE Yubikey.");
311+
}
312+
313+
314+
SimulatorSmartCardBackend sim = new SimulatorSmartCardBackend();
315+
sim.addSmartCard(new SimulatorOpenPGPSmartCard(sim, 1312));
316+
m = new OpenPGPSmartCardManager().addBackend(sim);
317+
318+
runTest(new SmartCardUserPinOwnershipTest(m, new SmartCardTestProperties(1312)));
319+
}
320+
}

0 commit comments

Comments
 (0)