package org.zz.gmhelper.cert;

import java.math.BigInteger;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;

public class SM2X509CertMaker {
  
    private static enum CertLevel {
        RootCA,
        SubCA,
        EndEntity
    } // class CertLevel

    public static final String SIGN_ALGO_SM3WITHSM2 = "SM3withSM2";

    private long certExpire;
    private X500Name issuerDN;
    private CertSNAllocator snAllocator;
    private KeyPair issuerKeyPair;

    /**
     * @param issuerKeyPair 证书颁发者的密钥对。
     *                      其实一般的CA的私钥都是要严格保护的。
     *                      一般CA的私钥都会放在加密卡/加密机里,证书的签名由加密卡/加密机完成。
     *                      这里仅是为了演示BC库签发证书的用法,所以暂时不作太多要求。
     * @param certExpire    证书有效时间,单位毫秒
     * @param issuer        证书颁发者信息
     * @param snAllocator   维护/分配证书序列号的实例,证书序列号应该递增且不重复
     */
    public SM2X509CertMaker(KeyPair issuerKeyPair, long certExpire, X500Name issuer,
        CertSNAllocator snAllocator) {
        this.issuerKeyPair = issuerKeyPair;
        this.certExpire = certExpire;
        this.issuerDN = issuer;
        this.snAllocator = snAllocator;
    }

