All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jmrtd.protocol.PACEProtocol Maven / Gradle / Ivy

The newest version!
/*
 * JMRTD - A Java API for accessing machine readable travel documents.
 *
 * Copyright (C) 2006 - 2023  The JMRTD team
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 * $Id: PACEProtocol.java 1880 2023-09-04 14:28:31Z martijno $
 */

package org.jmrtd.protocol;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.InvalidParameterException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.ECFieldFp;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.EllipticCurve;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.interfaces.DHPrivateKey;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.DHPublicKeySpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.jmrtd.APDULevelPACECapable;
import org.jmrtd.AccessKeySpec;
import org.jmrtd.BACKeySpec;
import org.jmrtd.CardServiceProtocolException;
import org.jmrtd.PACEKeySpec;
import org.jmrtd.PACESecretKeySpec;
import org.jmrtd.PassportService;
import org.jmrtd.Util;
import org.jmrtd.lds.PACEInfo;
import org.jmrtd.lds.PACEInfo.DHCParameterSpec;
import org.jmrtd.lds.PACEInfo.MappingType;

import net.sf.scuba.smartcards.CardServiceException;
import net.sf.scuba.tlv.TLVInputStream;
import net.sf.scuba.tlv.TLVOutputStream;
import net.sf.scuba.tlv.TLVUtil;
import net.sf.scuba.util.Hex;

/**
 * The Password Authenticated Connection Establishment protocol.
 *
 * @author The JMRTD team ([email protected])
 *
 * @version $Revision: 1880 $
 *
 * @since 0.5.6
 */
public class PACEProtocol {

  private static final Logger LOGGER = Logger.getLogger("org.jmrtd");

  private static final Provider BC_PROVIDER = Util.getBouncyCastleProvider();

