/*
 *
 * Copyright (c) 2013 - 2020 Lijun Liao
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.xipki.security.pkcs11.proxy;

import java.io.IOException;
import java.math.BigInteger;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.bouncycastle.asn1.ASN1Boolean;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Enumerated;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1String;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERTaggedObject;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.util.Arrays;
import org.xipki.security.BadAsn1ObjectException;
import org.xipki.security.X509Cert;
import org.xipki.security.pkcs11.P11IdentityId;
import org.xipki.security.pkcs11.P11ObjectIdentifier;
import org.xipki.security.pkcs11.P11Params.P11RSAPkcsPssParams;
import org.xipki.security.pkcs11.P11Slot.P11KeyUsage;
import org.xipki.security.pkcs11.P11Slot.P11NewKeyControl;
import org.xipki.security.pkcs11.P11Slot.P11NewObjectControl;
import org.xipki.security.pkcs11.P11SlotIdentifier;
import org.xipki.util.Args;
import org.xipki.util.CollectionUtil;
import org.xipki.util.StringUtil;

/**
 * ASN.1 Messages communicated between the PKCS#11 proxy client and server.
 *
 * @author Lijun Liao
 * @since 2.0.0
 */
public abstract class ProxyMessage extends ASN1Object {

  /**
   * Parameters to add certificate.
   *
   * <pre>
   * AddCertParams ::= SEQUENCE {
   *     slotId               P11SlotIdentifier,
   *     control              NewKeyControl,
   *     certificate          Certificate }
   * </pre>
   */
  public static class AddCertParams extends ProxyMessage {

    private final P11SlotIdentifier slotId;

    private final P11NewObjectControl control;

    private final Certificate certificate;

    public AddCertParams(P11SlotIdentifier slotId, P11NewObjectControl control,
        Certificate certificate) {
      this.slotId = Args.notNull(slotId, "slotId");
      this.control = Args.notNull(control, "control");
      this.certificate = Args.notNull(certificate, "certificate");
    }

    public AddCertParams(P11SlotIdentifier slotId, P11NewObjectControl control,
        X509Cert certificate) {
      this.slotId = Args.notNull(slotId, "slotId");
      this.control = Args.notNull(control, "control");
      Args.notNull(certificate, "certificate");
      this.certificate = certificate.toBcCert().toASN1Structure();
    }

    private AddCertParams(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 3, 3);
      int idx = 0;
      slotId = SlotIdentifier.getInstance(seq.getObjectAt(idx++)).getValue();
      control = NewKeyControl.getInstance(seq.getObjectAt(idx++)).getControl();
      this.certificate = getCertificate0(seq.getObjectAt(idx++));
    }