    /**
     * 生成根CA证书
     * 
     * @param csr CSR
     * @return 新的证书
     * @throws Exception 如果错误发生
     */
    public X509Certificate makeRootCACert(byte[] csr) 
            throws Exception {
        KeyUsage usage = new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign);
        return makeCertificate(CertLevel.RootCA, null, csr, usage, null);
    }
    
    /**
     * 生成SubCA证书
     * 
     * @param csr CSR
     * @return 新的证书
     * @throws Exception 如果错误发生
     */
    public X509Certificate makeSubCACert(byte[] csr) 
            throws Exception {
        KeyUsage usage = new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign);
        return makeCertificate(CertLevel.SubCA, 0, csr, usage, null);
    }

    /**
     * 生成SSL用户证书
     * 
     * @param csr CSR
     * @return 新的证书
     * @throws Exception 如果错误发生
     */
    public X509Certificate makeSSLEndEntityCert(byte[] csr) 
        throws Exception {
        return makeEndEntityCert(csr,
            new KeyPurposeId[] {KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth});
    }

    /**
     * 生成用户证书
     * 
     * @param csr CSR
     * @param extendedKeyUsages 扩展指数用途。
     * @return 新的证书
     * @throws Exception 如果错误发生
     */
    public X509Certificate makeEndEntityCert(byte[] csr,
          KeyPurposeId[] extendedKeyUsages) 
            throws Exception {
        KeyUsage usage = new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyAgreement
                            | KeyUsage.dataEncipherment | KeyUsage.keyEncipherment);
        return makeCertificate(CertLevel.SubCA, null, csr, usage, extendedKeyUsages);
    }

    /**
     * @param isCA     是否是颁发给CA的证书
     * @param keyUsage 证书用途
     * @param csr      CSR
     * @return
     * @throws Exception
     */
    private X509Certificate makeCertificate(CertLevel certLevel, Integer pathLenConstrain,
        byte[] csr, KeyUsage keyUsage, KeyPurposeId[] extendedKeyUsages)
            throws Exception {
        if (certLevel == CertLevel.EndEntity) {
            if (keyUsage.hasUsages(KeyUsage.keyCertSign)) {
              throw new IllegalArgumentException(
                  "keyusage keyCertSign is not allowed in EndEntity Certificate");
            }
        }
        
        PKCS10CertificationRequest request = new PKCS10CertificationRequest(csr);
        SubjectPublicKeyInfo subPub = request.getSubjectPublicKeyInfo();

        PrivateKey issPriv = issuerKeyPair.getPrivate();
        PublicKey issPub = issuerKeyPair.getPublic();
        
        X500Name subject = request.getSubject();
        String email = null;
        String commonName = null;
        /*
         * RFC 5280 §4.2.1.6 Subject
         *  Conforming implementations generating new certificates with
         *  electronic mail addresses MUST use the rfc822Name in the subject
         *  alternative name extension (Section 4.2.1.6) to describe such
         *  identities.  Simultaneous inclusion of the emailAddress attribute in
         *  the subject distinguished name to support legacy implementations is
         *  deprecated but permitted.
         */
        RDN[] rdns = subject.getRDNs();
        List<RDN> newRdns = new ArrayList<>(rdns.length);
        for (int i = 0; i < rdns.length; i++) {
            RDN rdn = rdns[i];
            
            AttributeTypeAndValue atv = rdn.getFirst();
            ASN1ObjectIdentifier type = atv.getType();
            if (BCStyle.EmailAddress.equals(type)) {
                email = IETFUtils.valueToString(atv.getValue());
            } else {
                if (BCStyle.CN.equals(type)) {
                    commonName = IETFUtils.valueToString(atv.getValue());
                }
                newRdns.add(rdn);
            }
        }
        
        List<GeneralName> subjectAltNames = new LinkedList<>();
        if (email != null) {
            subject = new X500Name(newRdns.toArray(new RDN[0]));
            subjectAltNames.add(
                new GeneralName(GeneralName.rfc822Name,
                    new DERIA5String(email, true)));
        }
        
        boolean selfSignedEECert = false;
        switch (certLevel) {
            case RootCA:
                if (issuerDN.equals(subject)) {
                    subject = issuerDN;
                } else {
                    throw new IllegalArgumentException("subject != issuer for certLevel " + CertLevel.RootCA);
                }
                break;
            case SubCA:
                if (issuerDN.equals(subject)) {
                    throw new IllegalArgumentException(
                        "subject MUST not equals issuer for certLevel " + certLevel);
                }
                break;
            default:
                if (issuerDN.equals(subject)) {
                    selfSignedEECert = true;
                    subject = issuerDN;
                }
        }

        BigInteger serialNumber = snAllocator.nextSerialNumber();
        Date notBefore = new Date();
        Date notAfter = new Date(notBefore.getTime() + certExpire);
        X509v3CertificateBuilder v3CertGen = new X509v3CertificateBuilder(
            issuerDN, serialNumber,
            notBefore, notAfter,
            subject, subPub);

        JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
        v3CertGen.addExtension(Extension.subjectKeyIdentifier, false,
            extUtils.createSubjectKeyIdentifier(subPub));
        if (certLevel != CertLevel.RootCA && !selfSignedEECert) {
            v3CertGen.addExtension(Extension.authorityKeyIdentifier, false,
                extUtils.createAuthorityKeyIdentifier(SubjectPublicKeyInfo.getInstance(issPub.getEncoded())));
        }

        // RFC 5280 §4.2.1.9 Basic Constraints:
        // Conforming CAs MUST include this extension in all CA certificates
        // that contain public keys used to validate digital signatures on
        // certificates and MUST mark the extension as critical in such
        // certificates.
        BasicConstraints basicConstraints;
        if (certLevel == CertLevel.EndEntity) {
            basicConstraints = new BasicConstraints(false);
        } else {
            basicConstraints = pathLenConstrain == null
                ? new BasicConstraints(true) : new BasicConstraints(pathLenConstrain.intValue());
        }
        v3CertGen.addExtension(Extension.basicConstraints, true, basicConstraints);

        // RFC 5280 §4.2.1.3 Key Usage: When present, conforming CAs SHOULD mark this extension as critical.
        v3CertGen.addExtension(Extension.keyUsage, true, keyUsage);

        if (extendedKeyUsages != null) {
            ExtendedKeyUsage xku = new ExtendedKeyUsage(extendedKeyUsages);
            v3CertGen.addExtension(Extension.extendedKeyUsage, false, xku);
          
            boolean forSSLServer = false;
            for (KeyPurposeId purposeId : extendedKeyUsages) {
                if (KeyPurposeId.id_kp_serverAuth.equals(purposeId)) {
                    forSSLServer = true;
                    break;
                }
            }

            if (forSSLServer) {
                if (commonName == null) {
                    throw new IllegalArgumentException("commonName must not be null");
                }
                GeneralName name = new GeneralName(GeneralName.dNSName,
                                      new DERIA5String(commonName, true));
                subjectAltNames.add(name);
            }
        }
        
        if (!subjectAltNames.isEmpty()) {
            v3CertGen.addExtension(Extension.subjectAlternativeName, false,
                new GeneralNames(subjectAltNames.toArray(new GeneralName[0])));
        }

        JcaContentSignerBuilder contentSignerBuilder = makeContentSignerBuilder(issPub);
        X509Certificate cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME)
            .getCertificate(v3CertGen.build(contentSignerBuilder.build(issPriv)));
        cert.verify(issPub);

        return cert;
    }

    private JcaContentSignerBuilder makeContentSignerBuilder(PublicKey issPub) throws Exception {
        if (issPub.getAlgorithm().equals("EC")) {
            JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder(SIGN_ALGO_SM3WITHSM2);
            contentSignerBuilder.setProvider(BouncyCastleProvider.PROVIDER_NAME);
            return contentSignerBuilder;
        }
        throw new Exception("Unsupported PublicKey Algorithm:" + issPub.getAlgorithm());
    }
}