package de.persosim.simulator.protocols.pace;
import static de.persosim.simulator.protocols.Tr03110Utils.buildAuthenticationTokenInput;
import static org.globaltester.logging.BasicLogger.DEBUG;
import static org.globaltester.logging.BasicLogger.ERROR;
import static org.globaltester.logging.BasicLogger.TRACE;
import static org.globaltester.logging.BasicLogger.log;
import static org.globaltester.logging.BasicLogger.logException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import javax.crypto.KeyAgreement;
import javax.crypto.spec.SecretKeySpec;
import org.globaltester.cryptoprovider.Crypto;
import de.persosim.simulator.apdu.ResponseApdu;
import de.persosim.simulator.cardobjects.AuthObjectIdentifier;
import de.persosim.simulator.cardobjects.CardObject;
import de.persosim.simulator.cardobjects.CardObjectIdentifier;
import de.persosim.simulator.cardobjects.CardObjectUtils;
import de.persosim.simulator.cardobjects.DomainParameterSetCardObject;
import de.persosim.simulator.cardobjects.DomainParameterSetIdentifier;
import de.persosim.simulator.cardobjects.Iso7816LifeCycleState;
import de.persosim.simulator.cardobjects.MasterFile;
import de.persosim.simulator.cardobjects.OidIdentifier;
import de.persosim.simulator.cardobjects.PasswordAuthObject;
import de.persosim.simulator.cardobjects.PasswordAuthObjectWithRetryCounter;
import de.persosim.simulator.cardobjects.TrustPointCardObject;
import de.persosim.simulator.cardobjects.TrustPointIdentifier;
import de.persosim.simulator.crypto.CryptoSupport;
import de.persosim.simulator.crypto.DomainParameterSet;
import de.persosim.simulator.crypto.KeyDerivationFunction;
import de.persosim.simulator.crypto.certificates.PublicKeyReference;
import de.persosim.simulator.exception.AccessDeniedException;
import de.persosim.simulator.exception.CertificateNotParseableException;
import de.persosim.simulator.exception.CryptoException;
import de.persosim.simulator.exception.ProcessingException;
import de.persosim.simulator.platform.CardStateAccessor;
import de.persosim.simulator.platform.Iso7816;
import de.persosim.simulator.platform.Iso7816Lib;
import de.persosim.simulator.protocols.AbstractProtocolStateMachine;
import de.persosim.simulator.protocols.Oid;
import de.persosim.simulator.protocols.ProtocolUpdate;
import de.persosim.simulator.protocols.ResponseData;
import de.persosim.simulator.protocols.SecInfoPublicity;
import de.persosim.simulator.protocols.Tr03110Utils;
import de.persosim.simulator.protocols.ta.Authorization;
import de.persosim.simulator.protocols.ta.CertificateHolderAuthorizationTemplate;
import de.persosim.simulator.protocols.ta.TerminalType;
import de.persosim.simulator.secstatus.AuthorizationStore;
import de.persosim.simulator.secstatus.ConfinedAuthorizationMechanism;
import de.persosim.simulator.secstatus.PaceMechanism;
import de.persosim.simulator.secstatus.PaceUsedPasswordMechanism;
import de.persosim.simulator.secstatus.SecMechanism;
import de.persosim.simulator.secstatus.SecStatus.SecContext;
import de.persosim.simulator.secstatus.SecStatusMechanismUpdatePropagation;
import de.persosim.simulator.secstatus.SessionContextIdMechanism;
import de.persosim.simulator.securemessaging.SmDataProviderTr03110;
import de.persosim.simulator.tlv.ConstructedTlvDataObject;
import de.persosim.simulator.tlv.PrimitiveTlvDataObject;
import de.persosim.simulator.tlv.TlvConstants;
import de.persosim.simulator.tlv.TlvDataObject;
import de.persosim.simulator.tlv.TlvDataObjectContainer;
import de.persosim.simulator.tlv.TlvPath;
import de.persosim.simulator.tlv.TlvTag;
import de.persosim.simulator.tlv.TlvValue;
import de.persosim.simulator.utils.HexString;
/**
* @author slutters
*
*/
public abstract class AbstractPaceProtocol extends AbstractProtocolStateMachine implements Pace, TlvConstants {
/*--------------------------------------------------------------------------------*/
public static final short P1P2_C1A4_SET_AT = (short) 0xC1A4;
public static final short P1P2_81B6_SET_DST = (short) 0x81B6;
public static final short P1P2_00BE_VERIFY_CERTIFICATE = (short) 0x00BE;
public static final short P1P2_0000_NO_FURTHER_INFORMATION = (short) 0x0000;
// values 0x00 - 0x3F are reserved for common COMMAND_X variables
public static final byte COMMAND_GET_NONCE = (byte) 0x40;
public static final byte COMMAND_MAP_NONCE = (byte) 0x41;
public static final byte COMMAND_PERFORM_KEY_AGREEMENT = (byte) 0x42;
public static final byte APDU_SET_AT = 0;
public static final byte APDU_GET_NONCE = 1;
public static final byte APDU_MAP_NONCE = 2;
public static final byte APDU_PERFORM_KEY_AGREEMENT = 3;
public static final byte APDU_MUTUAL_AUTHENTICATE = 4;
/*--------------------------------------------------------------------------------*/
protected PaceOid paceOid;
protected PasswordAuthObject pacePassword;
protected int paceDomainParameterId;
protected DomainParameterSet paceDomainParametersUnmapped;
protected DomainParameterSet paceDomainParametersMapped;
protected SecureRandom secureRandom;
protected SecretKeySpec secretKeySpecNonce;
protected SecretKeySpec secretKeySpecMAC;
protected SecretKeySpec secretKeySpecENC;
protected CryptoSupport cryptoSupport;
protected byte[] piccsPlainNonceS;
protected KeyPair ephemeralKeyPairPicc;
protected PublicKey ephemeralPublicKeyPcd;
protected MappingResult mappingResult;
protected AuthorizationStore authorizationStore;
TrustPointCardObject trustPoint;
CertificateHolderAuthorizationTemplate usedChat;
Oid terminalTypeOid;
/*--------------------------------------------------------------------------------*/
public AbstractPaceProtocol() {
super("PACE");
secureRandom = new SecureRandom();
}
/**
* @param bytes the OID given in the SET AT command
* @return the PaceOid helper class to be used by this protocol
*/
protected PaceOid getOid(byte [] bytes){
return new PaceOid(bytes);
}
/**
* This method processes the command APDU SET_AT.
*/
public void processCommandSetAT() {
try {
//get commandDataContainer
TlvDataObjectContainer commandData = processingData.getCommandApdu().getCommandDataObjectContainer();
/*
* Extract security parameters
*/
/* PACE OID */
/* Check for the PACE OID for itself */
/* tlvObject will never be null if APDU passed check against APDU specification */
TlvDataObject tlvObject = commandData.getTlvDataObject(TAG_80);
try {
paceOid = getOid(tlvObject.getValueField());
} catch (RuntimeException e) {
ResponseApdu resp = new ResponseApdu(Iso7816.SW_6A80_WRONG_DATA);
this.processingData.updateResponseAPDU(this, e.getMessage(), resp);
logException(this, e);
/* there is nothing more to be done here */
return;
}
/* PACE password */
/* Check for the PACE password itself */
tlvObject = commandData.getTlvDataObject(TAG_83);
CardObject pwdCandidate = CardObjectUtils.getSpecificChild(cardState.getMasterFile(), new AuthObjectIdentifier(tlvObject.getValueField()));
if (pwdCandidate instanceof PasswordAuthObject){
pacePassword = (PasswordAuthObject) pwdCandidate;
log(this, "selected password is: " + getPasswordName(), DEBUG);
} else {
ResponseApdu resp = new ResponseApdu(Iso7816.SW_6A88_REFERENCE_DATA_NOT_FOUND);
this.processingData.updateResponseAPDU(this, "no fitting authentication object found", resp);
/* there is nothing more to be done here */
return;
}
PaceUsedPasswordMechanism paceUsedPasswordMechanism = new PaceUsedPasswordMechanism(pacePassword);
processingData.addUpdatePropagation(this, "PACE started with "+pacePassword.getPasswordName(), new SecStatusMechanismUpdatePropagation(SecContext.APPLICATION, paceUsedPasswordMechanism));
/* PACE domain parameters */
/*
* {@link #tlvObject} may only be null if the combination of OID and domain parameters
* set is not ambiguous. This is the case iff among all sets of domain
* parameters registered for the selected OID exactly one matches the
* key agreement implicitly indicated by the OID.
*/
tlvObject = commandData.getTlvDataObject(TAG_84);
DomainParameterSetIdentifier domainParameterSetIdentifier;
if(tlvObject == null) {
domainParameterSetIdentifier = new DomainParameterSetIdentifier();
} else{
domainParameterSetIdentifier = new DomainParameterSetIdentifier(tlvObject.getValueField());
}
CardObject cardObject;
try {
cardObject = CardObjectUtils.getSpecificChild(cardState.getMasterFile(), domainParameterSetIdentifier, new OidIdentifier(paceOid));
} catch (IllegalArgumentException e) {
ResponseApdu resp = new ResponseApdu(Iso7816.SW_6A88_REFERENCE_DATA_NOT_FOUND);
this.processingData.updateResponseAPDU(this, e.getMessage(), resp);
/* there is nothing more to be done here */
return;
}
if((cardObject instanceof DomainParameterSetCardObject)) {
DomainParameterSetCardObject domainParameterObject = (DomainParameterSetCardObject) cardObject;
paceDomainParametersUnmapped = domainParameterObject.getDomainParameterSet();
paceDomainParameterId = domainParameterObject.getPrimaryIdentifier().getInteger();
} else{
ResponseApdu resp = new ResponseApdu(Iso7816.SW_6A88_REFERENCE_DATA_NOT_FOUND);
this.processingData.updateResponseAPDU(this, "invalid key reference", resp);
/* there is nothing more to be done here */
return;
}
/* CHAT */
/*
* {@link #tlvObject} may be null if no terminal authentication follows PACE
*/
tlvObject = commandData.getTlvDataObject(TAG_7F4C);
if (tlvObject != null){
try {
usedChat = new CertificateHolderAuthorizationTemplate((ConstructedTlvDataObject) tlvObject);
HashMap<Oid, Authorization> authorizations = getAuthorizationsFromCommandData(commandData);
authorizationStore = new AuthorizationStore(authorizations);
terminalTypeOid = usedChat.getObjectIdentifier();
TerminalType terminalType = usedChat.getTerminalType();
trustPoint = (TrustPointCardObject) CardObjectUtils.getSpecificChild(cardState.getMasterFile(), new TrustPointIdentifier(terminalType));
if (!checkPasswordAndAccessRights(usedChat, pacePassword)){
ResponseApdu resp = new ResponseApdu(
Iso7816.SW_6A80_WRONG_DATA);
this.processingData.updateResponseAPDU(this, "The given terminal type and password does not match the access rights", resp);
/* there is nothing more to be done here */
return;
}
} catch (Exception e) {
//FIXME check PokemonException
ResponseApdu resp = new ResponseApdu(
Iso7816.SW_6A88_REFERENCE_DATA_NOT_FOUND);
this.processingData.updateResponseAPDU(this, e.getMessage(),
resp);
logException(this, e);
/* there is nothing more to be done here */
return;
}
}
this.cryptoSupport = paceOid.getCryptoSupport();
String logString = "new OID is " + paceOid + ", new " + pacePassword;
log(this, logString, DEBUG);
/*
* Create and set crypto parameters
*/
KeyDerivationFunction kdf = new KeyDerivationFunction(paceOid.getSymmetricCipherKeyLengthInBytes());
byte[] commonSecret = pacePassword.getPassword();
log(this, "common secret is: " + HexString.encode(commonSecret), TRACE);
byte[] keyMaterialForEncryptionOfNonce = kdf.derivePI(commonSecret);
log(this, "computed raw key material of byte length " + keyMaterialForEncryptionOfNonce.length + " is: " + HexString.encode(keyMaterialForEncryptionOfNonce), TRACE);
this.secretKeySpecNonce = this.cryptoSupport.generateSecretKeySpecCipher(keyMaterialForEncryptionOfNonce);
log(this, "computed " + paceOid.getSymmetricCipherAlgorithmName() + " key material: " + HexString.encode(keyMaterialForEncryptionOfNonce), DEBUG);
// If PIN is used, check for retry counter.
ResponseData isPasswordUsable = isPasswordUsable(pacePassword, cardState);
if (isPasswordUsable != null){
//create and propagate response APDU
ResponseApdu resp = new ResponseApdu(isPasswordUsable.getStatusWord());
this.processingData.updateResponseAPDU(this, isPasswordUsable.getResponse(), resp);
return;
}
//create and propagate response APDU
ResponseApdu resp = new ResponseApdu(Iso7816.SW_9000_NO_ERROR);
this.processingData.updateResponseAPDU(this, "Command SetAt successfully processed", resp);
} catch (ProcessingException e) {
ResponseApdu resp = new ResponseApdu(e.getStatusWord());
processingData.updateResponseAPDU(this, e.getMessage(), resp);
return;
}
}
public HashMap<Oid, Authorization> getAuthorizationsFromCommandData(TlvDataObjectContainer commandData) {
HashMap<Oid, Authorization> authorizations = new HashMap<Oid, Authorization>();
TlvDataObject tlvObject = commandData.getTlvDataObject(TAG_7F4C);
CertificateHolderAuthorizationTemplate chatFromCommandData = null;
if (tlvObject != null){
try {
chatFromCommandData = new CertificateHolderAuthorizationTemplate((ConstructedTlvDataObject) tlvObject);
authorizations.put(chatFromCommandData.getObjectIdentifier(), chatFromCommandData.getRelativeAuthorization());
} catch (CertificateNotParseableException e) {
ResponseApdu resp = new ResponseApdu(
Iso7816.SW_6A88_REFERENCE_DATA_NOT_FOUND);
this.processingData.updateResponseAPDU(this, e.getMessage(),
resp);
}
}
return authorizations;
}
/**
* Get the status word for the use of the given password.
*
* @param pacePassword
* the password to check
* @param cardState
* the card state accessor to be used
* @return the status word encoding an error or null if the password is
* usable
*/
public static ResponseData isPasswordUsable(PasswordAuthObject pacePassword, CardStateAccessor cardState){
if (pacePassword instanceof PasswordAuthObjectWithRetryCounter) {
PasswordAuthObjectWithRetryCounter pacePasswordWithRetryCounter = (PasswordAuthObjectWithRetryCounter) pacePassword;
int retryCounter = pacePasswordWithRetryCounter.getRetryCounterCurrentValue();
short retryCounterDefault = (short) pacePasswordWithRetryCounter.getRetryCounterDefaultValue();
short sw;
String note;
if (pacePassword.getLifeCycleState().equals(Iso7816LifeCycleState.OPERATIONAL_ACTIVATED)) {
if (retryCounter != retryCounterDefault) {
if (retryCounter == 1) {
sw = Iso7816.SW_63C1_COUNTER_IS_1;
if(isPinTemporarilyResumed(cardState)) {
note = "PIN is temporarily resumed due to preceding CAN";
} else{
note = "PIN is suspended, use CAN first for temporary resume or unblock PIN";
}
/* there is nothing more to be done here */
return new ResponseData(sw, note);
} else {
// If(retryCounterPin > 1) - 0 is not possible as this
// would have caused the PIN to be deactivated before.
// In this case this code would have never been reached
// due to the previous check for the PIN being activated
sw = (short) 0x63C0;
sw |= ((short) (retryCounter & (short) 0x000F));
note = "PACE with PIN has previously failed - current retry counter for PIN is " + retryCounter;
return new ResponseData(sw, note);
}
}
} else {
/* there is nothing more to be done here */
return new ResponseData(Iso7816.SW_6283_SELECTED_FILE_DEACTIVATED, "PIN is deactivated");
}
}
return null;
}
/**
* This method checks for allowed combinations of chat and password as
* defined in TR-03110 v2.10 Part 2 2.4.
*
* @param chat
* Certificate holder authorization template to check
* @param password
* to check
* @return true, iff the combination used is allowed
*/
public static boolean checkPasswordAndAccessRights(
CertificateHolderAuthorizationTemplate chat,
PasswordAuthObject password) {
switch (chat.getTerminalType()) {
case AT:
if (password.getPasswordIdentifier() == ID_PIN
|| (password.getPasswordIdentifier() == ID_CAN && chat
.getRelativeAuthorization().getAuthorization()
.getBit(Tr03110Utils.ACCESS_RIGHTS_AT_CAN_ALLOWED_BIT))) {
return true;
}
break;
case IS:
if (password.getPasswordIdentifier() == ID_CAN
|| password.getPasswordIdentifier() == ID_MRZ) {
return true;
}
break;
case ST:
if (password.getPasswordIdentifier() == ID_CAN
|| password.getPasswordIdentifier() == ID_PUK
|| password.getPasswordIdentifier() == ID_PIN) {
return true;
}
break;
}
return false;
}
/**
* This method processes the command APDU GET_NONCE
*/
public void processCommandGetNonce() {
byte[] encryptedNonce;
int blockSizeInBytes, keySizeInBytes, nonceSizeInBytes, multiplicationFactor;
PrimitiveTlvDataObject primitive80;
ConstructedTlvDataObject constructed7C;
keySizeInBytes = paceOid.getSymmetricCipherKeyLengthInBytes();
blockSizeInBytes = this.cryptoSupport.getBlockSize();
multiplicationFactor = (int) Math.ceil(keySizeInBytes/(double) blockSizeInBytes);
nonceSizeInBytes = multiplicationFactor * blockSizeInBytes;
log(this, "key length k in Bytes is " + keySizeInBytes + ", block size in Bytes is " + blockSizeInBytes + " --> nonce s must be of smallest length l in Bytes, l being a multiple of the block size, such that l<=k", TRACE);
this.piccsPlainNonceS = new byte[nonceSizeInBytes];
this.secureRandom.nextBytes(this.piccsPlainNonceS);
log(this, "new (plain) nonce s of byte length " + this.piccsPlainNonceS.length + " is " + HexString.encode(this.piccsPlainNonceS), TRACE);
encryptedNonce = this.cryptoSupport.encryptWithIvZero(this.piccsPlainNonceS, this.secretKeySpecNonce);
log(this, "(encryted) nonce z = E_KPi(s) is " + HexString.encode(encryptedNonce), TRACE);
primitive80 = new PrimitiveTlvDataObject(TAG_80, encryptedNonce);
log(this, "primitive tag 80 is: " + primitive80, TRACE);
constructed7C = new ConstructedTlvDataObject(TAG_7C);
constructed7C.addTlvDataObject(primitive80);
//create and propagate response APDU
TlvValue responseData = new TlvDataObjectContainer(constructed7C);
ResponseApdu resp = new ResponseApdu(responseData, Iso7816.SW_9000_NO_ERROR);
this.processingData.updateResponseAPDU(this, "Command GetNonce successfully processed", resp);
}
/**
* This method processes the command APDU MAP_NONCE
*/
public void processCommandMapNonce() {
// create and initialize mapping
Mapping mapping = paceOid.getMapping();
String keyAgreementName = paceDomainParametersUnmapped.getKeyAgreementAlgorithm();
/*
* Extract mapping data
*/
TlvDataObjectContainer commandData = processingData.getCommandApdu().getCommandDataObjectContainer();
TlvDataObject tlvObject = commandData.getTlvDataObject(new TlvPath(TAG_7C, TAG_81));
/*
* The received mapping data may contain the following
* objects depending on the selected mapping:
* IM: the PCD's nonce t
* GM: the PCD's public DH/ECDH key component to the base of g/G as defined by the used domain parameters
*
* Due to this differentiation validity checks of the received mapping data are performed directly by the mapping itself.
*/
byte[] mappingDataFromPcd = tlvObject.getValueField();
log(this, "mapping data received from PCD is expected to contain " + mapping.getMeaningOfMappingData(), DEBUG);
log(this, "unchecked mapping data content of " + mappingDataFromPcd.length + " bytes length is: " + HexString.encode(mappingDataFromPcd), DEBUG);
log(this, "nonce s generated by PICC during processing of GetNonce command is " + HexString.encode(piccsPlainNonceS), TRACE);
byte[] mappingResponse;
try {
log(this, "about to perform " + mapping.getMappingName(), DEBUG);
mappingResult = mapping.performMapping(paceDomainParametersUnmapped, piccsPlainNonceS, mappingDataFromPcd);
ephemeralKeyPairPicc = mappingResult.getKeyPairPiccMapped();
paceDomainParametersMapped = mappingResult.getMappedDomainParameters();
mappingResponse = mappingResult.getMappingResponse();
} catch (InvalidAlgorithmParameterException | InvalidKeySpecException e) {
ResponseApdu resp = new ResponseApdu(Iso7816.SW_6A80_WRONG_DATA);
processingData.updateResponseAPDU(this, "Mapping failed due to " + e.getMessage(), resp);
logException(this, e);
/* there is nothing more to be done here */
return;
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
ResponseApdu resp = new ResponseApdu(Iso7816.SW_6FFF_IMPLEMENTATION_ERROR);
processingData.updateResponseAPDU(this, e.getMessage(), resp);
logException(this, e);
/* there is nothing more to be done here */
return;
}
log(this, "PICC's ephemeral public mapped " + keyAgreementName + " key is " + new TlvDataObjectContainer(ephemeralKeyPairPicc.getPublic().getEncoded()), TRACE);
log(this, "PICC's ephemeral private mapped " + keyAgreementName + " key is " + new TlvDataObjectContainer(ephemeralKeyPairPicc.getPrivate().getEncoded()), TRACE);
// Build response data
TlvValue responseData = buildResponseDataForMapNonce(mappingResponse);
ResponseApdu resp = new ResponseApdu(responseData, Iso7816.SW_9000_NO_ERROR);
this.processingData.updateResponseAPDU(this, "Command MapNonce successfully processed", resp);
}
/**
* Build the response data structure to GAP:MapNonce
*
* @param mappingResponse
* @return the response data
*/
public TlvValue buildResponseDataForMapNonce(byte [] mappingResponse){
// Build response data
PrimitiveTlvDataObject primitive82 = new PrimitiveTlvDataObject(TAG_82, mappingResponse);
ConstructedTlvDataObject constructed7C = new ConstructedTlvDataObject(TAG_7C);
constructed7C.addTlvDataObject(primitive82);
// Create and propagate response APDU
TlvValue responseData = new TlvDataObjectContainer(constructed7C);
return responseData;
}
/**
* This method processes the command APDU PERFORM_KEY_AGREEMENT.
*/
public void processCommandPerformKeyAgreement() {
byte[] ephemeralPublicKeyComponentPicc;
//get commandDataContainer
TlvDataObjectContainer commandData = processingData.getCommandApdu().getCommandDataObjectContainer();
TlvDataObject tlvObject = commandData.getTlvDataObject(new TlvPath(new TlvTag((byte) 0x7C), new TlvTag((byte) 0x83)));
byte[] rawKeyPlain = tlvObject.getValueField();
log(this, "PCD's public raw key of " + rawKeyPlain.length + " bytes length is: " + HexString.encode(rawKeyPlain), TRACE);
try {
ephemeralPublicKeyPcd = paceDomainParametersMapped.reconstructPublicKey(rawKeyPlain);
ephemeralPublicKeyComponentPicc = paceDomainParametersMapped.encodePublicKey(ephemeralKeyPairPicc.getPublic());
log(this, "PCD's ephemeral public mapped " + paceDomainParametersMapped.getKeyAgreementAlgorithm() + " key is " + new TlvDataObjectContainer(ephemeralPublicKeyPcd.getEncoded()), TRACE);
} catch (IllegalArgumentException e) {
logException(this, e, ERROR);
ResponseApdu resp = new ResponseApdu(Iso7816.SW_6A80_WRONG_DATA);
processingData.updateResponseAPDU(this, e.getMessage(), resp);
return;
} catch (Exception e) {
logException(this, e, ERROR);
ResponseApdu resp = new ResponseApdu(Iso7816.SW_6FFF_IMPLEMENTATION_ERROR);
processingData.updateResponseAPDU(this, e.getMessage(), resp);
return;
}
log(this, "bare response data of byte length " + ephemeralPublicKeyComponentPicc.length + " is " + HexString.encode(ephemeralPublicKeyComponentPicc), DEBUG);
/* create and propagate response APDU */
TlvValue responseData = buildResponseDataForKeyAgreement(paceDomainParametersMapped, ephemeralPublicKeyComponentPicc);
ResponseApdu resp = new ResponseApdu(responseData, Iso7816.SW_9000_NO_ERROR);
this.processingData.updateResponseAPDU(this, "Command PerformKeyAgreement successfully processed", resp);
}
/**
* Build the response data structure to GAP:PerformKeyAgreement
*
* @param paceDomainParametersMapped
* @param ephemeralPublicKeyComponentPicc
* @return the response data
*/
public TlvValue buildResponseDataForKeyAgreement(DomainParameterSet paceDomainParametersMapped, byte [] ephemeralPublicKeyComponentPicc){
PrimitiveTlvDataObject primitive84 = new PrimitiveTlvDataObject(TAG_84, ephemeralPublicKeyComponentPicc);
ConstructedTlvDataObject constructed7C = new ConstructedTlvDataObject(TAG_7C);
constructed7C.addTlvDataObject(primitive84);
TlvValue responseData = new TlvDataObjectContainer(constructed7C);
return responseData;
}
/**
* This method processes the command APDU MUTUAL_AUTHENTICATE.
*/
public void processCommandMutualAuthenticate() {
try {
TlvDataObject tlvObject;
byte[] pcdTokenReceivedFromPCD, piccToken, pcdToken;
TlvPath path;
path = new TlvPath(new TlvTag[]{TAG_7C, TAG_85});
/* get commandDataContainer */
TlvDataObjectContainer commandData = processingData.getCommandApdu().getCommandDataObjectContainer();
tlvObject = commandData.getTlvDataObject(path);
pcdTokenReceivedFromPCD = tlvObject.getValueField();
/* construct authentication token input based on PACE OID and ephemeral keys */
TlvDataObjectContainer piccTokenInput = buildAuthenticationTokenInput(ephemeralPublicKeyPcd, paceDomainParametersMapped, paceOid);
TlvDataObjectContainer pcdTokenInput = buildAuthenticationTokenInput(ephemeralKeyPairPicc.getPublic(), paceDomainParametersMapped, paceOid);
log(this, "picc token raw data " + piccTokenInput, DEBUG);
log(this, "pcd token raw data " + pcdTokenInput, DEBUG);
try {
KeyAgreement keyAgreement = KeyAgreement.getInstance(paceOid.getKeyAgreementName(), Crypto.getCryptoProvider());
keyAgreement.init(this.ephemeralKeyPairPicc.getPrivate());
keyAgreement.doPhase(this.ephemeralPublicKeyPcd, true);
byte[] sharedSecret = keyAgreement.generateSecret();
log(this, "shared secret of byte length " + sharedSecret.length + " resulting from " + paceOid.getKeyAgreementName() + " key agreement is " + HexString.encode(sharedSecret), DEBUG);
KeyDerivationFunction kdf = new KeyDerivationFunction(paceOid.getSymmetricCipherKeyLengthInBytes());
byte[] keyMaterialMAC = kdf.deriveMAC(sharedSecret);
byte[] keyMaterialENC = kdf.deriveENC(sharedSecret);
this.secretKeySpecMAC = this.cryptoSupport.generateSecretKeySpecMac(keyMaterialMAC);
this.secretKeySpecENC = this.cryptoSupport.generateSecretKeySpecCipher(keyMaterialENC);
log(this, "final " + secretKeySpecENC.getAlgorithm() + " symmetric key material ENC is " + HexString.encode(secretKeySpecENC.getEncoded()), DEBUG);
log(this, "final " + secretKeySpecMAC.getAlgorithm() + " symmetric key material MAC is " + HexString.encode(secretKeySpecMAC.getEncoded()), DEBUG);
} catch (InvalidKeyException | IllegalStateException | NoSuchAlgorithmException e) {
ResponseApdu resp = new ResponseApdu(Iso7816.SW_6A80_WRONG_DATA);
processingData.updateResponseAPDU(this, "Invalid symmetric key", resp);
logException(this, e);
//XXX shouldn't we return here? (remember to mark the protocol as finished)
}
/* get first 8 bytes of mac */
piccToken = Arrays.copyOf(this.cryptoSupport.macAuthenticationToken(piccTokenInput.toByteArray(), this.secretKeySpecMAC), 8);
log(this, "picc token data is: " + HexString.encode(piccToken), DEBUG);
pcdToken = Arrays.copyOf(this.cryptoSupport.macAuthenticationToken(pcdTokenInput.toByteArray(), this.secretKeySpecMAC), 8);
log(this, "pcd token data is: " + HexString.encode(pcdToken), DEBUG);
log(this, "expected pcd token data is: " + HexString.encode(pcdToken), DEBUG);
log(this, "received pcd token data is: " + HexString.encode(pcdTokenReceivedFromPCD), DEBUG);
boolean paceSuccessful;
short sw;
String note;
if(Arrays.equals(pcdToken, pcdTokenReceivedFromPCD)) {
log(this, "Token received from PCD matches expected one", DEBUG);
if(pacePassword instanceof PasswordAuthObjectWithRetryCounter) {
ResponseData pinResponse = getMutualAuthenticatePinManagementResponsePaceSuccessful(pacePassword, cardState);
sw = pinResponse.getStatusWord();
note = pinResponse.getResponse();
paceSuccessful = !Iso7816Lib.isReportingError(sw);
} else{
sw = Iso7816.SW_9000_NO_ERROR;
note = "MutualAuthenticate processed successfully";
paceSuccessful = true;
}
} else{
//PACE failed
log(this, "Token received from PCD does NOT match expected one", DEBUG);
paceSuccessful = false;
if(pacePassword.getPasswordIdentifier() == Pace.ID_PIN) {
ResponseData pinResponse = getMutualAuthenticatePinManagementResponsePaceFailed((PasswordAuthObjectWithRetryCounter) pacePassword);
sw = pinResponse.getStatusWord();
note = pinResponse.getResponse();
} else{
sw = Iso7816.SW_6A80_WRONG_DATA;
note = "authentication token received from PCD does NOT match expected one";
}
}
ResponseApdu responseApdu;
if(paceSuccessful) {
ConstructedTlvDataObject responseContent = buildMutualAuthenticateResponse(piccToken);
if (setSmDataProvider()){
PaceMechanism paceMechanism = new PaceMechanism(pacePassword, paceDomainParametersMapped.comp(ephemeralKeyPairPicc.getPublic()), terminalTypeOid);
processingData.addUpdatePropagation(this, "Security status updated with PACE mechanism", new SecStatusMechanismUpdatePropagation(SecContext.APPLICATION, paceMechanism));
ConfinedAuthorizationMechanism newAuthMechanism;
if(authorizationStore == null) {
authorizationStore = new AuthorizationStore();
}
newAuthMechanism = new ConfinedAuthorizationMechanism(authorizationStore);
processingData.addUpdatePropagation(this, "Security status updated with authorization mechanism", new SecStatusMechanismUpdatePropagation(SecContext.APPLICATION, newAuthMechanism));
responseApdu = new ResponseApdu(new TlvDataObjectContainer(responseContent), sw);
//store the new session context id (0 for default session context)
SessionContextIdMechanism scim = new SessionContextIdMechanism(0);
processingData.addUpdatePropagation(this, "Security status updated with SessionContextIdMechanism", new SecStatusMechanismUpdatePropagation(SecContext.APPLICATION, scim));
} else {
return;
}
} else {
responseApdu = new ResponseApdu(sw);
}
this.processingData.updateResponseAPDU(this, note, responseApdu);
/*
* Request removal of this instance from the stack.
* Protocol either successfully completed or failed.
* In either case protocol is completed.
*/
processingData.addUpdatePropagation(this, "Command MutualAuthenticate successfully processed - Protocol PACE completed", new ProtocolUpdate(true));
} catch (ProcessingException e ) {
ResponseApdu resp = new ResponseApdu(e.getStatusWord());
processingData.updateResponseAPDU(this, e.getMessage(), resp);
return;
}
}
protected void addCars(ConstructedTlvDataObject constructed7c){
//add CARs to response data if available
if (trustPoint != null) {
if (trustPoint.getCurrentCertificate() != null
&& trustPoint.getCurrentCertificate()
.getCertificateHolderReference() instanceof PublicKeyReference) {
constructed7c
.addTlvDataObject(new PrimitiveTlvDataObject(
TAG_87, trustPoint.getCurrentCertificate()
.getCertificateHolderReference()
.getBytes()));
if (trustPoint.getPreviousCertificate() != null
&& trustPoint.getPreviousCertificate()
.getCertificateHolderReference() instanceof PublicKeyReference) {
constructed7c
.addTlvDataObject(new PrimitiveTlvDataObject(
TAG_88,
trustPoint
.getPreviousCertificate()
.getCertificateHolderReference()
.getBytes()));
}
}
}
}
protected boolean setSmDataProvider(){
try {
//create and propagate new secure messaging data provider
SmDataProviderTr03110 smDataProvider = new SmDataProviderTr03110(this.secretKeySpecENC, this.secretKeySpecMAC);
processingData.addUpdatePropagation(this, "init SM after successful PACE", smDataProvider);
return true;
} catch (CryptoException e) {
logException(this, e);
ResponseApdu failureResponse = new ResponseApdu(Iso7816.SW_6FFF_IMPLEMENTATION_ERROR);
processingData.updateResponseAPDU(this, "Unable to initialize new secure messaging", failureResponse);
return false;
}
}
protected ConstructedTlvDataObject buildMutualAuthenticateResponse(byte[] piccToken){
PrimitiveTlvDataObject primitive86;
ConstructedTlvDataObject constructed7C;
primitive86 = new PrimitiveTlvDataObject(TAG_86, piccToken);
constructed7C = new ConstructedTlvDataObject(TAG_7C);
constructed7C.addTlvDataObject(primitive86);
addCars(constructed7C);
return constructed7C;
}
/**
* This method returns data required to send a response APDU for Mutual Authenticate if PACE was performed using PIN as password and failed.
* @return data required to send a response APDU for Mutual Authenticate
*/
public static ResponseData getMutualAuthenticatePinManagementResponsePaceFailed(PasswordAuthObjectWithRetryCounter pacePasswordPin) {
int pinRetryCounter = pacePasswordPin.getRetryCounterCurrentValue();
log(AbstractPaceProtocol.class, "PACE with PIN has failed - PIN retry counter will be decremented, current value is: " + pinRetryCounter, DEBUG);
pacePasswordPin.decrementRetryCounter();
pinRetryCounter = pacePasswordPin.getRetryCounterCurrentValue();
log(AbstractPaceProtocol.class, "PACE with PIN has failed - PIN retry counter has been decremented, current value is: " + pinRetryCounter, DEBUG);
short sw = (short) 0x63C0;
sw |= ((short) (pinRetryCounter & (short) 0x000F));
String note = "PACE with PIN has failed - PIN retry counter has been decremented, current value is: " + pinRetryCounter;
return new ResponseData(sw, note);
}
/**
* This method returns data required to send a response APDU for Mutual Authenticate if PACE was performed using PIN as password and succeeded.
* @responseData the response data to be sent with the response APDU
* @return data required to send a response APDU for Mutual Authenticate
*/
public static ResponseData getMutualAuthenticatePinManagementResponsePaceSuccessful(PasswordAuthObject password, CardStateAccessor cardState) {
short sw;
String note;
if(password.getLifeCycleState() == Iso7816LifeCycleState.OPERATIONAL_ACTIVATED) {
PasswordAuthObjectWithRetryCounter pacePasswordPin = (PasswordAuthObjectWithRetryCounter) password;
int pinRetryCounter = pacePasswordPin.getRetryCounterCurrentValue();
int pinRetryCounterDefault = pacePasswordPin.getRetryCounterDefaultValue();
if(pinRetryCounter != pinRetryCounterDefault) {
if(pinRetryCounter == 1) {
if (isPinTemporarilyResumed(cardState)) {
// everything ok, password is PIN, PIN activated, retry counter is not default value, retry counter == 1, PIN is suspended, PIN is temporarily resumed
try {
pacePasswordPin.resetRetryCounterToDefault();
} catch (AccessDeniedException e) {
throw new IllegalStateException(e);
}
sw = Iso7816.SW_9000_NO_ERROR;
note = "MutualAuthenticate processed successfully with password PIN after CAN - PIN retry counter has been reset from: " + pinRetryCounter + " to: " + pacePasswordPin.getRetryCounterCurrentValue();
} else{
// everything ok, password is PIN, PIN activated, retry counter is not default value, retry counter == 1, PIN is suspended, PIN is not temporarily resumed
sw = Iso7816.SW_6985_CONDITIONS_OF_USE_NOT_SATISFIED;
note = "MutualAuthenticate processed successfully but PIN is suspended";
}
} else if (pinRetryCounter == 0){
// everything ok, password is PIN, PIN activated, retry counter is not default value, retry counter == 0, PIN is blocked
sw = Iso7816.SW_6983_FILE_INVALID;
note = "MutualAuthenticate processed successfully but PIN is blocked";
} else{
// everything ok, password is PIN, PIN activated, retry counter is not default value, retry counter > 1
try {
pacePasswordPin.resetRetryCounterToDefault();
} catch (AccessDeniedException e) {
throw new IllegalStateException(e);
}
sw = Iso7816.SW_9000_NO_ERROR;
note = "MutualAuthenticate processed successfully with password PIN - PIN retry counter has been reset from: " + pinRetryCounter + " to: " + pacePasswordPin.getRetryCounterCurrentValue();
}
} else{
// everything ok, password is PIN, PIN activated, retry counter is default value
sw = Iso7816.SW_9000_NO_ERROR;
note = "MutualAuthenticate processed successfully";
}
} else{
// everything ok, password is PIN, PIN deactivated
sw = Iso7816.SW_6984_REFERENCE_DATA_NOT_USABLE;
note = "MutualAuthenticate processed successfully but PIN is deactivated";
}
return new ResponseData(sw, note);
}
/**
* This method returns the name of the password associated with the currently used password identifier.
* @return the name of the used password
*/
public String getPasswordName() {
return getPasswordName(pacePassword.getPasswordIdentifier());
}
/**
* This method returns the name of the password associated with the provided password identifier.
* @pwdIdentifier the password identifier
* @return the name of the used password
*/
public static String getPasswordName(int pwdIdentifier) {
switch (pwdIdentifier) {
case Pace.ID_CAN:
return "CAN";
case Pace.ID_MRZ:
return "MRZ";
case Pace.ID_PIN:
return "PIN";
case Pace.ID_PUK:
return "PUK";
default:
return "unknown password identifier " + pwdIdentifier;
}
}
@Override
public void initialize() {
}
/**
* This method checks whether PACE has previously been run with CAN.
* @return true iff PACE has previously been run with CAN, otherwise false
*/
public static boolean isPinTemporarilyResumed(CardStateAccessor cardState) {
HashSet<Class<? extends SecMechanism>> paceMechanisms = new HashSet<>();
paceMechanisms.add(PaceMechanism.class);
Collection<SecMechanism> currentMechanisms = cardState.getCurrentMechanisms(SecContext.APPLICATION, paceMechanisms);
if (currentMechanisms.size() > 0){
PaceMechanism paceMechanism = (PaceMechanism) currentMechanisms.toArray()[0];
PasswordAuthObject previouslyUsedPwd = paceMechanism.getUsedPassword();
int previouslyUsedPasswordIdentifier = previouslyUsedPwd.getPasswordIdentifier();
log(AbstractPaceProtocol.class, "last successfull PACE run used " + getPasswordName(previouslyUsedPasswordIdentifier) + " as password with value " + HexString.encode(previouslyUsedPwd.getPassword()), DEBUG);
return previouslyUsedPasswordIdentifier == Pace.ID_CAN;
} else{
return false;
}
}
/**
* This method performs all necessary steps to react to interrupted chaining.
*/
public void processChainingInterrupted() {
ResponseApdu resp = new ResponseApdu(SW_6883_LAST_COMMAND_EXPECTED);
processingData.updateResponseAPDU(this, "chaining interrupted", resp);
}
@Override
public Collection<TlvDataObject> getSecInfos(SecInfoPublicity publicity, MasterFile mf) {
OidIdentifier paceOidIdentifier = new OidIdentifier(OID_id_PACE);
Collection<CardObject> domainParameterCardObjects = mf.findChildren(
new DomainParameterSetIdentifier(), paceOidIdentifier);
HashSet<TlvDataObject> secInfos = new HashSet<TlvDataObject>();
for (CardObject curDomainParam : domainParameterCardObjects) {
Collection<CardObjectIdentifier> identifiers = curDomainParam.getAllIdentifiers();
//extract domainParameterId
int parameterId = -1;
for (CardObjectIdentifier curIdentifier : identifiers) {
if (curIdentifier instanceof DomainParameterSetIdentifier) {
parameterId = ((DomainParameterSetIdentifier) curIdentifier).getDomainParameterId();
break;
}
}
if (parameterId == -1) continue;
//construct and add PaceInfos
for (CardObjectIdentifier curIdentifier : identifiers) {
if (curIdentifier instanceof OidIdentifier) {
Oid curOid = ((OidIdentifier) curIdentifier).getOid();
if (curOid.startsWithPrefix(id_PACE)) {
byte[] oidBytes = curOid.toByteArray();
ConstructedTlvDataObject paceInfo = new ConstructedTlvDataObject(TAG_SEQUENCE);
paceInfo.addTlvDataObject(new PrimitiveTlvDataObject(TAG_OID, oidBytes));
paceInfo.addTlvDataObject(new PrimitiveTlvDataObject(TAG_INTEGER, new byte[]{2}));
paceInfo.addTlvDataObject(new PrimitiveTlvDataObject(TAG_INTEGER, new byte[]{(byte) parameterId}));
secInfos.add(paceInfo);
}
}
}
createDomainParameterInfo(secInfos, parameterId, paceOidIdentifier, identifiers, domainParameterCardObjects);
}
//TODO handle duplicates?
return secInfos;
}
/**
* This method creates security infos for proprietary domain parameter
*
* @param secInfos HashSet with all security infos
* @param parameterId the identifier for domain parameter
* @param paceOidIdentifier the pace oid identifier
* @param identifiers list of all identifier of the current domain parameter
* @param domainParameterCardObjects collection of card objects which matches the paceOidIdentifier
*/
public void createDomainParameterInfo(HashSet<TlvDataObject> secInfos, int parameterId,
OidIdentifier paceOidIdentifier, Collection<CardObjectIdentifier> identifiers,
Collection<CardObject> domainParameterCardObjects) {
//IMPL add required domainParameterInfo elemtens here
}
}