  /**
   * Used in the last step of PACE-CAM.
   *
   * From 9303-11:
   *
   * AES [19] SHALL be used in CBC-mode according to [ISO/IEC 10116]
   * with IV=E(KSEnc,-1), where -1 is the bit string of length 128
   * with all bits set to 1.
   */
  private static final byte[] IV_FOR_PACE_CAM_DECRYPTION = {
      (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF,
      (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF,
      (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF,
      (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF
  };

  /** Constant used in IM pseudo random number mapping, see Doc 9303 - Part 11, 4.4.3.3.2. */
  /* a668892a7c41e3ca739f40b057d85904, 16 bytes, 128 bits  */
  private static final byte[] C0_LENGTH_128 =
    { (byte)0xA6, 0x68, (byte)0x89, 0x2A, 0x7C, 0x41, (byte)0xE3, (byte)0xCA, 0x73, (byte)0x9F, 0x40, (byte)0xB0, 0x57, (byte)0xD8, 0x59, 0x04 };

  /** Constant used in IM pseudo random number mapping, see Doc 9303 - Part 11, 4.4.3.3.2. */
  /* a4e136ac725f738b01c1f60217c188ad, 16 bytes, 128 bits */
  private static final byte[] C1_LENGTH_128 =
    { (byte)0xA4, (byte)0xE1, 0x36, (byte)0xAC, 0x72, 0x5F, 0x73, (byte)0x8B, 0x01, (byte)0xC1, (byte)0xF6, 0x02, 0x17, (byte)0xC1, (byte)0x88, (byte)0xAD };

  /** Constant used in IM pseudo random number mapping, see Doc 9303 - Part 11, 4.4.3.3.2. */
  /* d463d65234124ef7897054986dca0a174e28df758cbaa03f240616414d5a1676, 32 bytes, 256 bits */
  private static final byte[] C0_LENGTH_256 =
    { (byte)0xD4, 0x63, (byte)0xD6, 0x52, 0x34, 0x12, 0x4E, (byte)0xF7, (byte)0x89, 0x70, 0x54, (byte)0x98, 0x6D, (byte)0xCA, 0x0A, 0x17,
        0x4E, 0x28, (byte)0xDF, 0x75, (byte)0x8C, (byte)0xBA, (byte)0xA0, 0x3F, 0x24, 0x06, 0x16, 0x41, 0x4D, 0x5A, 0x16, 0x76 };

  /** Constant used in IM pseudo random number mapping, see Doc 9303 - Part 11, 4.4.3.3.2. */
  /* 54bd7255f0aaf831bec3423fcf39d69b6cbf066677d0faae5aadd99df8e53517, 32 bytes, 256 bits */
  private static final byte[] C1_LENGTH_256 =
    { 0x54, (byte)0xBD, 0x72, 0x55, (byte)0xF0, (byte)0xAA, (byte)0xF8, 0x31, (byte)0xBE, (byte)0xC3, 0x42, 0x3F, (byte)0xCF, 0x39, (byte)0xD6, (byte)0x9B,
        0x6C, (byte)0xBF, 0x06, 0x66, 0x77, (byte)0xD0, (byte)0xFA, (byte)0xAE, 0x5A, (byte)0xAD, (byte)0xD9, (byte)0x9D, (byte)0xF8, (byte)0xE5, 0x35, 0x17 };

  private APDULevelPACECapable service;

  private SecureMessagingWrapper wrapper;

  private int maxTranceiveLengthForSecureMessaging;

  private int maxTranceiveLengthForProtocol;

  private boolean shouldCheckMAC;

  private Random random;

  /**
   * Constructs a PACE protocol instance.
   * The max tranceive lengths used during PACE protocol execution will be set to 256.
   *
   * @param service the service for sending APDUs
   * @param wrapper the already established secure messaging channel (or {@code null})
   * @param maxTranceiveLength the maximal tranceive length (on responses to {@code READ BINARY})
   *        to use in the resulting secure messaging channel
   * @param shouldCheckMAC whether the resulting secure messaging channel should apply strict MAC
   *        checking on response APDUs
   *
   * @deprecated Use the other constructor with explicit max tranceive lengths for protocol and secure messaging
   */
  @Deprecated
  public PACEProtocol(APDULevelPACECapable service, SecureMessagingWrapper wrapper,
      int maxTranceiveLength, boolean shouldCheckMAC) {
    this(service, wrapper, 256, maxTranceiveLength, shouldCheckMAC);
  }

  /**
   * Constructs a PACE protocol instance.
   *
   * @param service the service for sending APDUs
   * @param wrapper the already established secure messaging channel (or {@code null})
   * @param maxTranceiveLengthForProtocol the maximal tranceive length PACE during protocol execution, {@code 256} or {@code 65536}
   * @param maxTranceiveLengthForSecureMessaging the maximal tranceive length (on responses to {@code READ BINARY})
   *        to use in the resulting secure messaging channel
   * @param shouldCheckMAC whether the resulting secure messaging channel should apply strict MAC
   *        checking on response APDUs
   */
  public PACEProtocol(APDULevelPACECapable service, SecureMessagingWrapper wrapper,
      int maxTranceiveLengthForProtocol,
      int maxTranceiveLengthForSecureMessaging, boolean shouldCheckMAC) {
    this.service = service;
    this.wrapper = wrapper;
    this.maxTranceiveLengthForProtocol = maxTranceiveLengthForProtocol;
    this.maxTranceiveLengthForSecureMessaging = maxTranceiveLengthForSecureMessaging;
    this.shouldCheckMAC = shouldCheckMAC;
    this.random = new SecureRandom();
  }

  /**
   * Performs the PACE 2.0 / SAC protocol.
   *
   * @param accessKey the MRZ or CAN based access key
   * @param oid as specified in the PACEInfo, indicates GM or IM or CAM, DH or ECDH, cipher, digest, length
   * @param staticParameters explicit static domain parameters for DH or ECDH
   * @param parameterId parameter identifier or {@code null}
   *
   * @return a PACE result
   *
   * @throws CardServiceException if authentication failed or on some lower-level error
   */
  public PACEResult doPACE(AccessKeySpec accessKey, String oid, AlgorithmParameterSpec staticParameters, BigInteger parameterId) throws CardServiceException {
    try {
      return doPACE(accessKey, deriveStaticPACEKey(accessKey, oid), oid, staticParameters, parameterId);
    } catch (GeneralSecurityException gse) {
      throw new CardServiceProtocolException("PCD side error in key derivation step", 0, gse);
    }
  }

  /**
   * Performs the PACE 2.0 / SAC protocol.
   *
   * @param accessKey the key specification from which the static PACE key is derived
   * @param staticPACEKey the password key
   * @param oid as specified in the PACEInfo, indicates GM or IM or CAM, DH or ECDH, cipher, digest, length
   * @param staticParameters explicit static domain parameters the domain params for DH or ECDH
   * @param parameterId parameter identifier or {@code null}
   *
   * @return a PACE result
   *
   * @throws CardServiceException if authentication failed or on lower level errors
   */
  private PACEResult doPACE(AccessKeySpec accessKey, SecretKey staticPACEKey, String oid, AlgorithmParameterSpec staticParameters, BigInteger parameterId) throws CardServiceException {
    MappingType mappingType = PACEInfo.toMappingType(oid); /* Either GM, CAM, or IM. */
    String agreementAlg = PACEInfo.toKeyAgreementAlgorithm(oid); /* Either DH or ECDH. */
    String cipherAlg  = PACEInfo.toCipherAlgorithm(oid); /* Either DESede or AES. */
    String digestAlg = PACEInfo.toDigestAlgorithm(oid); /* Either SHA-1 or SHA-256. */
    int keyLength = PACEInfo.toKeyLength(oid); /* Of the enc cipher. Either 128, 192, or 256. */

    checkConsistency(agreementAlg, cipherAlg, digestAlg, keyLength, staticParameters);

    Cipher staticPACECipher = null;
    try {
      staticPACECipher = Cipher.getInstance(cipherAlg + "/CBC/NoPadding");
    } catch (GeneralSecurityException gse) {
      throw new CardServiceProtocolException("PCD side error in static cipher construction during key derivation step", 0, gse);
    }

    try {

      /*
       * Doc 9303 4.4.4: Reference of a public key / secret key. REQUIRED.
       * The password to be used is indicated as follows:
       * 0x01: MRZ_information
       * 0x02: CAN
       */
      byte paceKeyReference = PassportService.MRZ_PACE_KEY_REFERENCE;
      if (staticPACEKey instanceof PACESecretKeySpec) {
        paceKeyReference = ((PACESecretKeySpec)staticPACEKey).getKeyReference();
      }

      /*
       * Doc 9303 4.4.4: Reference of a private key / Reference for computing a session key. CONDITIONAL.
       * This data object is REQUIRED to indicate the identifier of the domain
       * parameters to be used if the domain parameters are ambiguous, i.e.
       * more than one set of domain parameters is available for PACE.
       *
       * See discussion here: https://sourceforge.net/p/jmrtd/discussion/580232/thread/ff434886d2/.
       */
      byte[] referencePrivateKeyOrForComputingSessionKey = parameterId == null ? null : Util.i2os(parameterId);

      /* Send to the PICC. */
      service.sendMSESetATMutualAuth(wrapper, oid, paceKeyReference, referencePrivateKeyOrForComputingSessionKey);
    } catch (CardServiceException cse) {
      throw new CardServiceProtocolException("PICC side error in static PACE key derivation step", 0, cse);
    } catch (Exception e) {
      /* NOTE: Any other exception, must be PCD side. */
      throw new CardServiceProtocolException("PCD side error in static PACE key derivation step", 0, e);
    }

    /*
     * PCD and PICC exchange a chain of general authenticate commands.
     * Steps 1 to 4 below correspond with steps in table 3.3 of
     * ICAO TR-SAC 1.01.
     */

    /*
     * Receive encrypted nonce z = E(K_pi, s).
     * Decrypt nonce s = D(K_pi, z).
     */
    byte[] piccNonce = doPACEStep1(staticPACEKey, staticPACECipher);

    /*
     * Receive additional data required for map, i.e.,
     * a public key from PICC, and (conditionally) a nonce t.
     * Compute ephemeral domain parameters D~ = Map(D_PICC, s).
     */

    PACEMappingResult mappingResult = doPACEStep2(mappingType, agreementAlg, staticParameters, piccNonce, staticPACECipher);
    AlgorithmParameterSpec ephemeralParams = mappingResult.getEphemeralParameters();

    /* Choose random ephemeral PCD side keys (SK_PCD~, PK_PCD~, D~). */
    KeyPair ephemeralPCDKeyPair = doPACEStep3GenerateKeyPair(agreementAlg, ephemeralParams);

    /*
     * Exchange PK_PCD~ and PK_PICC~ with PICC.
     * Check that PK_PCD~ and PK_PICC~ differ.
     */
    PublicKey ephemeralPICCPublicKey = doPACEStep3ExchangePublicKeys(ephemeralPCDKeyPair.getPublic(), ephemeralParams);

    /* Key agreement K = KA(SK_PCD~, PK_PICC~, D~). */
    byte[] sharedSecretBytes = doPACEStep3KeyAgreement(agreementAlg, ephemeralPCDKeyPair.getPrivate(), ephemeralPICCPublicKey);

    /* Derive secure messaging keys. */
    /* Compute session keys K_mac = KDF_mac(K), K_enc = KDF_enc(K). */
    SecretKey encKey = null;
    SecretKey macKey = null;
    try {
      encKey = Util.deriveKey(sharedSecretBytes, cipherAlg, keyLength, Util.ENC_MODE);
      macKey = Util.deriveKey(sharedSecretBytes, cipherAlg, keyLength, Util.MAC_MODE);
    } catch (GeneralSecurityException gse) {
      throw new CardServiceProtocolException("Security exception during secure messaging key derivation", 3, gse);
    }

    /*
     * Compute authentication token T_PCD = MAC(K_mac, PK_PICC~).
     * Exchange authentication token T_PCD and T_PICC with PICC.
     * Check authentication token T_PICC.
     *
     * Extract encryptedChipAuthenticationData, if mapping is CAM.
     */
    byte[] encryptedChipAuthenticationData = doPACEStep4(oid, mappingType, ephemeralPCDKeyPair, ephemeralPICCPublicKey, macKey);
    byte[] chipAuthenticationData = null;
    /*
     * Start secure messaging.
     *
     * 4.6 of TR-SAC: If Secure Messaging is restarted, the SSC is used as follows:
     *  - The commands used for key agreement are protected with the old session keys and old SSC.
     *    This applies in particular for the response of the last command used for session key agreement.
     *  - The Send Sequence Counter is set to its new start value, i.e. within this specification the SSC is set to 0.
     *  - The new session keys and the new SSC are used to protect subsequent commands/responses.
     */
    try {
      long ssc = wrapper == null ? 0L : wrapper.getSendSequenceCounter();
      if (cipherAlg.startsWith("DESede")) {
        wrapper = new DESedeSecureMessagingWrapper(encKey, macKey, maxTranceiveLengthForSecureMessaging, shouldCheckMAC, 0L);
      } else if (cipherAlg.startsWith("AES")) {
        wrapper = new AESSecureMessagingWrapper(encKey, macKey, maxTranceiveLengthForSecureMessaging, shouldCheckMAC, ssc);
      } else {
        LOGGER.warning("Unsupported cipher algorithm " + cipherAlg);
      }
    } catch (GeneralSecurityException gse) {
      throw new CardServiceProtocolException("Security exception in secure messaging establishment", 4, gse);
    }

    if (MappingType.CAM.equals(mappingType)) {

      if (encryptedChipAuthenticationData == null) {
        LOGGER.warning("Encrypted Chip Authentication data is null");
      }

      /* Decrypt A_PICC to recover CA_PICC. */
      try {
        Cipher decryptCipher = Cipher.getInstance("AES/CBC/NoPadding");
        decryptCipher.init(Cipher.DECRYPT_MODE, encKey, new IvParameterSpec(IV_FOR_PACE_CAM_DECRYPTION));
        byte[] paddedChipAuthenticationData = decryptCipher.doFinal(encryptedChipAuthenticationData);
        chipAuthenticationData = Util.unpad(paddedChipAuthenticationData);
      } catch (GeneralSecurityException gse) {
        LOGGER.log(Level.WARNING, "Could not decrypt Chip Authentication data", gse);
      }

      /* CAM result. Include Chip Authentication data. */
      return new PACECAMResult(accessKey, agreementAlg, cipherAlg, digestAlg, keyLength,
          mappingResult, ephemeralPCDKeyPair, ephemeralPICCPublicKey,
          encryptedChipAuthenticationData, chipAuthenticationData, wrapper);
    }

    /* GM or IM result. */
    return new PACEResult(accessKey, mappingType, agreementAlg, cipherAlg, digestAlg, keyLength,
        mappingResult, ephemeralPCDKeyPair, ephemeralPICCPublicKey, wrapper);
  }

  /*
   * 1. Encrypted Nonce     - --- Absent        - 0x80 Encrypted Nonce
   *
   * Receive encrypted nonce z = E(K_pi, s).
   * (This is steps 1-3 in Table 4.4 in BSI 03111 2.0.)
   *
   * Decrypt nonce s = D(K_pi, z).
   * (This is step 4 in Table 4.4 in BSI 03111 2.0.)
   */
  /**
   * The first step in the PACE protocol receives an encrypted nonce from the PICC
   * and decrypts it.
   *
   * @param staticPACEKey the static PACE key
   * @param staticPACECipher the cipher to reuse
   *
   * @return the decrypted encrypted PICC nonce
   *
   * @throws CardServiceProtocolException on error
   */
  public byte[] doPACEStep1(SecretKey staticPACEKey, Cipher staticPACECipher) throws CardServiceProtocolException {
    byte[] piccNonce = null;
    try {
      byte[] step1Data = new byte[] { };
      /* Command data is empty. This implies an empty dynamic authentication object. */
      byte[] step1Response = service.sendGeneralAuthenticate(wrapper, step1Data, 256, false);
      byte[] step1EncryptedNonce = TLVUtil.unwrapDO(0x80, step1Response);

      /* (Re)initialize the K_pi cipher for decryption. */
      staticPACECipher.init(Cipher.DECRYPT_MODE, staticPACEKey, new IvParameterSpec(new byte[staticPACECipher.getBlockSize()])); // Fix proposed by Halvdan Grelland ([email protected])

      piccNonce = staticPACECipher.doFinal(step1EncryptedNonce);
      return piccNonce;
    } catch (GeneralSecurityException gse) {
      throw new CardServiceProtocolException("PCD side exception in tranceiving nonce step", 1, gse);
    } catch (CardServiceException cse) {
      throw new CardServiceProtocolException("PICC side exception in tranceiving nonce step", 1, cse);
    }
  }

  /*
   * 2. Map Nonce       - 0x81 Mapping Data     - 0x82 Mapping Data
   *
   * (This is step 3.a) in the protocol in TR-SAC.)
   * (This is step 5 in Table 4.4 in BSI 03111 2.0.)
   *
   * Receive additional data required for map (i.e. a public key from PICC, and (conditionally) a nonce t).
   * Compute ephemeral domain parameters D~ = Map(D_PICC, s).
   */
  /**
   * The second step in the PACE protocol computes ephemeral domain parameters
   * by mapping the PICC generated nonce (and optionally the PCD generated nonce,
   * which will be exchanged, in case of Integrated Mapping).
   *
   * @param mappingType either CAM, GM, or IM
   * @param agreementAlg the agreement algorithm, either DH or ECDH
   * @param params the static domain parameters
   * @param piccNonce the nonce received from the PICC
   * @param staticPACECipher the cipher to use in IM
   *
   * @return the newly computed ephemeral domain parameters
   *
   * @throws CardServiceProtocolException on error
   */
  public PACEMappingResult doPACEStep2(MappingType mappingType, String agreementAlg, AlgorithmParameterSpec params, byte[] piccNonce, Cipher staticPACECipher) throws CardServiceProtocolException {
    switch(mappingType) {
      case CAM:
        // Fall through to GM case.
      case GM:
        return doPACEStep2GM(agreementAlg, params, piccNonce);
      case IM:
        return doPACEStep2IM(agreementAlg, params, piccNonce, staticPACECipher);
      default:
        throw new CardServiceProtocolException("Unsupported mapping type " + mappingType, 2);
    }
  }

  /**
   * The second step in the PACE protocol (GM case) computes ephemeral domain parameters
   * by performing a key agreement protocol with the PICC nonce as
   * input.
   *
   * @param agreementAlg the agreement algorithm, either DH or ECDH
   * @param params the static domain parameters
   * @param piccNonce the received nonce from the PICC
   *
   * @return the computed ephemeral domain parameters
   *
   * @throws CardServiceProtocolException on error
   */
  public PACEGMMappingResult doPACEStep2GM(String agreementAlg, AlgorithmParameterSpec params, byte[] piccNonce) throws CardServiceProtocolException {
    try {
      KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(agreementAlg, BC_PROVIDER);
      keyPairGenerator.initialize(params);
      KeyPair pcdMappingKeyPair = keyPairGenerator.generateKeyPair();
      PublicKey pcdMappingPublicKey = pcdMappingKeyPair.getPublic();
      PrivateKey pcdMappingPrivateKey = pcdMappingKeyPair.getPrivate();

      byte[] pcdMappingEncodedPublicKey = encodePublicKeyForSmartCard(pcdMappingPublicKey);
      byte[] step2Data = TLVUtil.wrapDO(0x81, pcdMappingEncodedPublicKey);

      byte[] step2Response = service.sendGeneralAuthenticate(wrapper, step2Data, maxTranceiveLengthForProtocol, false);

      byte[] piccMappingEncodedPublicKey = TLVUtil.unwrapDO(0x82, step2Response);
      PublicKey piccMappingPublicKey = decodePublicKeyFromSmartCard(piccMappingEncodedPublicKey, params);

      if ("ECDH".equals(agreementAlg)) {
        /* Treat shared secret as an ECPoint. */
        PACEGMWithECDHAgreement mappingAgreement = new PACEGMWithECDHAgreement();
        mappingAgreement.init(pcdMappingPrivateKey);
        ECPoint mappingSharedSecretPoint = mappingAgreement.doPhase(piccMappingPublicKey);
        AlgorithmParameterSpec ephemeralParameters = mapNonceGMWithECDH(piccNonce, mappingSharedSecretPoint, (ECParameterSpec)params);
        return new PACEGMWithECDHMappingResult(params, piccNonce, piccMappingPublicKey, pcdMappingKeyPair, mappingSharedSecretPoint, ephemeralParameters);
      } else if ("DH".equals(agreementAlg)) {
        KeyAgreement mappingAgreement = KeyAgreement.getInstance(agreementAlg);
        mappingAgreement.init(pcdMappingPrivateKey);
        mappingAgreement.doPhase(piccMappingPublicKey, true);
        byte[] mappingSharedSecretBytes = mappingAgreement.generateSecret();
        AlgorithmParameterSpec ephemeralParameters = mapNonceGMWithDH(piccNonce, Util.os2i(mappingSharedSecretBytes), (DHParameterSpec)params);
        return new PACEGMWithDHMappingResult(params, piccNonce, piccMappingPublicKey, pcdMappingKeyPair, mappingSharedSecretBytes, ephemeralParameters);
      } else {
        throw new IllegalArgumentException("Unsupported parameters for mapping nonce, expected \"ECDH\" / ECParameterSpec or \"DH\" / DHParameterSpec"
            + ", found \"" + agreementAlg + "\" /" + params.getClass().getCanonicalName());
      }
    } catch (CardServiceException cse) {
      throw new CardServiceProtocolException("PICC side exception in mapping nonce step", 2, cse);
    } catch (Exception e) {
      /* NOTE: Any exception, must be PCD side. Typically this is subclass of GeneralSecurityException. */
      throw new CardServiceProtocolException("PCD side error in mapping nonce step", 2, e);
    }
  }

  /*
   * The function Map:G -> G_Map is defined as
   * G_Map = f_G(R_p(s,t)),
   * where R_p() is a pseudo-random function that maps octet strings to elements of GF(p)
   * and f_G() is a function that maps elements of GF(p) to .
   * The random nonce t SHALL be chosen randomly by the inspection system
   * and sent to the MRTD chip.
   * The pseudo-random function R_p() is described in Section 3.4.2.2.3.
   * The function f_G() is defined in [4] and [25].
   *
   * [4]: Brier, Eric; Coron, Jean-Sé́bastien; Icart, Thomas; Madore, David; Randriam, Hugues; and
   *      Tibouch, Mehdi, Efficient Indifferentiable Hashing into Ordinary Elliptic Curves, Advances in
   *      Cryptology – CRYPTO 2010, Springer-Verlag, 2010.
   * [25]: Sagem, MorphoMapping Patents FR09-54043 and FR09-54053, 2009
   */
  /**
   * The second step in the PACE protocol computes ephemeral domain parameters
   * by performing a key agreement protocol with the PICC and PCD nonces as
   * input.
   *
   * @param agreementAlg the agreement algorithm, either DH or ECDH
   * @param params the static domain parameters
   * @param piccNonce the received nonce from the PICC
   * @param staticPACECipher the cipher to use for IM
   *
   * @return the computed ephemeral domain parameters
   *
   * @throws CardServiceProtocolException on error
   */
  public PACEIMMappingResult doPACEStep2IM(String agreementAlg, AlgorithmParameterSpec params, byte[] piccNonce, Cipher staticPACECipher) throws CardServiceProtocolException {
    try {

      byte[] pcdNonce = new byte[piccNonce.length];
      random.nextBytes(pcdNonce);

      byte[] step2Data = TLVUtil.wrapDO(0x81, pcdNonce);

      /*
       * NOTE: The context specific data object 0x82 SHALL be empty (TR SAC 3.3.2).
       */
      /* byte[] step2Response = */ service.sendGeneralAuthenticate(wrapper, step2Data, maxTranceiveLengthForProtocol, false);

      if ("ECDH".equals(agreementAlg)) {
        AlgorithmParameterSpec ephemeralParameters = mapNonceIMWithECDH(piccNonce, pcdNonce, staticPACECipher.getAlgorithm(), (ECParameterSpec)params);
        return new PACEIMMappingResult(params, piccNonce, pcdNonce, ephemeralParameters);
      } else if ("DH".equals(agreementAlg)) {
        AlgorithmParameterSpec ephemeralParameters = mapNonceIMWithDH(piccNonce, pcdNonce, staticPACECipher.getAlgorithm(), (DHParameterSpec)params);
        return new PACEIMMappingResult(params, piccNonce, pcdNonce, ephemeralParameters);
      } else {
        throw new IllegalArgumentException("Unsupported parameters for mapping nonce, expected \"ECDH\" / ECParameterSpec or \"DH\" / DHParameterSpec"
            + ", found \"" + agreementAlg + "\" /" + params.getClass().getCanonicalName());
      }
    } catch (GeneralSecurityException gse) {
      throw new CardServiceProtocolException("PCD side error in mapping nonce step", 2, gse);
    } catch (CardServiceException cse) {
      throw new CardServiceProtocolException("PICC side exception in mapping nonce step", 2, cse);
    }
  }

  /* Choose a random ephemeral key pair. (SK_PCD~, PK_PCD~, D~). */
  /**
   * Chooses a random ephemeral key pair.
   *
   * @param agreementAlg the agreement algorithm
   * @param ephemeralParams the parameters
   *
   * @return the key pair
   *
   * @throws CardServiceProtocolException on error
   */
  public KeyPair doPACEStep3GenerateKeyPair(String agreementAlg, AlgorithmParameterSpec ephemeralParams) throws CardServiceProtocolException {
    try {
      KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(agreementAlg, BC_PROVIDER);
      keyPairGenerator.initialize(ephemeralParams);
      return keyPairGenerator.generateKeyPair();
    } catch (GeneralSecurityException gse) {
      throw new CardServiceProtocolException("PCD side error during generation of PCD key pair", 3, gse);
    }
  }

  /*
   * 3. Perform Key Agreement - 0x83 Ephemeral Public Key - 0x84 Ephemeral Public Key
   *
   * Exchange PK_PCD~ and PK_PICC~ with PICC.
   * Check that PK_PCD~ and PK_PICC~ differ.
   */
  /**
   * Sends the PCD's public key to the PICC and receives and interprets the PICC's public key in exchange.
   *
   * @param pcdPublicKey the PCD's public key
   * @param ephemeralParams the ephemeral parameters to interpret the PICC's public key
   *
   * @return the PICC's public key
   *
   * @throws CardServiceProtocolException on error
   */
  public PublicKey doPACEStep3ExchangePublicKeys(PublicKey pcdPublicKey, AlgorithmParameterSpec ephemeralParams)  throws CardServiceProtocolException {
    try {
      byte[] pcdEncodedPublicKey = encodePublicKeyForSmartCard(pcdPublicKey);
      byte[] step3Data = TLVUtil.wrapDO(0x83, pcdEncodedPublicKey);
      byte[] step3Response = service.sendGeneralAuthenticate(wrapper, step3Data, maxTranceiveLengthForProtocol, false);
      byte[] piccEncodedPublicKey = TLVUtil.unwrapDO(0x84, step3Response);
      PublicKey piccPublicKey = decodePublicKeyFromSmartCard(piccEncodedPublicKey, ephemeralParams);

      if (keysAreEqual(pcdPublicKey, piccPublicKey)) {
        throw new CardServiceProtocolException("PCD's public key and PICC's public key are the same in key agreement step!", 3);
      }

      return piccPublicKey;
    } catch (CardServiceException cse) {
      throw new CardServiceProtocolException("PICC side exception in key agreement step", 3, cse);
    } catch (Exception e) {
      throw new CardServiceProtocolException("PCD side exception in key agreement step", 3, e);
    }
  }

  /* Key agreement K = KA(SK_PCD~, PK_PICC~, D~). */
  /**
   * Performs the key agreement.
   *
   * @param agreementAlg the agreement algorithm, either {@code "DH"} or {@code "ECDH"}
   * @param pcdPrivateKey the PCD's private key
   * @param piccPublicKey the PICC's public key
   *
   * @return the shared secret
   *
   * @throws CardServiceProtocolException on error
   */
  public byte[] doPACEStep3KeyAgreement(String agreementAlg, PrivateKey pcdPrivateKey, PublicKey piccPublicKey) throws CardServiceProtocolException {
    try {
      KeyAgreement keyAgreement = KeyAgreement.getInstance(agreementAlg, BC_PROVIDER);
      keyAgreement.init(pcdPrivateKey);
      keyAgreement.doPhase(updateParameterSpec(piccPublicKey, pcdPrivateKey), true);
      return keyAgreement.generateSecret();
    } catch (Exception e) {
      throw new CardServiceProtocolException("PCD side error during key agreement", 3, e);
    }
  }

  /*
   * 4. Mutual Authentication - 0x85 Authentication Token - 0x86 Authentication Token
   *
   * Compute authentication token T_PCD = MAC(K_mac, PK_PICC~).
   * Exchange authentication token T_PCD and T_PICC with PICC.
   * Check authentication token T_PICC.
   *
   * Extracts encryptedChipAuthenticationData, if mapping type id CAM.
   */
  /**
   * Exchanges authentication tokens.
   *
   * @param oid the object identifier
   * @param mappingType the mapping type (GM or IM)
   * @param pcdKeyPair the PCD's key pair
   * @param piccPublicKey the PICC's public key
   * @param macKey the MAC key to use
   *
   * @return possible encrypted chip authentication data (PACE-CAM case)
   *
   * @throws CardServiceException on error
   */
  public byte[] doPACEStep4(String oid, MappingType mappingType, KeyPair pcdKeyPair, PublicKey piccPublicKey, SecretKey macKey) throws CardServiceException {
    try {
      byte[] pcdToken = generateAuthenticationToken(oid, macKey, piccPublicKey);
      byte[] step4Data = TLVUtil.wrapDO(0x85, pcdToken);
      byte[] step4Response = service.sendGeneralAuthenticate(wrapper, step4Data, 256, true);
      TLVInputStream step4ResponseInputStream = new TLVInputStream(new ByteArrayInputStream(step4Response));
      try {
        int tag86 = step4ResponseInputStream.readTag();
        if (tag86 != 0x86) {
          LOGGER.warning("Was expecting tag 0x86, found: " + Integer.toHexString(tag86));
        }
        /* int piccTokenLength = */ step4ResponseInputStream.readLength();
        byte[] piccToken = step4ResponseInputStream.readValue();

        byte[] expectedPICCToken = generateAuthenticationToken(oid, macKey, pcdKeyPair.getPublic());
        if (!Arrays.equals(expectedPICCToken, piccToken)) {
          throw new GeneralSecurityException("PICC authentication token mismatch"
              + ", expectedPICCToken = " + Hex.bytesToHexString(expectedPICCToken)
              + ", piccToken = " + Hex.bytesToHexString(piccToken));
        }

        if (mappingType == MappingType.CAM) {
          int tag8A = step4ResponseInputStream.readTag();
          if (tag8A != 0x8A) {
            LOGGER.warning("Was expecting tag 0x8A, found: " + Integer.toHexString(tag8A));
          }
          /* int encryptedChipAuthenticationDataLength = */ step4ResponseInputStream.readLength();
          return step4ResponseInputStream.readValue();
        }
      } catch (IOException ioe) {
        LOGGER.log(Level.WARNING, "Could not parse step 4 response", ioe);
      } finally {
        try {
          step4ResponseInputStream.close();
        } catch (IOException ioe) {
          LOGGER.log(Level.FINE, "Exception closing stream", ioe);
        }
      }

      return null;
    } catch (Exception e) {
      throw new CardServiceProtocolException("PCD side exception in authentication token generation step", 4, e);
    }
  }

  /**
   * Derives the static key K_pi.
   *
   * @param accessKey the key material from the MRZ
   * @param oid the PACE object identifier is needed to determine the cipher algorithm and the key length
   *
   * @return the derived key
   *
   * @throws GeneralSecurityException on error
   */
  public static SecretKey deriveStaticPACEKey(AccessKeySpec accessKey, String oid) throws GeneralSecurityException {
    String cipherAlg  = PACEInfo.toCipherAlgorithm(oid); /* Either DESede or AES. */
    int keyLength = PACEInfo.toKeyLength(oid); /* Of the enc cipher. Either 128, 192, or 256. */
    byte[] keySeed = computeKeySeedForPACE(accessKey);

    byte paceKeyReference = 0;
    if (accessKey instanceof PACEKeySpec) {
      paceKeyReference = ((PACEKeySpec)accessKey).getKeyReference();
    }

    return Util.deriveKey(keySeed, cipherAlg, keyLength, null, Util.PACE_MODE, paceKeyReference);
  }

  /**
   * Computes a key seed based on an access key.
   *
   * @param accessKey the access key
   *
   * @return a key seed for secure messaging keys
   *
   * @throws GeneralSecurityException on error
   */
  public static byte[] computeKeySeedForPACE(AccessKeySpec accessKey) throws GeneralSecurityException {
    if (accessKey == null) {
      throw new IllegalArgumentException("Access key cannot be null");
    }

    /* MRZ based key. */
    if (accessKey instanceof BACKeySpec) {
      BACKeySpec bacKey = (BACKeySpec)accessKey;
      String documentNumber = bacKey.getDocumentNumber();
      String dateOfBirth = bacKey.getDateOfBirth();
      String dateOfExpiry = bacKey.getDateOfExpiry();

      if (dateOfBirth == null || dateOfBirth.length() != 6) {
        throw new IllegalArgumentException("Wrong date format used for date of birth. Expected yyMMdd, found " + dateOfBirth);
      }
      if (dateOfExpiry == null || dateOfExpiry.length() != 6) {
        throw new IllegalArgumentException("Wrong date format used for date of expiry. Expected yyMMdd, found " + dateOfExpiry);
      }
      if (documentNumber == null) {
        throw new IllegalArgumentException("Wrong document number. Found " + documentNumber);
      }

      documentNumber = fixDocumentNumber(documentNumber);

      return computeKeySeedForPACE(documentNumber, dateOfBirth, dateOfExpiry);
    }

    if (accessKey instanceof PACEKeySpec) {
      return ((PACEKeySpec)accessKey).getKey();
    }

    LOGGER.warning("JMRTD doesn't recognize this type of access key, best effort key derivation!");
    return accessKey.getKey();
  }

  /* Generic Mapping. */

  /**
   * Maps the nonce  for the ECDH case
   * using Generic Mapping to get new parameters
   * (notably a new generator).
   *
   * @param nonceS the nonce received from the PICC
   * @param sharedSecretPointH the shared secret
   * @param staticParameters the static parameters
   *
   * @return the new parameters
   */
  public static ECParameterSpec mapNonceGMWithECDH(byte[] nonceS, ECPoint sharedSecretPointH, ECParameterSpec staticParameters) {
    /*
     * D~ = (p, a, b, G~, n, h) where G~ = [s]G + H
     */
    ECPoint generator = staticParameters.getGenerator();
    EllipticCurve curve = staticParameters.getCurve();
    BigInteger a = curve.getA();
    BigInteger b = curve.getB();
    ECFieldFp field = (ECFieldFp)curve.getField();
    BigInteger p = field.getP();
    BigInteger order = staticParameters.getOrder();
    int cofactor = staticParameters.getCofactor();
    ECPoint ephemeralGenerator = Util.add(Util.multiply(Util.os2i(nonceS), generator, staticParameters), sharedSecretPointH, staticParameters);
    if (!Util.toBouncyCastleECPoint(ephemeralGenerator, staticParameters).isValid()) {
      LOGGER.info("ephemeralGenerator is not a valid point");
    }
    return new ECParameterSpec(new EllipticCurve(new ECFieldFp(p), a, b), ephemeralGenerator, order, cofactor);
  }

  /**
   * Maps the nonce for the DH case using Generic Mapping
   * to get new parameters
   * (notably a new generator).
   *
   * @param nonceS the nonce received from the PICC
   * @param sharedSecretH the shared secret point
   * @param staticParameters the static parameters
   *
   * @return the new parameters
   */
  public static DHParameterSpec mapNonceGMWithDH(byte[] nonceS, BigInteger sharedSecretH, DHParameterSpec staticParameters) {
    // g~ = g^s * h
    BigInteger p = staticParameters.getP();
    BigInteger generator = staticParameters.getG();
    BigInteger mappedGenerator = generator.modPow(Util.os2i(nonceS), p).multiply(sharedSecretH).mod(p);
    return new DHParameterSpec(p, mappedGenerator, staticParameters.getL());
  }

  /* Integrated Mapping. */

  /**
   * Transforms the nonces using a pseudo random number function and maps the resulting value to a point on the curve.
   * The resulting point is used as a generator as part of the returned domain parameters.
   *
   * @param nonceS the nonce from the PICC
   * @param nonceT the nonce from the PCD
   * @param cipherAlgorithm the cipher algorithm to be used by the pseudo random function (either {@code "AES"} or {@code "DESede"})
   * @param params the static domain parameters
   *
   * @return the newly computed domain parameters
   *
   * @throws GeneralSecurityException on error
   */
  public static AlgorithmParameterSpec mapNonceIMWithECDH(byte[] nonceS, byte[] nonceT, String cipherAlgorithm, ECParameterSpec params) throws GeneralSecurityException {
    BigInteger p = Util.getPrime(params);
    BigInteger order = params.getOrder();
    int cofactor = params.getCofactor();
    BigInteger a = params.getCurve().getA();
    BigInteger b = params.getCurve().getB();

    BigInteger t = Util.os2i(pseudoRandomFunction(nonceS, nonceT, p, cipherAlgorithm));

    ECPoint mappedGenerator = icartPointEncode(t, params);
    return new ECParameterSpec(new EllipticCurve(new ECFieldFp(p), a, b), mappedGenerator, order, cofactor);
  }

  /*
   * The function Map: g -> g~ is defined as g~ = f_g(R_p(s, t)), where R_p() is the pseudo-random function
   * that maps octet strings to elements of GF(p) and f_g() is a function that maps elements of GF(p) to
   * . The random nonce t SHALL be chosen randomly by the inspection system and sent to the MRTD
   * chip. The pseudo-random function R_p() is described in Section 4.3.3. The function f_g() is defined as
   * f_g(x) = x^a mod p, and a = (p-1)/q is the cofactor. Implementations MUST check that g~ != 1.
   *
   * NOTE: The public key validation method described in RFC 2631 MUST be used to
   * prevent small subgroup attacks.
   */
  /**
   * Transforms the nonces using a pseudo random number function and maps the resulting value to a field element.
   * The resulting field element is used as a generator as part of the returned domain parameters.
   *
   * @param nonceS the nonce from the PICC
   * @param nonceT the nonce from the PCD
   * @param cipherAlgorithm the cipher algorithm to be used by the pseudo random function (either {@code "AES"} or {@code "DESede"})
   * @param params the static domain parameters
   *
   * @return the newly computed domain parameters
   *
   * @throws GeneralSecurityException on error
   */
  public static AlgorithmParameterSpec mapNonceIMWithDH(byte[] nonceS, byte[] nonceT, String cipherAlgorithm, DHParameterSpec params) throws GeneralSecurityException {
    BigInteger g = params.getG();
    if (g == null || g.equals(BigInteger.ONE)) {
      throw new IllegalArgumentException("Invalid generator: " + g);
    }

    BigInteger p = params.getP();

    BigInteger q = params instanceof DHCParameterSpec ? ((DHCParameterSpec)params).getQ() : BigInteger.ONE; // FIXME: What if q not available? We use 1 here? Should be p-1? -- MO

    BigInteger x = Util.os2i(pseudoRandomFunction(nonceS, nonceT, p, cipherAlgorithm));

    BigInteger a = p.subtract(BigInteger.ONE).divide(q);

    BigInteger mappedGenerator = x.modPow(a, p);
    return new DHParameterSpec(p, mappedGenerator, params.getL());
  }

  /*
   * The function R_p(s,t) is a function that maps octet strings s (of bit length l) and t (of bit length k)
   * to an element int(x_1 || x_2 || ... || x_n) mod p of GF(p).
   * The function R(s,t) is specified in Figure 2.
   * The construction is based on the respective block cipher E() in CBC mode according to ISO/IEC 10116 [12]
   * with IV=0, where k is the key size (in bits) of E().
   * Where required, the output k_i MUST be truncated to key size k.
   * The value n SHALL be selected as smallest number, such that n*l >= log2 p + 64.
   */
  /**
   * Pseudo random number function as specified in Doc 9303 - Part 11, 4.4.3.3.2.
   * Used in PACE IM.
   *
   * @param s the nonce that was sent by the ICC
   * @param t the nonce that was generated by the PCD
   * @param p the order of the prime field
   * @param algorithm the algorithm for block cipher E (either {@code "AES"} or {@code "DESede"})
   *
   * @return the resulting x
   *
   * @throws GeneralSecurityException on cryptographic error
   */
  public static byte[] pseudoRandomFunction(byte[] s, byte[] t, BigInteger p, String algorithm) throws GeneralSecurityException {
    if (s == null || t == null) {
      throw new IllegalArgumentException("Null nonce");
    }

    int l = s.length * 8;
    int k = t.length * 8; /* Key size in bits. */

    byte[] c0 = null;
    byte[] c1 = null;
    switch (l) {
      case 128:
        c0 = C0_LENGTH_128;
        c1 = C1_LENGTH_128;
        break;
      case 192: // Fall through
      case 256:
        c0 = C0_LENGTH_256;
        c1 = C1_LENGTH_256;
        break;
      default:
        throw new IllegalArgumentException("Unknown length " + l + ", was expecting 128, 192, or 256");
    }

    Cipher cipher = Cipher.getInstance(algorithm + (algorithm.endsWith("/CBC/NoPadding") ? "" : "/CBC/NoPadding"));
    int blockSize = cipher.getBlockSize(); /* in bytes */

    IvParameterSpec zeroIV = new IvParameterSpec(new byte[blockSize]);

    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(t, algorithm), zeroIV);
    byte[] key = cipher.doFinal(s);

    ByteArrayOutputStream x = new ByteArrayOutputStream();

    try {
      int n = 0;
      while (n * l < p.bitLength() + 64) {
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, 0, k / 8, algorithm), zeroIV);
        key = cipher.doFinal(c0);
        x.write(cipher.doFinal(c1));
        n++;
      }

      byte[] xBytes = x.toByteArray();
      return Util.i2os(Util.os2i(xBytes).mod(p));
    } catch (Exception ioe) {
      /* NOTE: Never happens, writing to byte array output stream. */
      LOGGER.log(Level.WARNING, "Could not write to stream", ioe);

      return Util.i2os(Util.os2i(x.toByteArray()).mod(p));
    } finally {
      try {
        x.close();
      } catch (IOException ioe) {
        LOGGER.log(Level.FINE, "Could not close stream", ioe);
      }
    }
  }

  /*
   * Icart's point encoding.
   * For now this is based on the implementation for affine coordinates
   * as described in ICAO SAC TR 2010, Section 5.2.
   */
  /**
   * Icart's point encoding for Elliptic Curve over a prime field.
   * This maps a field element to a point on the curve.
   * Used in PACE IM ECDH.
   *
   * @param t the field element to encode
   * @param params the parameters describing the curve and field
   *
   * @return the point on the curve that the input is mapped to
   */
  public static ECPoint icartPointEncode(BigInteger t, ECParameterSpec params) {
    BigInteger p = Util.getPrime(params);
    if (!BigInteger.valueOf(3).equals(p.mod(BigInteger.valueOf(4)))) {
      throw new InvalidParameterException("Cannot encode point because p != 3 (mod 4)");
    }

    int cofactor = params.getCofactor();
    BigInteger a = params.getCurve().getA();
    BigInteger b = params.getCurve().getB();

    /* 1. */
    BigInteger alpha = t.modPow(BigInteger.valueOf(2), p).negate().mod(p);

    /* 2. (Using implementation note 5.2.2). */
    BigInteger alphaSq = alpha.modPow(BigInteger.valueOf(2), p);
    BigInteger alphaPlusAlphaSq = alpha.add(alphaSq).mod(p);
    BigInteger onePlusAlphaPlusAlphaSq = BigInteger.ONE.add(alphaPlusAlphaSq);
    BigInteger pMinus2 = p.subtract(BigInteger.ONE).subtract(BigInteger.ONE);
    BigInteger x2 = b.negate().multiply(onePlusAlphaPlusAlphaSq).multiply(a.multiply(alphaPlusAlphaSq).modPow(pMinus2, p)).mod(p);

    /* 3. */
    BigInteger x3 = alpha.multiply(x2).mod(p);

    /* 4. */
    BigInteger h2 = x2.modPow(BigInteger.valueOf(3), p).add(a.multiply(x2)).add(b).mod(p);

    /* 5. (Why are we calculating this?) */
    //    BigInteger h3 = x3.modPow(BigInteger.valueOf(3), p).add(a.multiply(x3)).add(b).mod(p);

    /* 6. */
    BigInteger u = t.modPow(BigInteger.valueOf(3), p).multiply(h2).mod(p);

    /* 7. */
    BigInteger pPlusOneOverFour = p.add(BigInteger.ONE).multiply(BigInteger.valueOf(4).modInverse(p)).mod(p);
    BigInteger pMinusOneMinusPPlusOneOverFour = p.subtract(BigInteger.ONE).subtract(pPlusOneOverFour);
    BigInteger aa = h2.modPow(pMinusOneMinusPPlusOneOverFour, p);

    BigInteger aaSqTimesH2 = aa.modPow(BigInteger.valueOf(2), p).multiply(h2).mod(p);

    ECPoint xy = aaSqTimesH2.equals(BigInteger.ONE) ? new ECPoint(x2, aa.multiply(h2).mod(p)) : new ECPoint(x3, aa.multiply(u).mod(p));

    if (cofactor == 1) {
      return Util.normalize(xy, params);
    } else {
      org.bouncycastle.math.ec.ECPoint bcPoint = Util.toBouncyCastleECPoint(xy, params);
      bcPoint.multiply(BigInteger.valueOf(cofactor));
      return Util.fromBouncyCastleECPoint(bcPoint);
    }
  }

  /**
   * Updates the parameters of the given public key to match the parameters of the given private key.
   *
   * @param publicKey the public key, should be an EC public key
   * @param privateKey the private key, should be an EC private key
   *
   * @return a new public key that uses the parameters of the private key
   *
   * @throws GeneralSecurityException on security error, or when keys are not EC
   */
  public static PublicKey updateParameterSpec(PublicKey publicKey, PrivateKey privateKey) throws GeneralSecurityException {
    String publicKeyAlgorithm = publicKey.getAlgorithm();
    String privateKeyAlgorithm = privateKey.getAlgorithm();

    if ("EC".equals(publicKeyAlgorithm) || "ECDH".equals(publicKeyAlgorithm)) {
      if (!("EC".equals(privateKeyAlgorithm) || "ECDH".equals(privateKeyAlgorithm))) {
        throw new NoSuchAlgorithmException("Unsupported key type public: " + publicKeyAlgorithm + ", private: " + privateKeyAlgorithm);
      }
      KeyFactory keyFactory = KeyFactory.getInstance("EC", BC_PROVIDER);
      KeySpec keySpec = new ECPublicKeySpec(((ECPublicKey)publicKey).getW(), ((ECPrivateKey)privateKey).getParams());
      return keyFactory.generatePublic(keySpec);
    } else if ("DH".equals(publicKeyAlgorithm)) {
      if (!("DH".equals(privateKeyAlgorithm))) {
        throw new NoSuchAlgorithmException("Unsupported key type public: " + publicKeyAlgorithm + ", private: " + privateKeyAlgorithm);
      }
      KeyFactory keyFactory = KeyFactory.getInstance("DH");
      DHPublicKey dhPublicKey = (DHPublicKey)publicKey;
      DHPrivateKey dhPrivateKey = (DHPrivateKey)privateKey;
      DHParameterSpec privateKeyParams = dhPrivateKey.getParams();
      KeySpec keySpec = new DHPublicKeySpec(dhPublicKey.getY(), privateKeyParams.getP(), privateKeyParams.getG());
      return keyFactory.generatePublic(keySpec);
    } else {
      throw new NoSuchAlgorithmException("Unsupported key type public: " + publicKeyAlgorithm + ", private: " + privateKeyAlgorithm);
    }
  }

  /**
   * Generates an authentication token.
   * The authentication token SHALL be computed over a public key data object (cf. Section 4.5)
   * containing the object identifier as indicated in MSE:Set AT (cf. Section 3.2.1), and the
   * received ephemeral public key (i.e. excluding the domain parameters, cf. Section 4.5.3)
   * using an authentication code and the key KS MAC derived from the key agreement.
   *
   * @param oid the object identifier as indicated in MSE Set AT
   * @param macKey the KS MAC key derived from the key agreement
   * @param publicKey the received public key
   *
   * @return the authentication code
   *
   * @throws GeneralSecurityException on error while performing the MAC operation
   */
  public static byte[] generateAuthenticationToken(String oid, SecretKey macKey, PublicKey publicKey) throws GeneralSecurityException {
    String cipherAlg = PACEInfo.toCipherAlgorithm(oid);
    String macAlg = inferMACAlgorithmFromCipherAlgorithm(cipherAlg);
    Mac mac = Util.getMac(macAlg, macKey);
    return generateAuthenticationToken(oid, mac, publicKey);
  }

  /**
   * Computes a key seed given a card access number (CAN).
   *
   * @param cardAccessNumber the card access number
   *
   * @return a key seed for deriving secure messaging keys
   *
   * @throws GeneralSecurityException on error
   */
  public static byte[] computeKeySeedForPACE(String cardAccessNumber) throws GeneralSecurityException {
    return Util.computeKeySeed(cardAccessNumber, "SHA-1", false);
  }

  /**
   * Based on TR-SAC 1.01 4.5.1 and 4.5.2.
   *
   * For signing authentication token, not for sending to smart card.
   * Assumes context is known.
   *
   * @param oid object identifier
   * @param publicKey public key
   *
   * @return encoded public key data object for signing as authentication token
   *
   * @throws InvalidKeyException when public key is not DH or EC
   */
  public static byte[] encodePublicKeyDataObject(String oid, PublicKey publicKey) throws InvalidKeyException {
    return encodePublicKeyDataObject(oid, publicKey, true);
  }

  /**
   * Based on TR-SAC 1.01 4.5.1 and 4.5.2.
   *
   * For signing authentication token, not for sending to smart card.
   *
   * @param oid object identifier
   * @param publicKey public key
   * @param isContextKnown whether context of public key is known to receiver (we will not include domain parameters in that case).
   *
   * @return encoded public key data object for signing as authentication token
   *
   * @throws InvalidKeyException when public key is not DH or EC
   */
  public static byte[] encodePublicKeyDataObject(String oid, PublicKey publicKey, boolean isContextKnown) throws InvalidKeyException {
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    TLVOutputStream tlvOutputStream = new TLVOutputStream(byteArrayOutputStream);
    try {
      tlvOutputStream.writeTag(0x7F49); // FIXME: constant for 7F49 */
      if (publicKey instanceof DHPublicKey) {
        DHPublicKey dhPublicKey = (DHPublicKey)publicKey;
        DHParameterSpec params = dhPublicKey.getParams();
        BigInteger p = params.getP();
        int l = params.getL();
        BigInteger generator = params.getG();
        BigInteger y = dhPublicKey.getY();

        tlvOutputStream.write(new ASN1ObjectIdentifier(oid).getEncoded()); /* Object Identifier, NOTE: encoding already contains 0x06 tag  */
        if (!isContextKnown) {
          /* p: Prime modulus */
          tlvOutputStream.writeTag(0x81);
          tlvOutputStream.writeValue(Util.i2os(p));

          /* q: Order of the subgroup */
          tlvOutputStream.writeTag(0x82);
          tlvOutputStream.writeValue(Util.i2os(BigInteger.valueOf(l)));

          /* Generator */
          tlvOutputStream.writeTag(0x83);
          tlvOutputStream.writeValue(Util.i2os(generator));
        }

        /* y: Public value */
        tlvOutputStream.writeTag(0x84);
        tlvOutputStream.writeValue(Util.i2os(y));
      } else if (publicKey instanceof ECPublicKey) {
        ECPublicKey ecPublicKey = (ECPublicKey)publicKey;
        ECParameterSpec params = ecPublicKey.getParams();
        BigInteger p = Util.getPrime(params);
        EllipticCurve curve = params.getCurve();
        BigInteger a = curve.getA();
        BigInteger b = curve.getB();
        ECPoint generator = params.getGenerator();
        BigInteger order = params.getOrder();
        int coFactor = params.getCofactor();
        ECPoint publicPoint = ecPublicKey.getW();

        /* Object Identifier, NOTE: encoding already contains 0x06 tag */
        tlvOutputStream.write(new ASN1ObjectIdentifier(oid).getEncoded());

        if (!isContextKnown) {
          /* Prime modulus */
          tlvOutputStream.writeTag(0x81);
          tlvOutputStream.writeValue(Util.i2os(p));

          /* First coefficient */
          tlvOutputStream.writeTag(0x82);
          tlvOutputStream.writeValue(Util.i2os(a));

          /* Second coefficient */
          tlvOutputStream.writeTag(0x83);
          tlvOutputStream.writeValue(Util.i2os(b));
          BigInteger affineX = generator.getAffineX();
          BigInteger affineY = generator.getAffineY();

          /* Base point, FIXME: correct encoding? */
          tlvOutputStream.writeTag(0x84);
          tlvOutputStream.write(Util.i2os(affineX));
          tlvOutputStream.write(Util.i2os(affineY));
          tlvOutputStream.writeValueEnd();

          /* Order of the base point */
          tlvOutputStream.writeTag(0x85);
          tlvOutputStream.writeValue(Util.i2os(order));
        }

        /* Public point */
        tlvOutputStream.writeTag(0x86);
        tlvOutputStream.writeValue(Util.ecPoint2OS(publicPoint, params.getCurve().getField().getFieldSize()));

        if (!isContextKnown) {
          /* Cofactor */
          tlvOutputStream.writeTag(0x87);
          tlvOutputStream.writeValue(Util.i2os(BigInteger.valueOf(coFactor)));
        }
      } else {
        throw new InvalidKeyException("Unsupported public key: " + publicKey.getClass().getCanonicalName());
      }
      tlvOutputStream.writeValueEnd(); /* 7F49 */
      tlvOutputStream.flush();
    } catch (IOException ioe) {
      LOGGER.log(Level.WARNING, "Exception", ioe);
      throw new IllegalStateException("Error in encoding public key");
    } finally {
      try {
        tlvOutputStream.close();
      } catch (IOException ioe) {
        LOGGER.log(Level.FINE, "Error closing stream", ioe);
      }
    }
    return byteArrayOutputStream.toByteArray();
  }

  /**
   * Write uncompressed coordinates (for EC) or public value (DH).
   *
   * @param publicKey public key
   *
   * @return encoding for smart card
   *
   * @throws InvalidKeyException if the key type is not EC or DH
   */
  public static byte[] encodePublicKeyForSmartCard(PublicKey publicKey) throws InvalidKeyException {
    if (publicKey == null) {
      throw new IllegalArgumentException("Cannot encode null public key");
    }
    if (publicKey instanceof ECPublicKey) {
      ECPublicKey ecPublicKey = (ECPublicKey)publicKey;
      try {
        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
        bOut.write(Util.ecPoint2OS(ecPublicKey.getW(), ecPublicKey.getParams().getCurve().getField().getFieldSize()));
        byte[] encodedPublicKey = bOut.toByteArray();
        bOut.close();
        return encodedPublicKey;
      } catch (IOException ioe) {
        /* NOTE: Should never happen, we're writing to a ByteArrayOutputStream. */
        throw new IllegalStateException("Internal error writing to memory", ioe);
      }
    } else if (publicKey instanceof DHPublicKey) {
      DHPublicKey dhPublicKey = (DHPublicKey)publicKey;
      return Util.i2os(dhPublicKey.getY());
    } else {
      throw new InvalidKeyException("Unsupported public key: " + publicKey.getClass().getCanonicalName());
    }
  }

  /**
   * Decodes a public key received from the PICC.
   *
   * @param encodedPublicKey the encoded public key that was received
   * @param params the parameters used for interpreting the public key
   *
   * @return the decoded public key object
   */
  public static PublicKey decodePublicKeyFromSmartCard(byte[] encodedPublicKey, AlgorithmParameterSpec params) {
    if (params == null) {
      throw new IllegalArgumentException("Params cannot be null");
    }

    try {
      if (params instanceof ECParameterSpec) {
        ECPoint w = Util.os2ECPoint(encodedPublicKey);
        ECParameterSpec ecParams = (ECParameterSpec)params;
        return Util.getPublicKey("EC", new ECPublicKeySpec(w, ecParams));
      } else if (params instanceof DHParameterSpec) {
        BigInteger y = Util.os2i(encodedPublicKey);
        DHParameterSpec dhParams = (DHParameterSpec)params;
        return Util.getPublicKey("DH", new DHPublicKeySpec(y, dhParams.getP(), dhParams.getG()));
      }

      throw new IllegalArgumentException("Expected ECParameterSpec or DHParameterSpec, found " + params.getClass().getCanonicalName());
    } catch (GeneralSecurityException gse) {
      LOGGER.log(Level.WARNING, "Exception", gse);
      throw new IllegalArgumentException(gse);
    }
  }

  /**
   * Generates an authentication token.
   *
   * @param oid the object identifier as indicated in MSE Set AT
   * @param mac the MAC which has already been initialized with the MAC key derived from key agreement
   * @param publicKey the received public key
   *
   * @return the authentication token
   *
   * @throws GeneralSecurityException on error while performing the MAC operation
   */
  private static byte[] generateAuthenticationToken(String oid, Mac mac, PublicKey publicKey) throws GeneralSecurityException {
    byte[] encodedPublicKeyDataObject = encodePublicKeyDataObject(oid, publicKey);
    byte[] maccedPublicKeyDataObject = mac.doFinal(encodedPublicKeyDataObject);

    /* Output length needs to be 64 bits, copy first 8 bytes. */
    byte[] authenticationToken = new byte[8];
    System.arraycopy(maccedPublicKeyDataObject, 0, authenticationToken, 0, authenticationToken.length);
    return authenticationToken;
  }

  /**
   * Fixes the document number so that it is in MRZ format.
   * This replaces white spaces with fillers and
   * makes sure the length is at least 9.
   *
   * @param documentNumber the document number
   *
   * @return a fixed document number
   */
  private static String fixDocumentNumber(String documentNumber) {

    /* The document number, excluding trailing '<'. */
    String minDocumentNumber = documentNumber.replace('<', ' ').trim().replace(' ', '<');

    /* The document number, including trailing '<' until length 9. */
    StringBuilder result = new StringBuilder(minDocumentNumber);
    while (result.length() < 9) {
      result.append('<');
    }
    return result.toString();
  }

  /**
   * Computes the static key seed to be used in PACE KDF, based on information from the MRZ.
   *
   * @param documentNumber a string containing the document number
   * @param dateOfBirth a string containing the date of birth (YYMMDD)
   * @param dateOfExpiry a string containing the date of expiry (YYMMDD)
   *
   * @return a byte array of length 16 containing the key seed
   *
   * @throws GeneralSecurityException on security error
   */
  private static byte[] computeKeySeedForPACE(String documentNumber, String dateOfBirth, String dateOfExpiry) throws GeneralSecurityException {
    return Util.computeKeySeed(documentNumber, dateOfBirth, dateOfExpiry, "SHA-1", false);
  }

  /**
   * Compares two keys, taking into account that an exception might
   * be thrown doing so. Returns {@code false} if an exception is thrown.
   *
   * @param first the first key
   * @param second the second key
   *
   * @return a boolean indicating equivalence of the two keys
   */
  private static boolean keysAreEqual(PublicKey first, PublicKey second) {
    try {
      return first.equals(second);
    } catch (RuntimeException e) {
      LOGGER.log(Level.WARNING, "Exception during public key equals", e);
      return false;
    }
  }

  /**
   * Checks consistency of input parameters.
   *
   * @param agreementAlg the agreement algorithm derived from the object identifier
   * @param cipherAlg the cipher algorithm derived from the object identifier
   * @param digestAlg the digest algorithm derived from the object identifier
   * @param keyLength the key length algorithm derived from the object identifier
   * @param params the parameters
   */
  private void checkConsistency(String agreementAlg, String cipherAlg, String digestAlg, int keyLength, AlgorithmParameterSpec params) {
    if (agreementAlg == null) {
      throw new IllegalArgumentException("Unknown agreement algorithm");
    }

    /* Agreement algorithm should be ECDH or DH. */
    if (!("ECDH".equalsIgnoreCase(agreementAlg) || "DH".equalsIgnoreCase(agreementAlg))) {
      throw new IllegalArgumentException("Unsupported agreement algorithm, expected \"ECDH\" or \"DH\", found \"" + agreementAlg + "\"");
    }

    if (cipherAlg == null) {
      throw new IllegalArgumentException("Unknown cipher algorithm");
    }

    if (!("DESede".equalsIgnoreCase(cipherAlg) || "AES".equalsIgnoreCase(cipherAlg))) {
      throw new IllegalArgumentException("Unsupported cipher algorithm, expected \"DESede\" or \"AES\", found \"" + cipherAlg + "\"");
    }

    if (!("SHA-1".equalsIgnoreCase(digestAlg) || "SHA1".equalsIgnoreCase(digestAlg)
        || "SHA-256".equalsIgnoreCase(digestAlg) || "SHA256".equalsIgnoreCase(digestAlg))) {
      throw new IllegalArgumentException("Unsupported cipher algorithm, expected \"SHA-1\" or \"SHA-256\", found \"" + digestAlg + "\"");
    }

    if (!(keyLength == 128 || keyLength == 192 || keyLength == 256)) {
      throw new IllegalArgumentException("Unsupported key length, expected 128, 192, or 256, found " + keyLength);
    }

    /* Params should be correct param spec type, given agreement algorithm. */
    if ("ECDH".equalsIgnoreCase(agreementAlg) && !(params instanceof ECParameterSpec)) {
      throw new IllegalArgumentException("Expected ECParameterSpec for agreement algorithm \"" + agreementAlg + "\", found " + params.getClass().getCanonicalName());
    } else if ("DH".equalsIgnoreCase(agreementAlg) && !(params instanceof DHParameterSpec)) {
      throw new IllegalArgumentException("Expected DHParameterSpec for agreement algorithm \"" + agreementAlg + "\", found " + params.getClass().getCanonicalName());
    }
  }

  /**
   * Infers a MAC algorithm given a encryption algorithm.
   *
   * @param cipherAlg the encryption algorithm.
   * If 3-DES is used for encryption, then the MAC algorithm is ISO9797 algorithm 3.
   * If AES is used for encryption, the the MAC algorithm is AES-CMAC.
   *
   * @return the MAC algorithm
   *
   * @throws InvalidAlgorithmParameterException for unknown encryption algorithm
   */
  private static String inferMACAlgorithmFromCipherAlgorithm(String cipherAlg) throws InvalidAlgorithmParameterException {
    if (cipherAlg == null) {
      throw new IllegalArgumentException("Cannot infer MAC algorithm from cipher algorithm null");
    }

    /*
     * NOTE: AESCMAC will generate 128 bit (16 byte) results, not 64 bit (8 byte),
     * both authentication token generation and secure messaging,
     * where the Mac is applied, will copy only the first 8 bytes.
     */
    if (cipherAlg.startsWith("DESede")) {
      /* NOTE: Some options to be considered here:
       *  - "DESedeMac" (not sure if similar to any of the below options)
       *  - "ISO9797Alg3Mac" (the same we use for BAC based secure messaging)
       *  - "ISO9797ALG3WITHISO7816-4PADDING" (this one was suggested Michal Iwanicki of Decatur Ltd. Thanks!)
       */
      return "ISO9797ALG3WITHISO7816-4PADDING";
    } else if (cipherAlg.startsWith("AES")) {
      return "AESCMAC";
    } else {
      throw new InvalidAlgorithmParameterException("Cannot infer MAC algorithm from cipher algorithm \"" + cipherAlg + "\"");
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy