/*******************************************************************************
 * Copyright (c) 2006, 2007 IBM Corporation and others. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors: IBM Corporation - initial API and implementation
 ******************************************************************************/
package org.eclipse.osgi.internal.verifier;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.*;
import java.security.cert.Certificate;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

import javax.security.auth.x500.X500Principal;
import org.eclipse.osgi.framework.log.FrameworkLogEntry;
import org.eclipse.osgi.internal.provisional.verifier.CertificateChain;
import org.eclipse.osgi.internal.provisional.verifier.CertificateTrustAuthority;
import org.eclipse.osgi.util.NLS;

/**
 * This class processes a PKCS7 file. See RFC 2315 for specifics.
 */
public class PKCS7Processor implements CertificateChain, JarVerifierConstant {

	private static CertificateFactory certFact;

	static {
		try {
			certFact = CertificateFactory.getInstance("X.509"); //$NON-NLS-1$
		} catch (CertificateException e) {
			SignedBundleHook.log(e.getMessage(), FrameworkLogEntry.ERROR, e);
		}
	}

	private String certChain;
	private Certificate[] certificates;
	private Certificate[] tsaCertificates;
	private boolean trusted;

	// key(object id) = value(structure)
	private Map signedAttrs;

	//	key(object id) = value(structure)
	private Map unsignedAttrs;

	// store the signature of a signerinfo
	private byte signature[];
	private String digestAlgorithm;
	private String signatureAlgorithm;

	private Certificate signerCert;
	private Date signingTime;

