/* * eID Applet Project. * Copyright (C) 2011 FedICT. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version * 3.0 as published by the Free Software Foundation. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, see * http://www.gnu.org/licenses/. */ package test.be.fedict.eid.applet; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; import java.security.MessageDigest; import java.security.SecureRandom; import java.security.Signature; import java.security.cert.X509Certificate; import java.util.Date; import java.util.List; import java.util.Locale; import javax.crypto.Cipher; import javax.smartcardio.CardChannel; import javax.smartcardio.CommandAPDU; import javax.smartcardio.ResponseAPDU; import org.apache.commons.codec.binary.Hex; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.asn1.x509.DigestInfo; import org.junit.After; import org.junit.Before; import org.junit.Test; import be.fedict.eid.applet.Messages; import be.fedict.eid.applet.sc.PcscEid; /** * Integration tests for the new secure pinpad readers from FedICT. * <p/> * These readers implement the specifications as described at: * http://code.google.com/p/eid-applet/wiki/SmartCardReader * * @author Frank Cornelis * */ public class SecurePinPadReaderTest { private static final Log LOG = LogFactory.getLog(SecurePinPadReaderTest.class); private Messages messages; private PcscEid pcscEid; /** * To aid the acceptance of the secure smart card reader we use specific QA * annotations to mark integration tests. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface QualityAssurance { Firmware firmware(); boolean approved(); } /** * Enumeration of the different versions of firmware. */ public enum Firmware { /** * First test sample provided at 18/08/2011. */ V006Z, /** * Second test sample provided at 13/10/2011. */ V010Z, /** * Fourth test sample provided at 14/02/2012. */ V012Z, /** * Fifth test sample provided at 05/2012. */ V015Z, /** * Not applicable. */ NA } @Before public void beforeTest() throws Exception { this.messages = new Messages(new Locale("fr")); // new Messages(new Locale("nl")); LOG.debug("locale: " + this.messages.getLocale()); this.pcscEid = new PcscEid(new TestView(), this.messages); if (false == this.pcscEid.isEidPresent()) { LOG.debug("insert eID card"); this.pcscEid.waitForEidPresent(); } } @After public void afterTest() throws Exception { this.pcscEid.close(); } /** * Creates a regular SHA1 signature using the non-repudiation key. * <p/> * Remark: right now you have to wait until the digest value has been * scrolled completely before being able to continue. Fixed in V015Z. * <p/> * Remark: The smart card reader does not honor the wLangId of the CCID pin * verification data structure yet. V010Z still does not honor the wLangId. * V015Z fixes this, except for dutch. (0x13, 0x04) * <p/> * V010Z: the reader first displays "Sign Hash?", then it requests the * "Authentication PIN?" and then it asks to "Sign Hash?" again. This is due * to the way the eID Applet code has been constructed. Fixed in recent * version of the eID Applet. * * @throws Exception */ @Test @QualityAssurance(firmware = Firmware.V015Z, approved = true) public void testRegularDigestValueWithNonRepudiation() throws Exception { this.pcscEid.sign("hello world".getBytes(), "SHA1"); } /** * Secure PIN Entry Capabilities * <p/> * PC/SC specs Interoperability Specification for ICCs and Personal Computer * Systems Part 10 IFDs with Secure PIN Entry Capabilities allow room for * vendor specific feature tags within the range of 0x80 – 0xFE. So we could * add a feature tag to indicate the specific capabilities of the new smart * card readers. This would allow us to have better user interaction. I.e. * when the smart card readers asks for validation of the digest value, the * software UI could display some info message that you have to check the * reader display to be able to continue. * <p/> * V012Z indicates ffffff80, so 0x80. * <p/> * V015Z no longer indicates this. * * @see http * ://www.pcscworkgroup.com/specifications/files/pcsc10_v2.02.08.pdf * @throws Exception */ @Test @QualityAssurance(firmware = Firmware.V015Z, approved = true) public void testGetCCIDFeatures() throws Exception { int ioctl; String osName = System.getProperty("os.name"); if (osName.startsWith("Windows")) { ioctl = (0x31 << 16 | (3400) << 2); } else { ioctl = 0x42000D48; } byte[] features = this.pcscEid.getCard().transmitControlCommand(ioctl, new byte[0]); int idx = 0; while (idx < features.length) { byte tag = features[idx]; idx++; idx++; LOG.debug("CCID feature tag: " + Integer.toHexString(tag)); idx += 4; } } @Test @QualityAssurance(firmware = Firmware.V015Z, approved = true) public void testRegularDigestValueWithAuth() throws Exception { byte[] signatureValue = this.pcscEid.signAuthn("hello world".getBytes()); LOG.debug("signature value size: " + signatureValue.length); assertEquals(128, signatureValue.length); } @Test @QualityAssurance(firmware = Firmware.V015Z, approved = true) public void testPlainTextAuthn() throws Exception { // operate String testMessage = "Test Application @ 14/2/2012 14:48:21"; byte[] signatureValue = this.pcscEid.sign(testMessage.getBytes(), "2.16.56.1.2.1.3.1", (byte) 0x82, false); // verify List<X509Certificate> authnCertChain = this.pcscEid.getAuthnCertificateChain(); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, authnCertChain.get(0)); byte[] signatureDigestInfoValue = cipher.doFinal(signatureValue); ASN1InputStream aIn = new ASN1InputStream(signatureDigestInfoValue); DigestInfo signatureDigestInfo = new DigestInfo((ASN1Sequence) aIn.readObject()); LOG.debug("result algo Id: " + signatureDigestInfo.getAlgorithmId().getObjectId().getId()); assertEquals("2.16.56.1.2.1.3.1", signatureDigestInfo.getAlgorithmId().getObjectId().getId()); assertArrayEquals(testMessage.getBytes(), signatureDigestInfo.getDigest()); } /** * Create a plain text authentication signature, directly after creating a * regular SHA1 authentication signature. This is the sequence that will be * implemented in the eID Applet. * <p/> * V006Z: Remark: without the SET APDU the secure smart card reader won't * display the plain text message. Fixed in V010Z. * <p/> * V012Z: language support is still shaky. * <p/> * V015Z also performs a logoff in case of plain text. Good. * * @throws Exception */ @Test @QualityAssurance(firmware = Firmware.V015Z, approved = true) public void testAuthnSignPlainText() throws Exception { CardChannel cardChannel = this.pcscEid.getCardChannel(); List<X509Certificate> authnCertChain = this.pcscEid.getAuthnCertificateChain(); /* * Make sure that the PIN authorization is already OK. */ this.pcscEid.signAuthn("hello world".getBytes()); CommandAPDU setApdu = new CommandAPDU(0x00, 0x22, 0x41, 0xB6, new byte[] { 0x04, // length of following data (byte) 0x80, // algo ref 0x01, // rsa pkcs#1 (byte) 0x84, // tag for private key ref (byte) 0x82 }); // auth key // ResponseAPDU responseApdu = cardChannel.transmit(setApdu); // assertEquals(0x9000, responseApdu.getSW()); String textMessage = "My Testcase"; AlgorithmIdentifier algoId = new AlgorithmIdentifier("2.16.56.1.2.1.3.1"); DigestInfo digestInfo = new DigestInfo(algoId, textMessage.getBytes()); LOG.debug("DigestInfo DER encoded: " + new String(Hex.encodeHex(digestInfo.getDEREncoded()))); CommandAPDU computeDigitalSignatureApdu = new CommandAPDU(0x00, 0x2A, 0x9E, 0x9A, digestInfo.getDEREncoded()); ResponseAPDU responseApdu2 = cardChannel.transmit(computeDigitalSignatureApdu); assertEquals(0x9000, responseApdu2.getSW()); byte[] signatureValue = responseApdu2.getData(); LOG.debug("signature value size: " + signatureValue.length); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, authnCertChain.get(0)); byte[] signatureDigestInfoValue = cipher.doFinal(signatureValue); ASN1InputStream aIn = new ASN1InputStream(signatureDigestInfoValue); DigestInfo signatureDigestInfo = new DigestInfo((ASN1Sequence) aIn.readObject()); LOG.debug("result algo Id: " + signatureDigestInfo.getAlgorithmId().getObjectId().getId()); assertEquals("2.16.56.1.2.1.3.1", signatureDigestInfo.getAlgorithmId().getObjectId().getId()); assertArrayEquals(textMessage.getBytes(), signatureDigestInfo.getDigest()); } /** * Creates a non-repudiation signature with plain text. * <p/> * Remark: "Enter NonRep PIN" should maybe be replaced with "Enter Sign PIN" * . Fixed in V010Z. * * @throws Exception */ @Test @QualityAssurance(firmware = Firmware.V015Z, approved = true) public void testNonRepSignPlainText() throws Exception { CardChannel cardChannel = this.pcscEid.getCardChannel(); List<X509Certificate> signCertChain = this.pcscEid.getSignCertificateChain(); CommandAPDU setApdu = new CommandAPDU(0x00, 0x22, 0x41, 0xB6, new byte[] { 0x04, // length of following data (byte) 0x80, // algo ref 0x01, // rsa pkcs#1 (byte) 0x84, // tag for private key ref (byte) 0x83 }); // non-rep key ResponseAPDU responseApdu = cardChannel.transmit(setApdu); assertEquals(0x9000, responseApdu.getSW()); this.pcscEid.verifyPin(); String textMessage = "My Testcase"; AlgorithmIdentifier algoId = new AlgorithmIdentifier("2.16.56.1.2.1.3.1"); DigestInfo digestInfo = new DigestInfo(algoId, textMessage.getBytes()); CommandAPDU computeDigitalSignatureApdu = new CommandAPDU(0x00, 0x2A, 0x9E, 0x9A, digestInfo.getDEREncoded()); responseApdu = cardChannel.transmit(computeDigitalSignatureApdu); assertEquals(0x9000, responseApdu.getSW()); byte[] signatureValue = responseApdu.getData(); LOG.debug("signature value size: " + signatureValue.length); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, signCertChain.get(0)); byte[] signatureDigestInfoValue = cipher.doFinal(signatureValue); ASN1InputStream aIn = new ASN1InputStream(signatureDigestInfoValue); DigestInfo signatureDigestInfo = new DigestInfo((ASN1Sequence) aIn.readObject()); LOG.debug("result algo Id: " + signatureDigestInfo.getAlgorithmId().getObjectId().getId()); assertEquals("2.16.56.1.2.1.3.1", signatureDigestInfo.getAlgorithmId().getObjectId().getId()); assertArrayEquals(textMessage.getBytes(), signatureDigestInfo.getDigest()); } /** * Only applicable for 2048 bit keys. * * @throws Exception */ @Test @QualityAssurance(firmware = Firmware.V015Z, approved = true) public void testLargePlainTextMessage() throws Exception { CardChannel cardChannel = this.pcscEid.getCardChannel(); List<X509Certificate> signCertChain = this.pcscEid.getSignCertificateChain(); CommandAPDU setApdu = new CommandAPDU(0x00, 0x22, 0x41, 0xB6, new byte[] { 0x04, // length of following data (byte) 0x80, // algo ref 0x01, // rsa pkcs#1 (byte) 0x84, // tag for private key ref (byte) 0x83 }); // non-rep key ResponseAPDU responseApdu = cardChannel.transmit(setApdu); assertEquals(0x9000, responseApdu.getSW()); this.pcscEid.verifyPin(); byte[] data = new byte[115]; /* * If the length of the plain text message is >= 115, the message is not * visualized by the secure pinpad reader. */ SecureRandom secureRandom = new SecureRandom(); secureRandom.nextBytes(data); AlgorithmIdentifier algoId = new AlgorithmIdentifier("2.16.56.1.2.1.3.1"); DigestInfo digestInfo = new DigestInfo(algoId, data); CommandAPDU computeDigitalSignatureApdu = new CommandAPDU(0x00, 0x2A, 0x9E, 0x9A, digestInfo.getDEREncoded()); responseApdu = cardChannel.transmit(computeDigitalSignatureApdu); assertEquals(0x9000, responseApdu.getSW()); byte[] signatureValue = responseApdu.getData(); LOG.debug("signature value size: " + signatureValue.length); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, signCertChain.get(0)); byte[] signatureDigestInfoValue = cipher.doFinal(signatureValue); ASN1InputStream aIn = new ASN1InputStream(signatureDigestInfoValue); DigestInfo signatureDigestInfo = new DigestInfo((ASN1Sequence) aIn.readObject()); LOG.debug("result algo Id: " + signatureDigestInfo.getAlgorithmId().getObjectId().getId()); assertEquals("2.16.56.1.2.1.3.1", signatureDigestInfo.getAlgorithmId().getObjectId().getId()); assertArrayEquals(data, signatureDigestInfo.getDigest()); } /** * When creating a non-repudiation signature using PKCS#1-SHA1 (non-naked) * the digest value should also be confirmed via the secure pinpad reader. * * @throws Exception */ @Test @QualityAssurance(firmware = Firmware.V015Z, approved = true) public void testNonRepSignPKCS1_SHA1() throws Exception { CardChannel cardChannel = this.pcscEid.getCardChannel(); List<X509Certificate> signCertChain = this.pcscEid.getSignCertificateChain(); CommandAPDU setApdu = new CommandAPDU(0x00, 0x22, 0x41, 0xB6, new byte[] { 0x04, // length of following data (byte) 0x80, // algo ref 0x02, // RSA PKCS#1 SHA1 (byte) 0x84, // tag for private key ref (byte) 0x83 }); // non-rep key ResponseAPDU responseApdu = cardChannel.transmit(setApdu); assertEquals(0x9000, responseApdu.getSW()); this.pcscEid.verifyPin(); byte[] data = "My Testcase".getBytes(); MessageDigest messageDigest = MessageDigest.getInstance("SHA1"); byte[] digestValue = messageDigest.digest(data); CommandAPDU computeDigitalSignatureApdu = new CommandAPDU(0x00, 0x2A, 0x9E, 0x9A, digestValue); responseApdu = cardChannel.transmit(computeDigitalSignatureApdu); assertEquals(0x9000, responseApdu.getSW()); byte[] signatureValue = responseApdu.getData(); LOG.debug("signature value size: " + signatureValue.length); Signature signature = Signature.getInstance("SHA1withRSA"); signature.initVerify(signCertChain.get(0).getPublicKey()); signature.update(data); boolean result = signature.verify(signatureValue); assertTrue(result); } @Test @QualityAssurance(firmware = Firmware.NA, approved = true) public void testQualityAssurance() throws Exception { LOG.debug("Quality Assurance report"); LOG.debug("Date: " + new Date()); LOG.debug("Tester: " + System.getProperty("user.name")); Method[] methods = SecurePinPadReaderTest.class.getMethods(); for (Method method : methods) { Test testAnnotation = method.getAnnotation(Test.class); if (null == testAnnotation) { continue; } QualityAssurance qualityAssuranceAnnotation = method.getAnnotation(QualityAssurance.class); if (null == qualityAssuranceAnnotation) { throw new RuntimeException("missing QualityAssurance status"); } LOG.debug("Test: " + method.getName()); LOG.debug("\tFirmware version: " + qualityAssuranceAnnotation.firmware()); LOG.debug("\tApproved: " + qualityAssuranceAnnotation.approved()); } } }