/*
* 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());
}
}
}