	String oid2String(int oid[]) {
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < oid.length; i++) {
			if (i > 0)
				sb.append('.');
			sb.append(oid[i]);
		}
		return sb.toString();
	}

	String findEncryption(int encOid[]) throws NoSuchAlgorithmException {
		if (Arrays.equals(DSA_OID, encOid)) {
			return "DSA"; //$NON-NLS-1$
		}
		if (Arrays.equals(RSA_OID, encOid)) {
			return "RSA"; //$NON-NLS-1$
		}
		throw new NoSuchAlgorithmException("No algorithm found for " + oid2String(encOid)); //$NON-NLS-1$
	}

	String findDigest(int digestOid[]) throws NoSuchAlgorithmException {
		if (Arrays.equals(SHA1_OID, digestOid)) {
			return SHA1_STR;
		}
		if (Arrays.equals(MD5_OID, digestOid)) {
			return MD5_STR;
		}
		if (Arrays.equals(MD2_OID, digestOid)) {
			return MD2_STR;
		}
		throw new NoSuchAlgorithmException("No algorithm found for " + oid2String(digestOid)); //$NON-NLS-1$
	}

	/*
	 * static void printBP(BERProcessor bp, int depth) {
	 * System.out.print(depth); for(int i = 0; i < depth; i++)
	 * System.out.print(" "); System.out.println(bp); }
	 * 
	 * static void dumpSeq(BERProcessor bp, int depth) {
	 * while(!bp.endOfSequence()) { printBP(bp, depth); if (bp.constructed) {
	 * dumpSeq(bp.stepInto(), depth+1); } bp.stepOver(); } }
	 * 
	 * void hexDump(byte buffer[], int off, int len) { for(int i = 0; i < len;
	 * i++) { System.out.print(Integer.toString(buffer[i]&0xff, 16) + " "); if
	 * (i % 16 == 15) System.out.println(); } System.out.println(); }
	 */

	public PKCS7Processor(String certChain, boolean trusted, byte[][] certificates, long signingTime) throws CertificateException {
		this.certChain = certChain;
		this.trusted = trusted;
		this.certificates = new Certificate[certificates.length];
		for (int i = 0; i < certificates.length; i++)
			this.certificates[i] = certFact.generateCertificate(new ByteArrayInputStream(certificates[i]));
		if (signingTime > Long.MIN_VALUE)
			this.signingTime = new Date(signingTime);
	}

	public PKCS7Processor(byte pkcs7[], int pkcs7Offset, int pkcs7Length) throws IOException, CertificateException, NoSuchAlgorithmException {

		// First grab the certificates
		List certs = null;

		BERProcessor bp = new BERProcessor(pkcs7, pkcs7Offset, pkcs7Length);

		// Just do a sanity check and make sure we are actually doing a PKCS7
		// stream
		// PKCS7: Step into the ContentType
		bp = bp.stepInto();
		if (!Arrays.equals(bp.getObjId(), SIGNEDDATA_OID)) {
			throw new IOException("Not a valid PKCS#7 file"); //$NON-NLS-1$
		}

		// PKCS7: Process the SignedData structure
		bp.stepOver(); // (**wrong comments**) skip over the oid
		bp = bp.stepInto(); // go into the Signed data
		bp = bp.stepInto(); // It is a structure;
		bp.stepOver(); // Yeah, yeah version = 1
		bp.stepOver(); // We'll see the digest stuff again; digestAlgorithms

		// process the encapContentInfo structure
		processEncapContentInfo(bp);

		bp.stepOver();

		// PKCS7: check if the class tag is 0
		if (bp.classOfTag == BERProcessor.CONTEXTSPECIFIC_TAGCLASS && bp.tag == 0) {
			// process the certificate elements inside the signeddata strcuture
			certs = processCertificates(bp);
		}

		if (certs == null || certs.size() < 1)
			throw new SecurityException("There are no certificates in the .RSA/.DSA file!"); //$NON-NLS-1$

		// Okay, here are our certificates.
		bp.stepOver();
		if (bp.classOfTag == BERProcessor.UNIVERSAL_TAGCLASS && bp.tag == 1) {
			bp.stepOver(); // Don't use the CRLs if present
		}

		processSignerInfos(bp, certs);

		// construct the cert path
		certs = constructCertPath(certs, signerCert);

		// set the cert chain variable
		int numCerts = certs.size();
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < numCerts; i++) {
			X509Certificate x509Cert = ((X509Certificate) certs.get(i));
			sb.append(x509Cert.getSubjectDN().getName());
			sb.append("; "); //$NON-NLS-1$
		}
		certChain = sb.toString();

		// initialize the certificates
		certificates = (Certificate[]) certs.toArray(new Certificate[numCerts]);

		// if this pkcs7process is tsa asn.1 block, the signingTime should already be set
		if (null == signingTime)
			signingTime = PKCS7DateParser.parseDate(this);
	}

	private void processEncapContentInfo(BERProcessor bp) throws IOException {
		// check immediately if TSTInfo is there
		BERProcessor encapContentBERS = bp.stepInto();
		if (Arrays.equals(encapContentBERS.getObjId(), TIMESTAMP_TST_OID)) {

			// eContent
			encapContentBERS.stepOver();
			BERProcessor encapContentBERS1 = encapContentBERS.stepInto();

			// obtain eContent octet structure
			byte bytesman[] = encapContentBERS1.getBytes();
			BERProcessor eContentStructure = new BERProcessor(bytesman, 0, bytesman.length);

			// pointing at 'version Integer' now
			BERProcessor eContentBER = eContentStructure.stepInto();
			int tsaVersion = eContentBER.getIntValue().intValue();

			if (tsaVersion != 1) {
				throw new IOException("Not a version 1 time-stamp token"); //$NON-NLS-1$
			}

			// policty : TSAPolicyId
			eContentBER.stepOver();

			// messageImprint : MessageImprint
			eContentBER.stepOver();

			// serialNumber : INTEGER
			eContentBER.stepOver();

			// genTime : GeneralizedTime
			eContentBER.stepOver();

			// check time ends w/ 'Z'
			String dateString = new String(eContentBER.getBytes());
			if (!dateString.endsWith("Z")) { //$NON-NLS-1$
				throw new IOException("Wrong dateformat used in time-stamp token"); //$NON-NLS-1$
			}

			// create the appropriate date time string format
			// date format could be yyyyMMddHHmmss[.s...]Z or yyyyMMddHHmmssZ
			int dotIndex = dateString.indexOf('.');
			StringBuffer dateFormatSB = new StringBuffer("yyyyMMddHHmmss"); //$NON-NLS-1$
			if (dotIndex != -1) {
				// yyyyMMddHHmmss[.s...]Z, find out number of s in the bracket
				int noS = dateString.indexOf('Z') - 1 - dotIndex;
				dateFormatSB.append('.');

				// append s	
				for (int i = 0; i < noS; i++) {
					dateFormatSB.append('s');
				}
			}
			dateFormatSB.append("'Z'"); //$NON-NLS-1$

			try {
				DateFormat dateFormt = new SimpleDateFormat(dateFormatSB.toString());
				dateFormt.setTimeZone(TimeZone.getTimeZone("GMT")); //$NON-NLS-1$
				signingTime = dateFormt.parse(dateString);
			} catch (ParseException e) {
				throw new IOException(JarVerifierMessages.PKCS7_Parse_Signing_Time_1);
			}
		}
	}

	private List constructCertPath(List certs, Certificate targetCert) {
		List certsList = new ArrayList();
		certsList.add(targetCert);

		X509Certificate currentCert = (X509Certificate) targetCert;
		int numIteration = certs.size();
		int i = 0;
		while (i < numIteration) {

			X500Principal subject = currentCert.getSubjectX500Principal();
			X500Principal issuer = currentCert.getIssuerX500Principal();

			if (subject.equals(issuer)) {
				// the cert path has been constructed
				break;
			}

			currentCert = null;
			Iterator itr = certs.iterator();

			while (itr.hasNext()) {
				X509Certificate tempCert = (X509Certificate) itr.next();

				if (tempCert.getSubjectX500Principal().equals(issuer)) {
					certsList.add(tempCert);
					currentCert = tempCert;
				}
			}

			i++;
		}

		return certsList;
	}

	public void validateCerts() throws CertificateExpiredException, CertificateNotYetValidException, InvalidKeyException, SignatureException {
		if (certificates == null || certificates.length == 0) {
			throw new SecurityException("There are no certificates in the signature block file!"); //$NON-NLS-1$
		}

		int len = certificates.length;

		// check the certs validity and signatures
		for (int i = 0; i < len; i++) {
			X509Certificate currentX509Cert = (X509Certificate) certificates[i];

			if (signingTime == null)
				currentX509Cert.checkValidity();
			else
				currentX509Cert.checkValidity(signingTime);

			try {
				if (i == len - 1) {
					if (currentX509Cert.getSubjectDN().equals(currentX509Cert.getIssuerDN()))
						currentX509Cert.verify(currentX509Cert.getPublicKey());
				} else {
					X509Certificate nextX509Cert = (X509Certificate) certificates[i + 1];
					currentX509Cert.verify(nextX509Cert.getPublicKey());
				}
			} catch (NoSuchAlgorithmException e) {
				SignedBundleHook.log(e.getMessage(), FrameworkLogEntry.ERROR, e);
				throw new SecurityException(NLS.bind(JarVerifierMessages.No_Such_Algorithm_Excep, new String[] {e.getMessage()}));
			} catch (NoSuchProviderException e) {
				SignedBundleHook.log(e.getMessage(), FrameworkLogEntry.ERROR, e);
				throw new SecurityException(NLS.bind(JarVerifierMessages.No_Such_Provider_Excep, new String[] {e.getMessage()}));
			} catch (CertificateException e) {
				SignedBundleHook.log(e.getMessage(), FrameworkLogEntry.ERROR, e);
				throw new SecurityException(NLS.bind(JarVerifierMessages.Validate_Certs_Certificate_Exception, new String[] {e.getMessage()}));
			}
		}
	}

	private Certificate processSignerInfos(BERProcessor bp, List certs) throws CertificateException, NoSuchAlgorithmException {
		// We assume there is only one SingerInfo element 

		// PKCS7: SignerINFOS processing
		bp = bp.stepInto(); // Step into the set of signerinfos
		bp = bp.stepInto(); // Step into the signerinfo sequence

		// make sure the version is 1
		BigInteger signerInfoVersion = bp.getIntValue();
		if (signerInfoVersion.intValue() != 1) {
			throw new CertificateException(JarVerifierMessages.PKCS7_SignerInfo_Version_Not_Supported);
		}

		// PKCS7: version CMSVersion 
		bp.stepOver(); // Skip the version

		// PKCS7: sid [SignerIdentifier : issuerAndSerialNumber or subjectKeyIdentifer]
		BERProcessor issuerAndSN = bp.stepInto();
		X500Principal signerIssuer = new X500Principal(new ByteArrayInputStream(issuerAndSN.buffer, issuerAndSN.offset, issuerAndSN.endOffset - issuerAndSN.offset));
		issuerAndSN.stepOver();
		BigInteger sn = issuerAndSN.getIntValue();

		// initilize the newSignerCert to the issuer cert of leaf cert
		Certificate newSignerCert = null;

		Iterator itr = certs.iterator();
		// PKCS7: compuare the issuers in the issuerAndSN BER equals to the issuers in Certs generated at the beginning of this method
		// it seems like there is no neeed, cause both ways use the same set of bytes
		while (itr.hasNext()) {
			X509Certificate cert = (X509Certificate) itr.next();
			if (cert.getIssuerX500Principal().equals(signerIssuer) && cert.getSerialNumber().equals(sn)) {
				newSignerCert = cert;
				break;
			}
		}

		if (newSignerCert == null)
			throw new CertificateException("Signer certificate not in pkcs7block"); //$NON-NLS-1$

		// set the signer cert
		signerCert = newSignerCert;

		// PKCS7: skip over the sid [SignerIdentifier : issuerAndSerialNumber or subjectKeyIdentifer]
		bp.stepOver(); // skip the issuer name and serial number

		// PKCS7: digestAlgorithm DigestAlgorithmIdentifier
		BERProcessor digestAlg = bp.stepInto();
		digestAlgorithm = findDigest(digestAlg.getObjId());

		// PKCS7: check if the next one if context class for signedAttrs
		bp.stepOver(); // skip the digest alg

		// process the signed attributes if there is any
		processSignedAttributes(bp);

		// PKCS7: signatureAlgorithm for this SignerInfo
		BERProcessor encryptionAlg = bp.stepInto();
		signatureAlgorithm = findEncryption(encryptionAlg.getObjId());
		bp.stepOver(); // skip the encryption alg

		// PKCS7: signature
		signature = bp.getBytes();

		// PKCS7: Step into the unsignedAttrs, 
		bp.stepOver();

		// process the unsigned attributes if there is any
		processUnsignedAttributes(bp);

		return newSignerCert;
	}

	private void processUnsignedAttributes(BERProcessor bp) {

		if (bp.classOfTag == BERProcessor.CONTEXTSPECIFIC_TAGCLASS && bp.tag == 1) {

			// there are some unsignedAttrs are found!!
			unsignedAttrs = new HashMap();

			// step into a set of unsigned attributes, I believe, when steps 
			// into here, the 'poiter' is pointing to the first element
			BERProcessor unsignedAttrsBERS = bp.stepInto();
			do {
				// process the unsignedAttrsBER by getting the attr type first,
				// then the strcuture for the type
				BERProcessor unsignedAttrBER = unsignedAttrsBERS.stepInto();

				// check if it is timestamp attribute type
				int objID[] = unsignedAttrBER.getObjId();
				// if(Arrays.equals(TIMESTAMP_OID, objID)) {
				// System.out.println("This is a timestamp type, to continue");
				// }

				// get the structure for the attribute type
				unsignedAttrBER.stepOver();
				byte structure[] = unsignedAttrBER.getBytes();
				unsignedAttrs.put(objID, structure);
				unsignedAttrsBERS.stepOver();
			} while (!unsignedAttrsBERS.endOfSequence());
		}
	}

	private void processSignedAttributes(BERProcessor bp) {
		if (bp.classOfTag == BERProcessor.CONTEXTSPECIFIC_TAGCLASS) {

			// process the signed attributes
			signedAttrs = new HashMap();

			BERProcessor signedAttrsBERS = bp.stepInto();
			do {
				BERProcessor signedAttrBER = signedAttrsBERS.stepInto();
				int[] signedAttrObjID = signedAttrBER.getObjId();

				// step over to the attribute value
				signedAttrBER.stepOver();

				byte[] signedAttrStructure = signedAttrBER.getBytes();

				signedAttrs.put(signedAttrObjID, signedAttrStructure);

				signedAttrsBERS.stepOver();
			} while (!signedAttrsBERS.endOfSequence());
			bp.stepOver();
		}
	}

	/**
	 * Returns the Certificate of the signer of this PKCS7Block
	 */
	public Certificate getSigner() {
		if (certificates == null || certificates.length == 0)
			return null;
		return certificates[0];
	}

	public Certificate getRoot() {
		if (certificates == null || certificates.length == 0)
			return null;
		return certificates[certificates.length - 1];
	}

	public Certificate[] getCertificates() {
		return certificates;
	}

	/**
	 * Returns the list of X500 distinguished names that make up the signature chain. Each
	 * distinguished name is separated by a ';'.
	 */
	public String getChain() {
		return certChain;
	}

	/**
	 * Returns true if the signer certificate is trusted
	 * @return true if the signer certificate is trusted
	 */
	public boolean isTrusted() {
		return trusted;
	}

	public boolean equals(Object obj) {
		if (!(obj instanceof CertificateChain))
			return false;
		if (certificates == null)
			return false;
		CertificateChain chain = (CertificateChain) obj;
		if((signingTime == null ? chain.getSigningTime() != null : !signingTime.equals(chain.getSigningTime())))
			return false;
		if (trusted != chain.isTrusted() || (certChain == null ? chain.getChain() != null : !certChain.equals(chain.getChain())))
			return false;
		Certificate[] otherCerts = chain.getCertificates();
		if (otherCerts == null || certificates.length != otherCerts.length)
			return false;
		for (int i = 0; i < certificates.length; i++)
			if (!certificates[i].equals(otherCerts[i]))
				return false;
		return true;
	}

	public void verifySFSignature(byte data[], int dataOffset, int dataLength) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException {
		Signature sig = Signature.getInstance(digestAlgorithm + "with" + signatureAlgorithm); //$NON-NLS-1$
		sig.initVerify(signerCert.getPublicKey());
		sig.update(data, dataOffset, dataLength);
		if (!sig.verify(signature)) {
			throw new SignatureException(JarVerifierMessages.Signature_Not_Verify);
		}
	}

	/**
	 * Return a map of signed attributes, the key(objid) = value(PKCSBlock in bytes for the key)
	 * 
	 * @return  map if there is any signed attributes, null otherwise
	 */
	public Map getUnsignedAttrs() {
		return unsignedAttrs;
	}

	/**
	 * Return a map of signed attributes, the key(objid) = value(PKCSBlock in bytes for the key)
	 * 
	 * @return  map if there is any signed attributes, null otherwise
	 */
	public Map getSignedAttrs() {
		return signedAttrs;
	}

	/**
	 * 
	 * @param bp
	 * @return		a List of certificates from target cert to root cert in order
	 * 
	 * @throws CertificateException
	 */
	private List processCertificates(BERProcessor bp) throws CertificateException {
		List rtvList = new ArrayList(3);

		// Step into the first certificate-element
		BERProcessor certsBERS = bp.stepInto();

		do {
			X509Certificate x509Cert = (X509Certificate) certFact.generateCertificate(new ByteArrayInputStream(certsBERS.buffer, certsBERS.offset, certsBERS.endOffset - certsBERS.offset));

			if (x509Cert != null) {
				rtvList.add(x509Cert);
			}

			// go to the next cert element
			certsBERS.stepOver();
		} while (!certsBERS.endOfSequence());

		//		Collections.reverse(rtvList);
		return rtvList;
	}

	void determineTrust(CertificateTrustAuthority certsTrust) {
		try {
			certsTrust.checkTrust(certificates);
			if (null != tsaCertificates) {
				certsTrust.checkTrust(tsaCertificates);
			}
			trusted = true;
		} catch (CertificateException e) {
			trusted = false;
		}
	}

	public Date getSigningTime() {
		return signingTime;
	}

	void setTSACertificates(Certificate[] tsaCertificates) {
		this.tsaCertificates = tsaCertificates;
	}

	/*
	 public static void main(String[] args) throws InvalidKeyException, CertificateException, NoSuchAlgorithmException, SignatureException, KeyStoreException, IOException {
	 byte buffer[] = new byte[65536];
	 int len = System.in.read(buffer);
	 byte manifestBuff[] = new byte[65536];
	 int rc = new FileInputStream("man").read(manifestBuff);
	 PKCS7Processor p7 = new PKCS7Processor(buffer, 0, len, manifestBuff, 0, rc);
	 System.out.println(p7.getSignerCertificate());
	 System.out.println(p7.getCertificateChain());
	 }
	 */
}