/*
 * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
 * See LICENSE in the project root for license information.
 */

package com.linkedin.mitm.services;

import com.linkedin.mitm.model.CertificateAuthority;
import com.linkedin.mitm.model.CertificateValidPeriod;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.OperatorCreationException;


/**
 * This class is used for generating server side certificate based on
 * CA certificate, CN and SAN
 * @author shfeng
 */
public class IdentityCertificateService extends AbstractX509CertificateService implements CertificateService {

  private static final BasicConstraints BASIC_CONSTRAINTS = new BasicConstraints(false);
  private final PrivateKey _issuerPrivateKey;
  private final X509Certificate _issuerCertificate;

  /**
   * @param certificateAuthority factory to generate public key/ private key pair
   * @param issuerKeyStore this filed could be null if we are building root certificate that issued by nobody
   *
   * */
  public IdentityCertificateService(CertificateAuthority certificateAuthority,
      CertificateValidPeriod certificateValidPeriod, KeyStore issuerKeyStore)
      throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
    super(certificateAuthority, certificateValidPeriod);
    _issuerPrivateKey =
        (PrivateKey) issuerKeyStore.getKey(certificateAuthority.getAlias(), certificateAuthority.getPassPhrase());
    _issuerCertificate = (X509Certificate) issuerKeyStore.getCertificate(certificateAuthority.getAlias());
  }

  /**
   * Create a certificate using key pair and signing certificate with CA certificate, common name and a list of subjective alternate name
   *
   * @return signed sever identity certificate
   * */
  @Override
  public X509Certificate createSignedCertificate(PublicKey publicKey, PrivateKey privateKey, String commonName,
      List<ASN1Encodable> sans)
      throws CertificateException, IOException, OperatorCreationException, NoSuchProviderException,
             NoSuchAlgorithmException, InvalidKeyException, SignatureException {
    X500Name issuer = new X509CertificateHolder(_issuerCertificate.getEncoded()).getSubject();
    BigInteger serial = getSerial();
    X500Name subject = getSubject(commonName);

    X509v3CertificateBuilder x509v3CertificateBuilder =
        new JcaX509v3CertificateBuilder(issuer, serial, getValidDateFrom(), getValidDateTo(), subject, publicKey);
    buildExtensions(x509v3CertificateBuilder, publicKey);

    fillSans(sans, x509v3CertificateBuilder);

    X509Certificate signedCertificate = createCertificate(_issuerPrivateKey, x509v3CertificateBuilder);

    signedCertificate.checkValidity();
    signedCertificate.verify(_issuerCertificate.getPublicKey());

    return signedCertificate;
  }

  @Override
  public void updateKeyStore(KeyStore keyStore, PrivateKey privateKey, Certificate identityCertificate)
      throws KeyStoreException {
    updateKeyStore(keyStore, privateKey, new Certificate[]{identityCertificate, _issuerCertificate});
  }

  /**
   * Fill subject alternate names in to signedCertificatebuilder to build new certificate
   * @param sans  a list of subject alternate name.
   *
   * */
  private void fillSans(List<ASN1Encodable> sans, X509v3CertificateBuilder x509v3CertificateBuilder)
      throws CertIOException {
    if (!sans.isEmpty()) {
      ASN1Encodable[] encodables = sans.toArray(new ASN1Encodable[sans.size()]);
      x509v3CertificateBuilder.addExtension(Extension.subjectAlternativeName, false, new DERSequence(encodables));
    }
  }

  @Override
  protected void buildExtensions(X509v3CertificateBuilder x509v3CertificateBuilder, PublicKey publicKey)
      throws IOException {
    x509v3CertificateBuilder.addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyIdentifier(publicKey));
    x509v3CertificateBuilder.addExtension(Extension.basicConstraints, false, BASIC_CONSTRAINTS);
  }
}