/**
 * X509Certificate
 * 
 * A representation for a X509 Certificate, with
 * methods to parse, verify and sign it.
 * Copyright (c) 2007 Henri Torgemane
 * 
 * See LICENSE.txt for full license information.
 */
package com.hurlant.crypto.cert {
    import com.hurlant.crypto.hash.IHash;
    import com.hurlant.crypto.hash.MD2;
    import com.hurlant.crypto.hash.MD5;
    import com.hurlant.crypto.hash.SHA1;
    import com.hurlant.crypto.rsa.RSAKey;
    import com.hurlant.util.ArrayUtil;
    import com.hurlant.util.Base64;
    import com.hurlant.util.der.ByteString;
    import com.hurlant.util.der.DER;
    import com.hurlant.util.der.OID;
    import com.hurlant.util.der.ObjectIdentifier;
    import com.hurlant.util.der.PEM;
    import com.hurlant.util.der.PrintableString;
    import com.hurlant.util.der.Sequence;
    import com.hurlant.util.der.Type;
    
    import flash.utils.ByteArray;
    
    public class X509Certificate {
        private var _loaded:Boolean;
        private var _param:*;
        private var _obj:Object;
        public function X509Certificate(p:*) {
            _loaded = false;
            _param = p;
            // lazy initialization, to avoid unnecessary parsing of every builtin CA at start-up.
        }
        private function load():void {
            if (_loaded) return;
            var p:* = _param;
            var b:ByteArray;
            if (p is String) {
                b = PEM.readCertIntoArray(p as String);
            } else if (p is ByteArray) {
                b = p;
            }
            if (b!=null) {
                _obj = DER.parse(b, Type.TLS_CERT);
                _loaded = true;
            } else {
                throw new Error("Invalid x509 Certificate parameter: "+p);
            }
        }
        public function isSigned(store:X509CertificateCollection, CAs:X509CertificateCollection, time:Date=null):Boolean {
            load();
            // check timestamps first. cheapest.
            if (time==null) {
                time = new Date;
            }
            var notBefore:Date = getNotBefore();
            var notAfter:Date = getNotAfter();
            if (time.getTime()<notBefore.getTime()) return false; // cert isn't born yet.
            if (time.getTime()>notAfter.getTime()) return false;  // cert died of old age.
            // check signature.
            var subject:String = getIssuerPrincipal();
            // try from CA first, since they're treated better.
            var parent:X509Certificate = CAs.getCertificate(subject);
            var parentIsAuthoritative:Boolean = false;
            if (parent == null) {
                parent = store.getCertificate(subject);
                if (parent == null) {
                    return false; // issuer not found
                }
            } else {
                parentIsAuthoritative = true;
            }
            if (parent == this) { // pathological case. avoid infinite loop
                return false; // isSigned() returns false if we're self-signed.
            }
            if (!(parentIsAuthoritative&&parent.isSelfSigned(time)) &&
                !parent.isSigned(store, CAs, time)) {
                return false;
            }
            var key:RSAKey = parent.getPublicKey();
            return verifyCertificate(key);
        }
        public function isSelfSigned(time:Date):Boolean {
            load();
            
            var key:RSAKey = getPublicKey();
            return verifyCertificate(key);
        }
        private function verifyCertificate(key:RSAKey):Boolean {
            var algo:String = getAlgorithmIdentifier();
            var hash:IHash;
            var oid:String;
            switch (algo) {
                case OID.SHA1_WITH_RSA_ENCRYPTION:
                    hash = new SHA1;
                    oid = OID.SHA1_ALGORITHM;
                    break;
                case OID.MD2_WITH_RSA_ENCRYPTION:
                    hash = new MD2;
                    oid = OID.MD2_ALGORITHM;
                    break;
                case OID.MD5_WITH_RSA_ENCRYPTION:
                    hash = new MD5;
                    oid = OID.MD5_ALGORITHM;
                    break;
                default:
                    return false;
            }
            var data:ByteArray = _obj.signedCertificate_bin;
            var buf:ByteArray = new ByteArray;
            key.verify(_obj.encrypted, buf, _obj.encrypted.length);
            buf.position=0;
            data = hash.hash(data);
            var obj:Object = DER.parse(buf, Type.RSA_SIGNATURE);
            if (obj.algorithm.algorithmId.toString() != oid) {
                return false; // wrong algorithm
            }
            if (!ArrayUtil.equals(obj.hash, data)) {
                return false; // hashes don't match
            }
            return true;
        }
        
        /**
         * This isn't used anywhere so far.
         * It would become useful if we started to offer facilities
         * to generate and sign X509 certificates.
         * 
         * @param key
         * @param algo
         * @return 
         * 
         */
        private function signCertificate(key:RSAKey, algo:String):ByteArray {
            var hash:IHash;
            var oid:String;
            switch (algo) {
                case OID.SHA1_WITH_RSA_ENCRYPTION:
                    hash = new SHA1;
                    oid = OID.SHA1_ALGORITHM;
                    break;
                case OID.MD2_WITH_RSA_ENCRYPTION:
                    hash = new MD2;
                    oid = OID.MD2_ALGORITHM;
                    break;
                case OID.MD5_WITH_RSA_ENCRYPTION:
                    hash = new MD5;
                    oid = OID.MD5_ALGORITHM;
                    break;
                default:
                    return null
            }
            var data:ByteArray = _obj.signedCertificate_bin;
            data = hash.hash(data);
            var seq1:Sequence = new Sequence;
            seq1[0] = new Sequence;
            seq1[0][0] = new ObjectIdentifier(0,0, oid);
            seq1[0][1] = null;
            seq1[1] = new ByteString;
            seq1[1].writeBytes(data);
            data = seq1.toDER();
            var buf:ByteArray = new ByteArray;
            key.sign(data, buf, data.length);
            return buf;
        }
        
        public function getPublicKey():RSAKey {
            load();
            var pk:ByteArray = _obj.signedCertificate.subjectPublicKeyInfo.subjectPublicKey as ByteArray;
            pk.position = 0;
            var rsaKey:Object = DER.parse(pk, [{name:"N"},{name:"E"}]);
            return new RSAKey(rsaKey.N, rsaKey.E.valueOf());
        }
        
        /**
         * Returns a subject principal, as an opaque base64 string.
         * This is only used as a hash key for known certificates.
         * 
         * Note that this assumes X509 DER-encoded certificates are uniquely encoded,
         * as we look for exact matches between Issuer and Subject fields.
         * 
         */
        public function getSubjectPrincipal():String {
            load();
            return Base64.encodeByteArray(_obj.signedCertificate.subject_bin);
        }
        /**
         * Returns an issuer principal, as an opaque base64 string.
         * This is only used to quickly find matching parent certificates.
         * 
         * Note that this assumes X509 DER-encoded certificates are uniquely encoded,
         * as we look for exact matches between Issuer and Subject fields.
         * 
         */
        public function getIssuerPrincipal():String {
            load();
            return Base64.encodeByteArray(_obj.signedCertificate.issuer_bin);
        }
        public function getAlgorithmIdentifier():String {
            return _obj.algorithmIdentifier.algorithmId.toString();
        }
        public function getNotBefore():Date {
            return _obj.signedCertificate.validity.notBefore.date;
        }
        public function getNotAfter():Date {
            return _obj.signedCertificate.validity.notAfter.date;
        }
        
        public function getCommonName():String {
            var subject:Sequence = _obj.signedCertificate.subject;
            return (subject.findAttributeValue(OID.COMMON_NAME) as PrintableString).getString();
        }
    }
}