/**************************************************************************** * Copyright (C) 2012 ecsec GmbH. * All rights reserved. * Contact: ecsec GmbH (info@ecsec.de) * * This file is part of the Open eCard App. * * GNU General Public License Usage * This file may be used under the terms of the GNU General Public * License version 3.0 as published by the Free Software Foundation * and appearing in the file LICENSE.GPL included in the packaging of * this file. Please review the following information to ensure the * GNU General Public License version 3.0 requirements will be met: * http://www.gnu.org/copyleft/gpl.html. * * Other Usage * Alternatively, this file may be used in accordance with the terms * and conditions contained in a signed written agreement between * you and ecsec GmbH. * ***************************************************************************/ package org.openecard.ifd.protocol.pace; import java.security.GeneralSecurityException; import org.openecard.common.ECardConstants; import org.openecard.common.apdu.GeneralAuthenticate; import org.openecard.common.apdu.common.CardCommandAPDU; import org.openecard.common.apdu.common.CardResponseAPDU; import org.openecard.common.apdu.exception.APDUException; import org.openecard.common.ifd.protocol.exception.ProtocolException; import org.openecard.common.interfaces.Dispatcher; import org.openecard.common.util.ByteUtils; import org.openecard.crypto.common.asn1.eac.PACEDomainParameter; import org.openecard.crypto.common.asn1.eac.PACESecurityInfos; import org.openecard.crypto.common.asn1.utils.ObjectIdentifierUtils; import org.openecard.ifd.protocol.pace.apdu.MSESetATPACE; import org.openecard.ifd.protocol.pace.crypto.AuthenticationToken; import org.openecard.ifd.protocol.pace.crypto.KDF; import org.openecard.ifd.protocol.pace.crypto.PACECryptoSuite; import org.openecard.ifd.protocol.pace.crypto.PACEGenericMapping; import org.openecard.ifd.protocol.pace.crypto.PACEIntegratedMapping; import org.openecard.ifd.protocol.pace.crypto.PACEKey; import org.openecard.ifd.protocol.pace.crypto.PACEMapping; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @author Moritz Horsch <horsch@cdc.informatik.tu-darmstadt.de> */ public class PACEImplementation { private static final Logger logger = LoggerFactory.getLogger(PACEImplementation.class); // Communication private Dispatcher dispatcher; private KDF kdf; private byte[] slotHandle; private CardResponseAPDU response; // Crypto private PACEDomainParameter domainParameter; private PACESecurityInfos psi; private PACECryptoSuite cryptoSuite; // Keys private PACEKey keyPCD, keyPICC; private byte[] keyMAC, keyENC; private byte[] password, s; // Certification Authority Reference (CAR) private byte[] currentCAR, previousCAR; private byte retryCounter = 3; // true if PACE is used with a CHAT private boolean specifiedCHAT; /** * Creates a new instance of the pace protocol. * * @param dispatcher Dispatcher * @param slotHandle Slot handle * @param paceSecurityInfos PACESecurityInfos * @throws Exception Exception */ public PACEImplementation(Dispatcher dispatcher, byte[] slotHandle, PACESecurityInfos paceSecurityInfos) throws Exception { this.dispatcher = dispatcher; this.slotHandle = slotHandle; this.psi = paceSecurityInfos; domainParameter = new PACEDomainParameter(psi); cryptoSuite = new PACECryptoSuite(psi, domainParameter); kdf = new KDF(); } /** * Start PACE. * * @param password Password * @param passwordID Password type (PIN, PUK, CAN, MRZ) * @param chat CHAT * @throws Exception Exception */ public void execute(byte[] password, byte passwordID, byte[] chat) throws Exception { this.password = password; specifiedCHAT = (chat != null); mseSetAT(passwordID, chat); } /** * Initialize Chip Authentication. Sends an MSE:Set AT APDU. (S * Step 1: Initialise PACE. * See BSI-TR-03110, version 2.10, part 3, B.11.1. */ private void mseSetAT(byte passwordID, byte[] chat) throws Exception { byte[] oID = ObjectIdentifierUtils.getValue(psi.getPACEInfo().getProtocol()); CardCommandAPDU mseSetAT = new MSESetATPACE(oID, passwordID, chat); try { response = mseSetAT.transmit(dispatcher, slotHandle); // Continue with step 2 generalAuthenticateEncryptedNonce(); } catch (APDUException e) { logger.error(e.getMessage(), e); short sw = e.getResponseAPDU().getSW(); if (sw == PACEConstants.PASSWORD_DEACTIVATED) { // Password is deactivated throw new ProtocolException(ECardConstants.Minor.IFD.PASSWORD_DEACTIVATED); } else if ((sw & (short) 0xFFF0) == (short) 0x63C0) { retryCounter = (byte) (sw & (short) 0x000F); if (retryCounter == (byte) 0x00) { // The password is blocked logger.warn("The password is blocked. The password MUST be unblocked."); if (passwordID == PACEConstants.PASSWORD_PUK) { generalAuthenticateEncryptedNonce(); } else { throw new ProtocolException( ECardConstants.Minor.IFD.PASSWORD_BLOCKED, "The password is blocked. The password MUST be unblocked."); } } else if (retryCounter == (byte) 0x01) { // The password is suspended logger.warn("The password is suspended. The password MUST be resumed."); //TODO check for an existing SM-Channel with the CAN // if (mseSetAT.isSecureMessaging()) { generalAuthenticateEncryptedNonce(); /*} else { throw new ProtocolException( ECardConstants.Minor.IFD.PASSWORD_SUSPENDED, "The password is suspended. The password MUST be resumed."); }*/ } else if (retryCounter == (byte) 0x02) { // The password is suspended logger.warn("The password is wrong."); generalAuthenticateEncryptedNonce(); } } } catch (ProtocolException e) { logger.error(e.getMessage(), e); throw e; } catch (Exception e) { logger.error(e.getMessage(), e); throw new ProtocolException(ECardConstants.Minor.IFD.UNKNOWN_ERROR, e.getMessage()); } } /** * Step 2: Encrypted nonce */ private void generalAuthenticateEncryptedNonce() throws Exception { CardCommandAPDU gaEncryptedNonce = new GeneralAuthenticate(); gaEncryptedNonce.setChaining(); // Derive key PI byte[] keyPI = kdf.derivePI(password); try { response = gaEncryptedNonce.transmit(dispatcher, slotHandle); s = cryptoSuite.decryptNonce(keyPI, response.getData()); // Continue with Step 3 generalAuthenticateMapNonce(); } catch (APDUException e) { logger.error(e.getMessage(), e); throw new ProtocolException(e.getResult()); } catch (GeneralSecurityException e) { logger.error(e.getMessage(), e); throw new ProtocolException(e.getMessage()); } } /** * Step 3: Mapping nonce */ private void generalAuthenticateMapNonce() throws Exception { byte[] pkMapPCD = null; PACEMapping mapping = cryptoSuite.getMapping(); if (mapping instanceof PACEGenericMapping) { PACEGenericMapping gm = (PACEGenericMapping) mapping; pkMapPCD = gm.getMappingKey().getEncodedPublicKey(); } else if (mapping instanceof PACEIntegratedMapping) { throw new UnsupportedOperationException("Not implemented yet."); } CardCommandAPDU gaMapNonce = new GeneralAuthenticate((byte) 0x81, pkMapPCD); gaMapNonce.setChaining(); try { response = gaMapNonce.transmit(dispatcher, slotHandle); } catch (APDUException e) { logger.error(e.getMessage(), e); throw new ProtocolException(e.getResult()); } if (mapping instanceof PACEGenericMapping) { PACEGenericMapping gm = (PACEGenericMapping) mapping; PACEKey keyMapPICC = new PACEKey(domainParameter); keyMapPICC.decodePublicKey(response.getData()); byte[] pkMapPICC = keyMapPICC.getEncodedPublicKey(); if (ByteUtils.compare(pkMapPICC, pkMapPCD)) { throw new GeneralSecurityException("PACE security violation: equal keys"); } domainParameter = gm.map(pkMapPICC, s); } else if (mapping instanceof PACEIntegratedMapping) { throw new UnsupportedOperationException("Not implemented yet."); } // Continue with Step 4 generalAuthenticateKeyAgreement(); } /** * Step 4: Key agreement * * @param mapPK_PICC */ private void generalAuthenticateKeyAgreement() throws Exception { keyPCD = new PACEKey(domainParameter); keyPCD.generateKeyPair(); byte[] keyPKPCD = keyPCD.getEncodedPublicKey(); CardCommandAPDU gaKeyAgreement = new GeneralAuthenticate((byte) 0x83, keyPKPCD); gaKeyAgreement.setChaining(); try { response = gaKeyAgreement.transmit(dispatcher, slotHandle); keyPICC = new PACEKey(domainParameter); byte[] keyPKPICC = keyPICC.decodePublicKey(response.getData()); if (!ByteUtils.compare(keyPKPCD, keyPKPICC)) { // Continue with Step 5 generalAuthenticateMutualAuthentication(); } else { throw new GeneralSecurityException("PACE security violation: equal keys"); } } catch (APDUException e) { logger.error(e.getMessage(), e); throw new ProtocolException(e.getResult()); } catch (GeneralSecurityException e) { logger.error(e.getMessage(), e); throw new ProtocolException(e.getMessage()); } } /** * Step 5: Mutual authentication */ private void generalAuthenticateMutualAuthentication() throws Exception { // Calculate shared key k byte[] k = cryptoSuite.generateSharedSecret(keyPCD.getEncodedPrivateKey(), keyPICC.getEncodedPublicKey()); // Derive key MAC keyMAC = kdf.deriveMAC(k); // Derive key ENC keyENC = kdf.deriveENC(k); // Calculate token T_PCD AuthenticationToken tokenPCD = new AuthenticationToken(psi); tokenPCD.generateToken(keyMAC, keyPICC.getEncodedPublicKey()); CardCommandAPDU gaMutualAuth = new GeneralAuthenticate((byte) 0x85, tokenPCD.toByteArray()); // Calculate token T_PICC AuthenticationToken tokenPICC = new AuthenticationToken(psi); tokenPICC.generateToken(keyMAC, keyPCD.getEncodedPublicKey()); try { response = gaMutualAuth.transmit(dispatcher, slotHandle); if (tokenPICC.verifyToken(response.getData(), specifiedCHAT)) { currentCAR = tokenPICC.getCurrentCAR(); previousCAR = tokenPCD.getPreviousCAR(); } else { throw new GeneralSecurityException("Cannot verify authentication token."); } } catch (APDUException e) { logger.error(e.getMessage(), e); int sw = e.getResponseAPDU().getSW(); if ((sw & (short) 0xFFF0) == (short) 0x63C0) { retryCounter = (byte) (sw & (short) 0x000F); if (retryCounter == (byte) 0x00) { // The password is blocked. logger.warn("The password is blocked. The password MUST be unblocked."); throw new ProtocolException( ECardConstants.Minor.IFD.PASSWORD_BLOCKED, "The password is blocked. The password MUST be unblocked."); } else if (retryCounter == (byte) 0x01) { // The password is suspended. logger.warn("The password is suspended. The password MUST be resumed."); throw new ProtocolException(ECardConstants.Minor.IFD.PASSWORD_SUSPENDED, "The password is suspended. The password MUST be resumed."); } else if (retryCounter == (byte) 0x02) { // The password is wrong. logger.warn("The password is wrong."); throw new ProtocolException( ECardConstants.Minor.IFD.PASSWORD_ERROR, "The password is wrong."); } } else { throw new ProtocolException( ECardConstants.Minor.IFD.AUTHENTICATION_FAILED, "Authentication failed."); } } catch (Exception e) { logger.error(e.getMessage(), e); throw new ProtocolException(ECardConstants.Minor.IFD.UNKNOWN_ERROR, e.getMessage()); } } /** * Returns the current Certification Authority Reference (CAR). * * @return Current Certification Authority Reference (CAR) */ public byte[] getCurrentCAR() { return currentCAR; } /** * Returns the previous Certification Authority Reference (CAR). * * @return Previous Certification Authority Reference (CAR) */ public byte[] getPreviousCAR() { return previousCAR; } /** * Returns the key for message authentication. * * @return KeyMAC */ public byte[] getKeyMAC() { return keyMAC; } /** * Returns the key for message encryption. * * @return KeyENC */ public byte[] getKeyENC() { return keyENC; } /** * Returns the card identifier ID_PICC = Comp(PK_PICC). * * @return ID_PICC */ public byte[] getIDPICC() { return keyPICC.getEncodedCompressedPublicKey(); } /** * Returns the retry counter. * * @return Retry counter */ public byte getRetryCounter() { return retryCounter; } }