    public static AddCertParams getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof AddCertParams) {
        return (AddCertParams) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new AddCertParams((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(new SlotIdentifier(slotId));
      vector.add(new NewObjectControl(control));
      vector.add(certificate);
      return new DERSequence(vector);
    }

    public P11SlotIdentifier getSlotId() {
      return slotId;
    }

    public P11NewObjectControl getControl() {
      return control;
    }

    public Certificate getCertificate() {
      return certificate;
    }

  } // class AddCertParams

  /**
   * Definition of DigestSecretKeyTemplate.
   *
   * <pre>
   * DigestSecretKeyTemplate ::= SEQUENCE {
   *     slotId         SlotIdentifier,
   *     objectId       ObjectIdentifier,
   *     mechanism      Mechanism}
   * </pre>
   */
  public static class DigestSecretKeyTemplate extends ProxyMessage {

    private final SlotIdentifier slotId;

    private final ObjectIdentifier objectId;

    private final Mechanism mechanism;

    private DigestSecretKeyTemplate(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 3, 3);
      int idx = 0;
      this.slotId = SlotIdentifier.getInstance(seq.getObjectAt(idx++));
      this.objectId = ObjectIdentifier.getInstance(seq.getObjectAt(idx++));
      this.mechanism = Mechanism.getInstance(seq.getObjectAt(idx++));
    }

    public DigestSecretKeyTemplate(SlotIdentifier slotId, ObjectIdentifier objectId,
        long mechanism) {
      this.slotId = Args.notNull(slotId, "slotId");
      this.objectId = Args.notNull(objectId, "objectId");
      this.mechanism = new Mechanism(mechanism, null);
    }

    public static DigestSecretKeyTemplate getInstance(Object obj)
        throws BadAsn1ObjectException {
      if (obj == null || obj instanceof DigestSecretKeyTemplate) {
        return (DigestSecretKeyTemplate) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new DigestSecretKeyTemplate((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(slotId);
      vector.add(objectId);
      vector.add(mechanism);
      return new DERSequence(vector);
    }

    public SlotIdentifier getSlotId() {
      return slotId;
    }

    public ObjectIdentifier getObjectId() {
      return objectId;
    }

    public Mechanism getMechanism() {
      return mechanism;
    }
  } // class DigestSecretKeyTemplate

  /**
   * Parameters to generate RSA keypair.
   *
   * <pre>
   * GenRSAKeypairParams ::= SEQUENCE {
   *     slotId               P11SlotIdentifier,
   *     control              NewKeyControl,
   *     p                    INTEGER,
   *     q                    INTEGER,
   *     g                    INTEGER}
   * </pre>
   */
  // CHECKSTYLE:SKIP
  public static class GenDSAKeypairParams extends ProxyMessage {

    private final P11SlotIdentifier slotId;

    private final P11NewKeyControl control;

    private final BigInteger p; // CHECKSTYLE:SKIP

    private final BigInteger q; // CHECKSTYLE:SKIP

    private final BigInteger g; // CHECKSTYLE:SKIP

    public GenDSAKeypairParams(P11SlotIdentifier slotId, P11NewKeyControl control,
        BigInteger p, BigInteger q, BigInteger g) {
      this.slotId = Args.notNull(slotId, "slotId");
      this.control = Args.notNull(control, "control");
      this.p = Args.notNull(p, "p");
      this.q = Args.notNull(q, "q");
      this.g = Args.notNull(g, "g");
    }

    private GenDSAKeypairParams(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 5, 5);
      int idx = 0;
      slotId = SlotIdentifier.getInstance(seq.getObjectAt(idx++)).getValue();
      control = NewKeyControl.getInstance(seq.getObjectAt(idx++)).getControl();
      p = getInteger(seq.getObjectAt(idx++));
      q = getInteger(seq.getObjectAt(idx++));
      g = getInteger(seq.getObjectAt(idx++));
    }

    public static GenDSAKeypairParams getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof GenDSAKeypairParams) {
        return (GenDSAKeypairParams) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new GenDSAKeypairParams((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(new SlotIdentifier(slotId));
      vector.add(new NewKeyControl(control));
      vector.add(new ASN1Integer(p));
      vector.add(new ASN1Integer(q));
      vector.add(new ASN1Integer(g));
      return new DERSequence(vector);
    }

    public P11SlotIdentifier getSlotId() {
      return slotId;
    }

    public P11NewKeyControl getControl() {
      return control;
    }

    public BigInteger getP() {
      return p;
    }

    public BigInteger getQ() {
      return q;
    }

    public BigInteger getG() {
      return g;
    }

  } // class GenDSAKeypairParams

  /**
   * Parameters to generate EC keypair.
   *
   * <pre>
   * GenECKeypairParams ::= SEQUENCE {
   *     slotId               P11SlotIdentifier,
   *     control              NewKeyControl,
   *     curveId              OBJECT IDENTIFIER }
   * </pre>
   */
  // CHECKSTYLE:SKIP
  public static class GenECKeypairParams extends ProxyMessage {

    private final P11SlotIdentifier slotId;

    private final P11NewKeyControl control;

    private final ASN1ObjectIdentifier curveId;

    public GenECKeypairParams(P11SlotIdentifier slotId,
        P11NewKeyControl control, ASN1ObjectIdentifier curveId) {
      this.slotId = Args.notNull(slotId, "slotId");
      this.control = Args.notNull(control, "control");
      this.curveId = Args.notNull(curveId, "curveId");
    }

    private GenECKeypairParams(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 3, 3);
      int idx = 0;
      slotId = SlotIdentifier.getInstance(seq.getObjectAt(idx++)).getValue();
      control = NewKeyControl.getInstance(seq.getObjectAt(idx++)).getControl();
      curveId = getObjectIdentifier(seq.getObjectAt(idx++));
    }

    public static GenECKeypairParams getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof GenECKeypairParams) {
        return (GenECKeypairParams) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new GenECKeypairParams((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(new SlotIdentifier(slotId));
      vector.add(new NewKeyControl(control));
      vector.add(curveId);
      return new DERSequence(vector);
    }

    public P11SlotIdentifier getSlotId() {
      return slotId;
    }

    public P11NewKeyControl getControl() {
      return control;
    }

    public ASN1ObjectIdentifier getCurveId() {
      return curveId;
    }

  } // class GenECKeypairParams

  /**
   * Paramters to generate EC keypair.
   *
   * <pre>
   * GenECKeypairParams ::= SEQUENCE {
   *     slotId               P11SlotIdentifier,
   *     control              NewKeyControl,
   *     ASN1ObjectIdentifier CurveOid }
   * </pre>
   */
  // CHECKSTYLE:SKIP
  public static class GenECEdwardsOrMontgomeryKeypairParams extends ProxyMessage {

    private final P11SlotIdentifier slotId;

    private final P11NewKeyControl control;

    private final ASN1ObjectIdentifier curveOid;

    public GenECEdwardsOrMontgomeryKeypairParams(P11SlotIdentifier slotId,
        P11NewKeyControl control, ASN1ObjectIdentifier curveOid) {
      this.slotId = Args.notNull(slotId, "slotId");
      this.control = Args.notNull(control, "control");
      this.curveOid = Args.notNull(curveOid, "curveOid");
    }

    private GenECEdwardsOrMontgomeryKeypairParams(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 3, 3);
      int idx = 0;
      slotId = SlotIdentifier.getInstance(seq.getObjectAt(idx++)).getValue();
      control = NewKeyControl.getInstance(seq.getObjectAt(idx++)).getControl();
      curveOid = ASN1ObjectIdentifier.getInstance(seq.getObjectAt(idx++));
    }

    public static GenECEdwardsOrMontgomeryKeypairParams getInstance(Object obj)
        throws BadAsn1ObjectException {
      if (obj == null || obj instanceof GenECEdwardsOrMontgomeryKeypairParams) {
        return (GenECEdwardsOrMontgomeryKeypairParams) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new GenECEdwardsOrMontgomeryKeypairParams((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(new SlotIdentifier(slotId));
      vector.add(new NewKeyControl(control));
      vector.add(curveOid);
      return new DERSequence(vector);
    }

    public P11SlotIdentifier getSlotId() {
      return slotId;
    }

    public P11NewKeyControl getControl() {
      return control;
    }

    public ASN1ObjectIdentifier getCurveOid() {
      return curveOid;
    }

  } // class GenECEdwardsOrMontgomeryKeypairParams

  /**
   * Parameters to generate RSA keypair.
   *
   * <pre>
   * GenRSAKeypairParams ::= SEQUENCE {
   *     slotId               P11SlotIdentifier,
   *     control              NewKeyControl,
   *     keysize              INTEGER,
   *     publicExponent       INTEGER OPTIONAL }
   * </pre>
   */
  // CHECKSTYLE:SKIP
  public static class GenRSAKeypairParams extends ProxyMessage {

    private final P11SlotIdentifier slotId;

    private final P11NewKeyControl control;

    private final int keysize;

    private final BigInteger publicExponent;

    public GenRSAKeypairParams(P11SlotIdentifier slotId,
        P11NewKeyControl control, int keysize, BigInteger publicExponent) {
      this.slotId = Args.notNull(slotId, "slotId");
      this.control = Args.notNull(control, "control");
      this.keysize = Args.min(keysize, "keysize", 1);
      this.publicExponent = publicExponent;
    }

    private GenRSAKeypairParams(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 3, 4);
      final int size = seq.size();
      int idx = 0;
      slotId = SlotIdentifier.getInstance(seq.getObjectAt(idx++)).getValue();
      control = NewKeyControl.getInstance(seq.getObjectAt(idx++)).getControl();
      keysize = getInteger(seq.getObjectAt(idx++)).intValue();
      Args.min(keysize, "keysize", 1);

      publicExponent = (size > 3) ? getInteger(seq.getObjectAt(idx++)) : null;
    }

    public static GenRSAKeypairParams getInstance(Object obj)
        throws BadAsn1ObjectException {
      if (obj == null || obj instanceof GenRSAKeypairParams) {
        return (GenRSAKeypairParams) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new GenRSAKeypairParams((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(new SlotIdentifier(slotId));
      vector.add(new NewKeyControl(control));
      vector.add(new ASN1Integer(keysize));
      if (publicExponent != null) {
        vector.add(new ASN1Integer(publicExponent));
      }
      return new DERSequence(vector);
    }

    public P11SlotIdentifier getSlotId() {
      return slotId;
    }

    public P11NewKeyControl getControl() {
      return control;
    }

    public int getKeysize() {
      return keysize;
    }

    public BigInteger getPublicExponent() {
      return publicExponent;
    }

  } // class GenRSAKeypairParams

  /**
   * Parameters to generate secret key.
   *
   * <pre>
   * GenSecretKeyParams ::= SEQUENCE {
   *     slotId               P11SlotIdentifier,
   *     control              NewKeyControl,
   *     keyType              INTEGER,
   *     keysize              INTEGER }
   * </pre>
   */
  public static class GenSecretKeyParams extends ProxyMessage {

    private final P11SlotIdentifier slotId;

    private final P11NewKeyControl control;

    private final long keyType;

    private final int keysize;

    public GenSecretKeyParams(P11SlotIdentifier slotId, P11NewKeyControl control, long keyType,
        int keysize) {
      this.slotId = Args.notNull(slotId, "slotId");
      this.control = Args.notNull(control, "control");
      this.keyType = keyType;
      this.keysize = Args.min(keysize, "keysize", 1);
    }

    private GenSecretKeyParams(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 4, 4);
      int idx = 0;
      slotId = SlotIdentifier.getInstance(seq.getObjectAt(idx++)).getValue();
      control = NewKeyControl.getInstance(seq.getObjectAt(idx++)).getControl();
      keyType = getInteger(seq.getObjectAt(idx++)).longValue();
      keysize = getInteger(seq.getObjectAt(idx++)).intValue();
      Args.min(keysize, "keysize", 1);
    }

    public static GenSecretKeyParams getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof GenSecretKeyParams) {
        return (GenSecretKeyParams) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new GenSecretKeyParams((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(new SlotIdentifier(slotId));
      vector.add(new NewKeyControl(control));
      vector.add(new ASN1Integer(keyType));
      vector.add(new ASN1Integer(keysize));
      return new DERSequence(vector);
    }

    public P11SlotIdentifier getSlotId() {
      return slotId;
    }

    public P11NewKeyControl getControl() {
      return control;
    }

    public long getKeyType() {
      return keyType;
    }

    public int getKeysize() {
      return keysize;
    }

  } // class GenSecretKeyParams

  /**
   * Parameters to generate SM2 keypair.
   *
   * <pre>
   * GenSM2KeypairParams ::= SEQUENCE {
   *     slotId               P11SlotIdentifier,
   *     control              NewKeyControl }
   * </pre>
   */
  // CHECKSTYLE:SKIP
  public static class GenSM2KeypairParams extends ProxyMessage {

    private final P11SlotIdentifier slotId;

    private final P11NewKeyControl control;

    public GenSM2KeypairParams(P11SlotIdentifier slotId, P11NewKeyControl control) {
      this.slotId = Args.notNull(slotId, "slotId");
      this.control = Args.notNull(control, "control");
    }

    private GenSM2KeypairParams(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 2, 2);
      int idx = 0;
      slotId = SlotIdentifier.getInstance(seq.getObjectAt(idx++)).getValue();
      control = NewKeyControl.getInstance(seq.getObjectAt(idx++)).getControl();
    }

    public static GenSM2KeypairParams getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof GenSM2KeypairParams) {
        return (GenSM2KeypairParams) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new GenSM2KeypairParams((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(new SlotIdentifier(slotId));
      vector.add(new NewKeyControl(control));
      return new DERSequence(vector);
    }

    public P11SlotIdentifier getSlotId() {
      return slotId;
    }

    public P11NewKeyControl getControl() {
      return control;
    }

  } // method GenSM2KeypairParams

  /**
   * Parameters to import secret key.
   *
   * <pre>
   * ImportSecretKeyParams ::= SEQUENCE {
   *     slotId               P11SlotIdentifier,
   *     control              NewKeyControl,
   *     keyType              INTEGER,
   *     keyValue             OCTET STRING}
   * </pre>
   */
  // CHECKSTYLE:SKIP
  public static class ImportSecretKeyParams extends ProxyMessage {

    private final P11SlotIdentifier slotId;

    private final P11NewKeyControl control;

    private final long keyType;

    private final byte[] keyValue;

    public ImportSecretKeyParams(P11SlotIdentifier slotId,
        P11NewKeyControl control, long keyType, byte[] keyValue) {
      this.slotId = Args.notNull(slotId, "slotId");
      this.control = Args.notNull(control, "control");
      this.keyType = keyType;
      this.keyValue = Args.notNull(keyValue, "keyValue");
    }

    private ImportSecretKeyParams(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 4, 4);
      int idx = 0;
      slotId = SlotIdentifier.getInstance(seq.getObjectAt(idx++)).getValue();
      control = NewKeyControl.getInstance(seq.getObjectAt(idx++)).getControl();
      keyType = getInteger(seq.getObjectAt(idx++)).longValue();
      keyValue = ASN1OctetString.getInstance(seq.getObjectAt(idx++)).getOctets();
    }

    public static ImportSecretKeyParams getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof ImportSecretKeyParams) {
        return (ImportSecretKeyParams) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new ImportSecretKeyParams((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(new SlotIdentifier(slotId));
      vector.add(new ASN1Integer(keyType));
      vector.add(new DEROctetString(keyValue));
      return new DERSequence(vector);
    }

    public P11SlotIdentifier getSlotId() {
      return slotId;
    }

    public P11NewKeyControl getControl() {
      return control;
    }

    public long getKeyType() {
      return keyType;
    }

    public byte[] getKeyValue() {
      return Arrays.copyOf(keyValue, keyValue.length);
    }

  } // method ImportSecretKeyParams

  /**
   * Definition of Mechanism.
   *
   * <pre>
   * Mechanism ::= SEQUENCE {
   *     mechanism     INTEGER,
   *     params        P11Params OPTIONAL }
   * </pre>
   */
  public static class Mechanism extends ProxyMessage {

    private final long mechanism;

    private final P11Params params;

    public Mechanism(long mechanism, P11Params params) {
      this.mechanism = mechanism;
      this.params = params;
    }

    private Mechanism(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 1, 2);
      int size = seq.size();
      int idx = 0;
      this.mechanism = getInteger(seq.getObjectAt(idx++)).longValue();
      this.params = (size > 1)  ? P11Params.getInstance(seq.getObjectAt(idx++)) : null;
    }

    public static Mechanism getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof Mechanism) {
        return (Mechanism) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new Mechanism((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(new ASN1Integer(mechanism));
      if (params != null) {
        vector.add(params);
      }
      return new DERSequence(vector);
    }

    public long getMechanism() {
      return mechanism;
    }

    public P11Params getParams() {
      return params;
    }

  } // method Mechanism

  /**
   * Control how to create new PKCS#11 keypair / secret key.
   *
   * <pre>
   * NewKeyControl ::= SEQUENCE {
   *     label                  UTF8 STRING,
   *     id                 [0] OCTET STRING OPTIONAL,
   *     keyUsages          [1] SEQUENCE OF P11KEYUSAGE OPTIONAL,
   *     extractable        [2] EXPLICIT BOOLEAN OPTIONAL }
   *
   * P11KEYUSAGE ::= ENUMERATED {
   *       DECRYPT         (0),
   *       DERIVE          (1),
   *       SIGN            (2),
   *       SIGN_RECOVER    (3),
   *       UNWRAP          (4)}
   * </pre>
   */
  public static class NewKeyControl extends ProxyMessage {

    private static final Map<Integer, P11KeyUsage> valueToUsageMap;

    private static final Map<P11KeyUsage, Integer> usageToValueMap;

    private final P11NewKeyControl control;

    static {
      valueToUsageMap = new HashMap<>(10);
      valueToUsageMap.put(0, P11KeyUsage.DECRYPT);
      valueToUsageMap.put(1, P11KeyUsage.DERIVE);
      valueToUsageMap.put(2, P11KeyUsage.SIGN);
      valueToUsageMap.put(3, P11KeyUsage.SIGN_RECOVER);
      valueToUsageMap.put(4, P11KeyUsage.UNWRAP);

      usageToValueMap = new HashMap<>(10);
      for (Integer value : valueToUsageMap.keySet()) {
        P11KeyUsage usage = valueToUsageMap.get(value);
        usageToValueMap.put(usage, value);
      }
    } // method static

    public NewKeyControl(P11NewKeyControl control) {
      this.control = Args.notNull(control, "control");
    }

    private NewKeyControl(ASN1Sequence seq) throws BadAsn1ObjectException {
      final int size = seq.size();
      Args.min(size, "seq.size", 1);
      String label = DERUTF8String.getInstance(seq.getObjectAt(0)).getString();

      Set<P11KeyUsage> usages = new HashSet<>();
      byte[] id = null;
      Boolean extractable = null;

      for (int i = 1; i < size; i++) {
        ASN1Encodable obj = seq.getObjectAt(i);
        if (!(obj instanceof ASN1TaggedObject)) {
          continue;
        }

        ASN1TaggedObject tagObj = (ASN1TaggedObject) obj;
        int tagNo = tagObj.getTagNo();
        if (tagNo == 0) {
          id = DEROctetString.getInstance(tagObj.getObject()).getOctets();
        } else if (tagNo == 1) {
          ASN1Sequence usageSeq = ASN1Sequence.getInstance(tagObj.getObject());
          final int usageSize = usageSeq.size();
          for (int j = 0; j < usageSize; j++) {
            ASN1Enumerated usageEnum = ASN1Enumerated.getInstance(usageSeq.getObjectAt(j));
            int enumValue = usageEnum.getValue().intValue();
            P11KeyUsage usage = valueToUsageMap.get(enumValue);
            if (usage == null) {
              throw new IllegalArgumentException("invalid usage " + enumValue);
            }
            usages.add(usage);
          }
        } else if (tagNo == 2) {
          extractable = ASN1Boolean.getInstance(tagObj.getObject()).isTrue();
        }
      }

      this.control = new P11NewKeyControl(id, label);
      this.control.setUsages(usages);
      this.control.setExtractable(extractable);
    } // constructor

    public static NewKeyControl getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof NewKeyControl) {
        return (NewKeyControl) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new NewKeyControl((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(new DERUTF8String(control.getLabel()));

      byte[] id = control.getId();
      if (id != null) {
        vector.add(new DERTaggedObject(0, new DEROctetString(id)));
      }

      Set<P11KeyUsage> usages = control.getUsages();
      if (CollectionUtil.isNotEmpty(usages)) {
        ASN1EncodableVector asn1Usages = new ASN1EncodableVector();
        for (P11KeyUsage usage : usages) {
          int value = usageToValueMap.get(usage);
          asn1Usages.add(new ASN1Enumerated(value));
        }
        vector.add(new DERTaggedObject(1, new DERSequence(asn1Usages)));
      }

      if (control.getExtractable() != null) {
        vector.add(new DERTaggedObject(2, ASN1Boolean.getInstance(control.getExtractable())));
      }

      return new DERSequence(vector);
    }

    public P11NewKeyControl getControl() {
      return control;
    }

  } // class NewKeyControl

  /**
   * Control how to create new PKCS#11 object.
   *
   * <pre>
   * NewKeyControl ::= SEQUENCE {
   *     label                  UTF8 STRING,
   *     id                 [0] OCTET STRING OPTIONAL }
   * </pre>
   */
  public static class NewObjectControl extends ProxyMessage {

    private final P11NewObjectControl control;

    public NewObjectControl(P11NewObjectControl control) {
      this.control = Args.notNull(control, "control");
    }

    private NewObjectControl(ASN1Sequence seq) throws BadAsn1ObjectException {
      final int size = seq.size();
      Args.min(size, "seq.size", 1);
      String label = DERUTF8String.getInstance(seq.getObjectAt(0)).getString();
      byte[] id = null;

      for (int i = 1; i < size; i++) {
        ASN1Encodable obj = seq.getObjectAt(i);
        if (obj instanceof ASN1TaggedObject) {
          continue;
        }

        ASN1TaggedObject tagObj = (ASN1TaggedObject) obj;
        int tagNo = tagObj.getTagNo();
        if (tagNo == 0) {
          id = DEROctetString.getInstance(tagObj.getObject()).getOctets();
        }
      }

      this.control = new P11NewKeyControl(id, label);
    }

    public static NewObjectControl getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof NewObjectControl) {
        return (NewObjectControl) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new NewObjectControl((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(new DERUTF8String(control.getLabel()));

      byte[] id = control.getId();
      if (id != null) {
        vector.add(new DERTaggedObject(0, new DEROctetString(id)));
      }

      return new DERSequence(vector);
    }

    public P11NewObjectControl getControl() {
      return control;
    }

  } // class NewObjectControl

  /**
   * Definition of ObjectIdAndCert.
   *
   * <pre>
   * ObjectIdAndCert ::= SEQUENCE {
   *     slotId         SlotIdentifier,
   *     objectId       ObjectIdentifier,
   *     certificate    Certificate }
   * </pre>
   */
  public static class ObjectIdAndCert extends ProxyMessage {

    private final SlotIdentifier slotId;

    private final ObjectIdentifier objectId;

    private final Certificate certificate;

    public ObjectIdAndCert(SlotIdentifier slotId, ObjectIdentifier objectId,
        Certificate certificate) {
      this.slotId = Args.notNull(slotId, "slotId");
      this.objectId = Args.notNull(objectId, "objectId");
      this.certificate = Args.notNull(certificate, "certificate");
    }

    public ObjectIdAndCert(SlotIdentifier slotId, ObjectIdentifier objectId,
        X509Cert certificate) {
      this.slotId = Args.notNull(slotId, "slotId");
      this.objectId = Args.notNull(objectId, "objectId");
      Args.notNull(certificate, "certificate");
      this.certificate = certificate.toBcCert().toASN1Structure();
    }

    private ObjectIdAndCert(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 3, 3);
      int idx = 0;
      this.slotId = SlotIdentifier.getInstance(seq.getObjectAt(idx++));
      this.objectId = ObjectIdentifier.getInstance(seq.getObjectAt(idx++));
      this.certificate = getCertificate0(seq.getObjectAt(idx++));
    }

    public static ObjectIdAndCert getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof ObjectIdAndCert) {
        return (ObjectIdAndCert) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new ObjectIdAndCert((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(slotId);
      vector.add(objectId);
      vector.add(certificate);
      return new DERSequence(vector);
    }

    public SlotIdentifier getSlotId() {
      return slotId;
    }

    public ObjectIdentifier getObjectId() {
      return objectId;
    }

    public Certificate getCertificate() {
      return certificate;
    }

  } // class ObjectIdAndCert

  /**
   * Identifier of the PKCS#11 identity.
   *
   * <pre>
   * IdentityIdentifer ::= SEQUENCE {
   *     slotId              SlotIdentifier,
   *     keyId               ObjectIdentifier,
   *     publicKeyLabel  [1] UTF8 STRING OPTIONAL,
   *     certLabel       [2] UTF8 STRING OPTIONAL }
   * </pre>
   */
  public static class IdentityId extends ProxyMessage {

    private final P11IdentityId value;

    public IdentityId(P11IdentityId value) {
      this.value = Args.notNull(value, "value");
    }

    private IdentityId(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 2, 4);
      P11SlotIdentifier slotId =
          SlotIdentifier.getInstance(seq.getObjectAt(0)).getValue();
      P11ObjectIdentifier keyId =
          ObjectIdentifier.getInstance(seq.getObjectAt(1)).getValue();
      String publicKeyLabel = null;
      String certLabel = null;

      final int n = seq.size();
      for (int i = 2; i < n; i++) {
        ASN1Encodable asn1 = seq.getObjectAt(i);
        if (asn1 instanceof ASN1TaggedObject) {
          ASN1TaggedObject tagAsn1 = (ASN1TaggedObject) asn1;
          int tag = tagAsn1.getTagNo();
          if (tag == 1) {
            publicKeyLabel = DERUTF8String.getInstance(tagAsn1.getObject()).getString();
          } else if (tag == 2) {
            certLabel = DERUTF8String.getInstance(tagAsn1.getObject()).getString();
          }
        }

      }

      this.value = new P11IdentityId(slotId, keyId, publicKeyLabel, certLabel);
    }

    public static IdentityId getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof IdentityId) {
        return (IdentityId) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new IdentityId((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(new SlotIdentifier(value.getSlotId()));
      vector.add(new ObjectIdentifier(value.getKeyId()));

      if (value.getPublicKeyId() != null) {
        String label = value.getPublicKeyId().getLabel();
        vector.add(new DERTaggedObject(true, 1, new DERUTF8String(label)));
      }

      if (value.getCertId() != null) {
        String label = value.getCertId().getLabel();
        vector.add(new DERTaggedObject(true, 2, new DERUTF8String(label)));
      }

      return new DERSequence(vector);
    }

    public P11IdentityId getValue() {
      return value;
    }

  } // class IdentityId

  /**
   * Identifier of PKCS#11 object.
   *
   * <pre>
   * P11ObjectIdentifier ::= SEQUENCE {
   *     id        OCTET STRING,
   *     label     UTF8STRING }
   * </pre>
   */
  public static class ObjectIdentifier extends ProxyMessage {

    private final P11ObjectIdentifier value;

    public ObjectIdentifier(P11ObjectIdentifier value) {
      this.value = Args.notNull(value, "value");
    }

    private ObjectIdentifier(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 2, 2);
      int idx = 0;
      byte[] id = getOctetStringBytes(seq.getObjectAt(idx++));
      String label = getUtf8String(seq.getObjectAt(idx++));
      this.value = new P11ObjectIdentifier(id, label);
    }

    public static ObjectIdentifier getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof ObjectIdentifier) {
        return (ObjectIdentifier) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new ObjectIdentifier((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vec = new ASN1EncodableVector();
      vec.add(new DEROctetString(value.getId()));
      vec.add(new DERUTF8String(value.getLabel()));
      return new DERSequence(vec);
    }

    public P11ObjectIdentifier getValue() {
      return value;
    }

  } // class ObjectIdentifier

  /**
   * List of {@link ObjectIdentifier}s.
   *
   * <pre>
   * P11ObjectIdentifiers ::= SEQUENCE OF P11ObjectIdentifier
   * </pre>
   */
  public static class ObjectIdentifiers extends ProxyMessage {

    private final List<ObjectIdentifier> objectIds;

    public ObjectIdentifiers(List<ObjectIdentifier> objectIds) {
      this.objectIds = Args.notNull(objectIds, "objectIds");
    }

    private ObjectIdentifiers(ASN1Sequence seq) throws BadAsn1ObjectException {
      this.objectIds = new LinkedList<>();
      final int size = seq.size();
      for (int i = 0; i < size; i++) {
        objectIds.add(ObjectIdentifier.getInstance(seq.getObjectAt(i)));
      }
    }

    public static ObjectIdentifiers getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof ObjectIdentifiers) {
        return (ObjectIdentifiers) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new ObjectIdentifiers((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vec = new ASN1EncodableVector();
      for (ObjectIdentifier objectId : objectIds) {
        vec.add(objectId);
      }
      return new DERSequence(vec);
    }

    public List<ObjectIdentifier> getObjectIds() {
      return objectIds;
    }

  } // class ObjectIdentifiers

  /**
   * ASN.1 PKCS#11 params.
   *
   * <pre>
   * P11Params ::= CHOICE {
   *     rsaPkcsPssParams   [0]  RSA-PKCS-PSS-Parameters,
   *     opaqueParams       [1]  OCTET-STRING,
   *     iv                 [2]  IV }
   * </pre>
   */
  public static class P11Params extends ProxyMessage {

    public static final int TAG_RSA_PKCS_PSS = 0;

    public static final int TAG_OPAQUE = 1;

    public static final int TAG_IV = 2;

    private final int tagNo;
    private final ASN1Encodable p11Params;

    public P11Params(int tagNo, ASN1Encodable p11Params) {
      this.tagNo = tagNo;
      this.p11Params = Args.notNull(p11Params, "p11Params");
    }

    private P11Params(ASN1TaggedObject taggedObject) throws BadAsn1ObjectException {
      this.tagNo = taggedObject.getTagNo();
      this.p11Params = taggedObject.getObject();
    }

    public static P11Params getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof P11Params) {
        return (P11Params) obj;
      }

      try {
        if (obj instanceof ASN1TaggedObject) {
          return new P11Params((ASN1TaggedObject) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      return new DERTaggedObject(tagNo, p11Params);
    }

    public int getTagNo() {
      return tagNo;
    }

    public ASN1Encodable getP11Params() {
      return p11Params;
    }

  } // class P11Params

  /**
   * Slot identifier and Object identifier.
   *
   * <pre>
   * SlotIdAndObjectId ::= SEQUENCE {
   *     slotId     SlotIdentifier,
   *     objectId   ObjectIdentifier}
   * </pre>
   */
  public static class SlotIdAndObjectId extends ProxyMessage {

    private final SlotIdentifier slotId;

    private final ObjectIdentifier objectId;

    public SlotIdAndObjectId(P11SlotIdentifier slotId, P11ObjectIdentifier objectId) {
      Args.notNull(slotId, "slotId");
      Args.notNull(objectId, "objectId");

      this.slotId = new SlotIdentifier(slotId);
      this.objectId = new ObjectIdentifier(objectId);
    }

    public SlotIdAndObjectId(SlotIdentifier slotId,
        ObjectIdentifier objectId) {
      this.slotId = Args.notNull(slotId, "slotId");
      this.objectId = Args.notNull(objectId, "objectId");
    }

    private SlotIdAndObjectId(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 2, 2);
      int idx = 0;
      this.slotId = SlotIdentifier.getInstance(seq.getObjectAt(idx++));
      this.objectId = ObjectIdentifier.getInstance(seq.getObjectAt(idx++));
    }

    public static SlotIdAndObjectId getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof SlotIdAndObjectId) {
        return (SlotIdAndObjectId) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new SlotIdAndObjectId((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      return new DERSequence(new ASN1Encodable[]{slotId, objectId});
    }

    public SlotIdentifier getSlotId() {
      return slotId;
    }

    public ObjectIdentifier getObjectId() {
      return objectId;
    }

  } // class SlotIdAndObjectId

  /**
   * Slot identifier.
   *
   * <pre>
   * SlotIdentifier ::= SEQUENCE {
   *     id         INTEGER,
   *     index      INTEGER }
   * </pre>
   */
  public static class SlotIdentifier extends ProxyMessage {

    private final P11SlotIdentifier value;

    public SlotIdentifier(P11SlotIdentifier value) {
      this.value = Args.notNull(value, "value");
    }

    private SlotIdentifier(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 2, 2);
      int idx = 0;
      long id = getInteger(seq.getObjectAt(idx++)).longValue();
      int index = getInteger(seq.getObjectAt(idx++)).intValue();
      this.value = new P11SlotIdentifier(index, id);
    }

    public static SlotIdentifier getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof SlotIdentifier) {
        return (SlotIdentifier) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new SlotIdentifier((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(new ASN1Integer(value.getId()));
      vector.add(new ASN1Integer(value.getIndex()));
      return new DERSequence(vector);
    }

    public P11SlotIdentifier getValue() {
      return value;
    }

  } // class SlotIdentifier

  /**
   * Parameters to remove objects.
   *
   * <pre>
   * RemoveObjectsParams ::= SEQUENCE {
   *     slotId     SlotIdentifier,
   *     id         OCTET STRING OPTIONAL, -- at least one of id and label must be present
   *     label      UTF8String OPTIONAL }
   * </pre>
   */
  public static class RemoveObjectsParams extends ProxyMessage {

    private final P11SlotIdentifier slotId;

    private final byte[] objectId;

    private final String objectLabel;

    public RemoveObjectsParams(P11SlotIdentifier slotId, byte[] objectId, String objectLabel) {
      Args.notNull(slotId, "slotId");
      if ((objectId == null || objectId.length == 0) && StringUtil.isBlank(objectLabel)) {
        throw new IllegalArgumentException(
            "at least one of objectId and objectLabel must not be null");
      }

      this.objectId = objectId;
      this.objectLabel = objectLabel;
      this.slotId = slotId;
    }

    private RemoveObjectsParams(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 2, 3);
      int idx = 0;
      slotId = SlotIdentifier.getInstance(seq.getObjectAt(idx++)).getValue();
      final int size = seq.size();
      ASN1Encodable asn1Id = null;
      ASN1Encodable asn1Label = null;
      if (size == 2) {
        ASN1Encodable asn1 = seq.getObjectAt(1);
        if (asn1 instanceof ASN1String) {
          asn1Label = asn1;
        } else {
          asn1Id = asn1;
        }
      } else {
        asn1Id = seq.getObjectAt(idx++);
        asn1Label = seq.getObjectAt(idx++);
      }

      objectId = (asn1Id == null) ? null : getOctetStringBytes(asn1Id);
      objectLabel = (asn1Label == null) ? null : getUtf8String(seq.getObjectAt(idx++));

      if ((objectId == null || objectId.length == 0) && StringUtil.isBlank(objectLabel)) {
        throw new BadAsn1ObjectException("invalid object RemoveObjectsParams: "
            + "at least one of id and label must not be null");
      }
    }

    public static RemoveObjectsParams getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof RemoveObjectsParams) {
        return (RemoveObjectsParams) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new RemoveObjectsParams((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(new SlotIdentifier(slotId));
      vector.add(new DERUTF8String(objectLabel));
      return new DERSequence(vector);
    }

    public P11SlotIdentifier getSlotId() {
      return slotId;
    }

    public byte[] getOjectId() {
      return objectId == null ? null : Arrays.copyOf(objectId, objectId.length);
    }

    public String getObjectLabel() {
      return objectLabel;
    }

  } // class RemoveObjectsParams

  /**
   * Parameters to create RSAPkcsPss signature.
   *
   * <pre>
   * RSAPkcsPssParams ::= SEQUENCE {
   *     contentHash       INTEGER,
   *     mgfHash           INTEGER,
   *     saltLength        INTEGER }
   * </pre>
   */
  // CHECKSTYLE:SKIP
  public static class RSAPkcsPssParams extends ProxyMessage {

    private final P11RSAPkcsPssParams pkcsPssParams;

    public RSAPkcsPssParams(P11RSAPkcsPssParams pkcsPssParams) {
      this.pkcsPssParams = Args.notNull(pkcsPssParams, "pkcsPssParams");
    }

    private RSAPkcsPssParams(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 3, 3);
      int idx = 0;
      long contentHash = getInteger(seq.getObjectAt(idx++)).longValue();
      long mgfHash = getInteger(seq.getObjectAt(idx++)).longValue();
      int saltLength = getInteger(seq.getObjectAt(idx++)).intValue();
      this.pkcsPssParams = new P11RSAPkcsPssParams(contentHash, mgfHash, saltLength);
    } // constructor

    public static RSAPkcsPssParams getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof RSAPkcsPssParams) {
        return (RSAPkcsPssParams) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new RSAPkcsPssParams((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(new ASN1Integer(pkcsPssParams.getHashAlgorithm()));
      vector.add(new ASN1Integer(pkcsPssParams.getMaskGenerationFunction()));
      vector.add(new ASN1Integer(pkcsPssParams.getSaltLength()));
      return new DERSequence(vector);
    }

    public P11RSAPkcsPssParams getPkcsPssParams() {
      return pkcsPssParams;
    }

  } // class RSAPkcsPssParams

  /**
   * Server capability.
   *
   * <pre>
   * ServerCaps ::= SEQUENCE {
   *     readOnly      BOOLEAN,
   *     versions      SET OF ServerVersion }
   *
   * ServerVersion ::= INTEGER
   * </pre>
   */
  public static class ServerCaps extends ProxyMessage {

    private final Set<Short> versions;

    private final boolean readOnly;

    public ServerCaps(boolean readOnly, Set<Short> versions) {
      this.readOnly = readOnly;
      this.versions = Collections.unmodifiableSet(Args.notEmpty(versions, "versions"));
    }

    private ServerCaps(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 2, 2);
      try {
        this.readOnly = ASN1Boolean.getInstance(seq.getObjectAt(0)).isTrue();
      } catch (IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("invalid readOnly: " + ex.getMessage(), ex);
      }

      ASN1Sequence vecVersions;
      try {
        vecVersions = ASN1Sequence.getInstance(seq.getObjectAt(1));
      } catch (IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("invalid versions: " + ex.getMessage(), ex);
      }

      int count = vecVersions.size();

      Set<Short> tmpVersions = new HashSet<>(count * 2);
      for (int i = 0; i < count; i++) {
        ASN1Integer asn1Int;
        try {
          asn1Int = ASN1Integer.getInstance(vecVersions.getObjectAt(i));
        } catch (IllegalArgumentException ex) {
          throw new BadAsn1ObjectException("invalid version: " + ex.getMessage(), ex);
        }
        tmpVersions.add(asn1Int.getValue().shortValue());
      }
      this.versions = Collections.unmodifiableSet(tmpVersions);
    }

    public static ServerCaps getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof ServerCaps) {
        return (ServerCaps) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new ServerCaps((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(), ex);
      }
    }

    public Set<Short> getVersions() {
      return versions;
    }

    public boolean isReadOnly() {
      return readOnly;
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vecVersions = new ASN1EncodableVector();
      for (Short version : versions) {
        vecVersions.add(new ASN1Integer(BigInteger.valueOf(version)));
      }

      ASN1EncodableVector vec = new ASN1EncodableVector();
      vec.add(ASN1Boolean.getInstance(readOnly));
      vec.add(new DERSequence(vecVersions));
      return new DERSequence(vec);
    }
  } // class ServerCaps

  /**
   * Definition of SignTemplate.
   *
   * <pre>
   * SignTemplate ::= SEQUENCE {
   *     slotId         SlotIdentifier,
   *     objectId       ObjectIdentifier,
   *     mechanism      Mechanism,
   *     message        OCTET STRING }
   * </pre>
   */
  public static class SignTemplate extends ProxyMessage {

    private final SlotIdentifier slotId;

    private final ObjectIdentifier objectId;

    private final Mechanism mechanism;

    private final byte[] message;

    private SignTemplate(ASN1Sequence seq) throws BadAsn1ObjectException {
      requireRange(seq, 4, 4);
      int idx = 0;
      this.slotId = SlotIdentifier.getInstance(seq.getObjectAt(idx++));
      this.objectId = ObjectIdentifier.getInstance(seq.getObjectAt(idx++));
      this.mechanism = Mechanism.getInstance(seq.getObjectAt(idx++));
      this.message = getOctetStringBytes(seq.getObjectAt(idx++));
    }

    public SignTemplate(SlotIdentifier slotId, ObjectIdentifier objectId,
        long mechanism, P11Params parameter, byte[] message) {
      this.slotId = Args.notNull(slotId, "slotId");
      this.objectId = Args.notNull(objectId, "objectId");
      this.message = Args.notNull(message, "message");
      this.mechanism = new Mechanism(mechanism, parameter);
    }

    public static SignTemplate getInstance(Object obj) throws BadAsn1ObjectException {
      if (obj == null || obj instanceof SignTemplate) {
        return (SignTemplate) obj;
      }

      try {
        if (obj instanceof ASN1Sequence) {
          return new SignTemplate((ASN1Sequence) obj);
        } else if (obj instanceof byte[]) {
          return getInstance(ASN1Primitive.fromByteArray((byte[]) obj));
        } else {
          throw new BadAsn1ObjectException("unknown object: " + obj.getClass().getName());
        }
      } catch (IOException | IllegalArgumentException ex) {
        throw new BadAsn1ObjectException("unable to parse encoded object: " + ex.getMessage(),
            ex);
      }
    }

    @Override
    public ASN1Primitive toASN1Primitive() {
      ASN1EncodableVector vector = new ASN1EncodableVector();
      vector.add(slotId);
      vector.add(objectId);
      vector.add(mechanism);
      vector.add(new DEROctetString(message));
      return new DERSequence(vector);
    }

    public byte[] getMessage() {
      return message;
    }

    public SlotIdentifier getSlotId() {
      return slotId;
    }

    public ObjectIdentifier getObjectId() {
      return objectId;
    }

    public Mechanism getMechanism() {
      return mechanism;
    }
  } // class SignTemplate

  private static void requireRange(ASN1Sequence seq, int minSize, int maxSize)
      throws BadAsn1ObjectException {
    int size = seq.size();
    if (size < minSize || size > maxSize) {
      String msg = String.format("seq.size() must not be out of the range [%d, %d]: %d",
          minSize, maxSize, size);
      throw new IllegalArgumentException(msg);
    }
  }

  private static Certificate getCertificate0(ASN1Encodable object) throws BadAsn1ObjectException {
    try {
      return Certificate.getInstance(object);
    } catch (IllegalArgumentException ex) {
      throw new BadAsn1ObjectException("invalid object Certificate: " + ex.getMessage(), ex);
    }
  }

  private static BigInteger getInteger(ASN1Encodable object) throws BadAsn1ObjectException {
    try {
      return ASN1Integer.getInstance(object).getValue();
    } catch (IllegalArgumentException ex) {
      throw new BadAsn1ObjectException("invalid object ASN1Integer: " + ex.getMessage(), ex);
    }
  }

  private static String getUtf8String(ASN1Encodable object) throws BadAsn1ObjectException {
    try {
      return DERUTF8String.getInstance(object).getString();
    } catch (IllegalArgumentException ex) {
      throw new BadAsn1ObjectException("invalid object UTF8String: " + ex.getMessage(), ex);
    }
  }

  public static byte[] getOctetStringBytes(ASN1Encodable object) throws BadAsn1ObjectException {
    try {
      return DEROctetString.getInstance(object).getOctets();
    } catch (IllegalArgumentException ex) {
      throw new BadAsn1ObjectException("invalid object OctetString: " + ex.getMessage(), ex);
    }
  }

  private static ASN1ObjectIdentifier getObjectIdentifier(ASN1Encodable object)
      throws BadAsn1ObjectException {
    try {
      return ASN1ObjectIdentifier.getInstance(object);
    } catch (IllegalArgumentException ex) {
      throw new BadAsn1ObjectException("invalid object ObjectIdentifier: " + ex.getMessage(), ex);
    }
  }

}