diff --git a/browser/root-certs b/browser/root-certs index 6c5796560..54038f33c 100755 --- a/browser/root-certs +++ b/browser/root-certs @@ -50,14 +50,14 @@ function getRootCerts(callback) { + ' pem = pem.replace(/-----BEGIN CERTIFICATE-----/g, "");\n' + ' pem = pem.replace(/-----END CERTIFICATE-----/g, "");\n' + ' pem = pem.replace(/\\s+/g, "");\n' - + ' if (!Object.prototype.hasOwnProperty.call(certs, pem)) return;\n' - + ' return certs[pem];\n' + + ' if (!Object.prototype.hasOwnProperty.call(trusted, pem)) return;\n' + + ' return trusted[pem];\n' + '}\n' + '\n' + 'function getCert(name) {\n' + ' name = name.replace(/^\s+|\s+$/g, "");\n' - + ' if (!Object.prototype.hasOwnProperty.call(trusted, name)) return;\n' - + ' return trusted[name];\n' + + ' if (!Object.prototype.hasOwnProperty.call(certs, name)) return;\n' + + ' return certs[name];\n' + '}\n' + '\n' + 'exports.certs = certs;\n' diff --git a/lib/PayPro.js b/lib/PayPro.js index 0d9abc557..eb0cd7abb 100644 --- a/lib/PayPro.js +++ b/lib/PayPro.js @@ -6,39 +6,48 @@ var RootCerts = require('./common/RootCerts'); var PayPro = require('./common/PayPro'); -PayPro.prototype.x509Sign = function(key) { +var asn1 = require('asn1.js'); +var rfc3280 = require('asn1.js/rfc/3280'); + +PayPro.prototype.x509Sign = function(key, returnTrust) { var self = this; var crypto = require('crypto'); var pki_type = this.get('pki_type'); - var pki_data = this.get('pki_data'); // contains one or more x509 certs + var pki_data = this.get('pki_data'); pki_data = PayPro.X509Certificates.decode(pki_data); pki_data = pki_data.certificate; var details = this.get('serialized_payment_details'); var type = pki_type.split('+')[1].toUpperCase(); - var trusted = pki_data.map(function(cert) { - var der = cert.toString('hex'); - var pem = self._DERtoPEM(der, 'CERTIFICATE'); - return RootCerts.getTrusted(pem); - }); - - // XXX Figure out what to do here - if (!trusted.length) { - // throw new Error('Unstrusted certificate.'); - } else { - trusted.forEach(function(name) { - // console.log('Certificate: %s', name); - }); - } - var signature = crypto.createSign('RSA-' + type); var buf = this.serializeForSig(); signature.update(buf); var sig = signature.sign(key); + + if (returnTrust) { + var cert = pki_data[pki_data.length - 1]; + var der = cert.toString('hex'); + var pem = PayPro.DERtoPEM(der, 'CERTIFICATE'); + var caName = RootCerts.getTrusted(pem); + var selfSigned = 0; + if (!caName) { + selfSigned = pki_data.length > 1 + ? -1 + : 1; + } + return { + selfSigned: selfSigned, + isChain: pki_data.length > 1, + signature: sig, + caTrusted: !!caName, + caName: caName || null + }; + } + return sig; }; -PayPro.prototype.x509Verify = function() { +PayPro.prototype.x509Verify = function(returnTrust) { var self = this; var crypto = require('crypto'); var pki_type = this.get('pki_type'); @@ -53,20 +62,125 @@ PayPro.prototype.x509Verify = function() { var verifier = crypto.createVerify('RSA-' + type); verifier.update(buf); - return pki_data.every(function(cert) { - var der = cert.toString('hex'); - var pem = self._DERtoPEM(der, 'CERTIFICATE'); + var signedCert = pki_data[0]; + var der = signedCert.toString('hex'); + var pem = PayPro.DERtoPEM(der, 'CERTIFICATE'); + var verified = verifier.verify(pem, sig); - var name = RootCerts.getTrusted(pem); - // XXX Figure out what to do here - if (!name) { - // throw new Error('Unstrusted certificate.'); - } else { - // console.log('Certificate: %s', name); + var chain = pki_data; + + // + // Get the CA cert's name + // + var issuer = chain[chain.length - 1]; + der = issuer.toString('hex'); + pem = PayPro.DERtoPEM(der, 'CERTIFICATE'); + var caName = RootCerts.getTrusted(pem); + + if (chain.length === 1 && !caName) { + if (returnTrust) { + return { + selfSigned: 1, // yes + isChain: false, + verified: verified, + caTrusted: false, + caName: null, + chainVerified: false + }; } + return verified; + } - return verifier.verify(pem, sig); + // If there's no trusted root cert, don't + // bother validating the cert chain. + if (!caName) { + if (returnTrust) { + return { + selfSigned: -1, // unknown + isChain: chain.length > 1, + verified: verified, + caTrusted: false, + caName: null, + chainVerified: false + }; + } + return verified; + } + + var chainVerified = chain.every(function(cert, i) { + var der = cert.toString('hex'); + var pem = PayPro.DERtoPEM(der, 'CERTIFICATE'); + var name = RootCerts.getTrusted(pem); + + var ncert = chain[i + 1]; + // The root cert, check if it's trusted: + if (!ncert || name) { + if (!name) { + return false; + } + chain.length = 0; + return true; + } + var nder = ncert.toString('hex'); + var npem = PayPro.DERtoPEM(nder, 'CERTIFICATE'); + + // + // Get Public Key from next certificate: + // + var ndata = new Buffer(nder, 'hex'); + var nc = rfc3280.Certificate.decode(ndata, 'der'); + var npubKeyAlg = PayPro.getAlgorithm( + nc.tbsCertificate.subjectPublicKeyInfo.algorithm.algorithm); + var npubKey = nc.tbsCertificate.subjectPublicKeyInfo.subjectPublicKey.data; + npubKey = PayPro.DERtoPEM(npubKey, npubKeyAlg + ' PUBLIC KEY'); + + // + // Get Signature Value from current certificate: + // + var data = new Buffer(der, 'hex'); + var c = rfc3280.Certificate.decode(data, 'der'); + var sigAlg = PayPro.getAlgorithm(c.signatureAlgorithm.algorithm, 1); + var sig = c.signature.data; + + // + // Check Validity of Certificates + // + var validityVerified = PayPro.validateCertTime(c, nc); + + // + // Check the Issuer matches the Subject of the next certificate: + // + var issuerVerified = PayPro.validateCertIssuer(c, nc); + + // + // Verify current Certificate signature + // + + // Get the raw DER TBSCertificate + // from the DER Certificate: + var tbs = PayPro.getTBSCertificate(data); + + var verifier = crypto.createVerify('RSA-' + sigAlg); + verifier.update(tbs); + var sigVerified = verifier.verify(npubKey, sig); + + return validityVerified + && issuerVerified + && sigVerified; }); + + if (returnTrust) { + return { + selfSigned: 0, // no + isChain: true, + verified: verified, + caTrusted: !!caName, + caName: caName || null, + chainVerified: chainVerified + }; + } + + return verified && chainVerified; }; module.exports = PayPro; diff --git a/lib/browser/PayPro.js b/lib/browser/PayPro.js index b9ab06113..3058284e1 100644 --- a/lib/browser/PayPro.js +++ b/lib/browser/PayPro.js @@ -6,11 +6,14 @@ var assert = require('assert'); var PayPro = require('../common/PayPro'); var RootCerts = require('../common/RootCerts'); +var asn1 = require('asn1.js'); +var rfc3280 = require('asn1.js/rfc/3280'); + // Documentation: // http://kjur.github.io/jsrsasign/api/symbols/KJUR.crypto.Signature.html#.sign // http://kjur.github.io/jsrsasign/api/symbols/RSAKey.html -PayPro.prototype.x509Sign = function(key) { +PayPro.prototype.x509Sign = function(key, returnTrust) { var pki_type = this.get('pki_type'); var pki_data = this.get('pki_data'); // contains one or more x509 certs pki_data = PayPro.X509Certificates.decode(pki_data); @@ -18,21 +21,6 @@ PayPro.prototype.x509Sign = function(key) { var type = pki_type.split('+')[1].toUpperCase(); var buf = this.serializeForSig(); - var trusted = pki_data.map(function(cert) { - var der = cert.toString('hex'); - var pem = KJUR.asn1.ASN1Util.getPEMStringFromHex(der, 'CERTIFICATE'); - return RootCerts.getTrusted(pem); - }); - - // XXX Figure out what to do here - if (!trusted.length) { - // throw new Error('Unstrusted certificate.'); - } else { - trusted.forEach(function(name) { - // console.log('Certificate: %s', name); - }); - } - var rsa = new KJUR.RSAKey(); rsa.readPrivateKeyFromPEMString(key.toString()); key = rsa; @@ -47,10 +35,31 @@ PayPro.prototype.x509Sign = function(key) { jsrsaSig.updateHex(buf.toString('hex')); var sig = new Buffer(jsrsaSig.sign(), 'hex'); + + if (returnTrust) { + var cert = pki_data[pki_data.length - 1]; + var der = cert.toString('hex'); + var pem = KJUR.asn1.ASN1Util.getPEMStringFromHex(der, 'CERTIFICATE'); + var caName = RootCerts.getTrusted(pem); + var selfSigned = 0; + if (!caName) { + selfSigned = pki_data.length > 1 + ? -1 + : 1; + } + return { + selfSigned: selfSigned, + isChain: pki_data.length > 1, + signature: sig, + caTrusted: !!caName, + caName: caName || null + }; + } + return sig; }; -PayPro.prototype.x509Verify = function(key) { +PayPro.prototype.x509Verify = function(returnTrust) { var sig = this.get('signature'); var pki_type = this.get('pki_type'); var pki_data = this.get('pki_data'); @@ -64,24 +73,143 @@ PayPro.prototype.x509Verify = function(key) { prov: 'cryptojs/jsrsa' }); - return pki_data.every(function(cert) { - var der = cert.toString('hex'); - var pem = KJUR.asn1.ASN1Util.getPEMStringFromHex(der, 'CERTIFICATE'); + var signedCert = pki_data[0]; + var der = signedCert.toString('hex'); + // var pem = self._DERtoPEM(der, 'CERTIFICATE'); + var pem = KJUR.asn1.ASN1Util.getPEMStringFromHex(der, 'CERTIFICATE'); + jsrsaSig.initVerifyByCertificatePEM(pem); + jsrsaSig.updateHex(buf.toString('hex')); + var verified = jsrsaSig.verify(sig.toString('hex')); - // XXX Figure out what to do here - var name = RootCerts.getTrusted(pem); - if (!name) { - // throw new Error('Unstrusted certificate.'); - } else { - // console.log('Certificate: %s', name); + var chain = pki_data; + + // + // Get the CA cert's name + // + var issuer = chain[chain.length - 1]; + der = issuer.toString('hex'); + // pem = this._DERtoPEM(der, 'CERTIFICATE'); + pem = KJUR.asn1.ASN1Util.getPEMStringFromHex(der, 'CERTIFICATE'); + var caName = RootCerts.getTrusted(pem); + + if (chain.length === 1 && !caName) { + if (returnTrust) { + return { + selfSigned: 1, // yes + isChain: false, + verified: verified, + caTrusted: false, + caName: null, + chainVerified: false + }; } + return verified; + } - jsrsaSig.initVerifyByCertificatePEM(pem); + // If there's no trusted root cert, don't + // bother validating the cert chain. + if (!caName) { + if (returnTrust) { + return { + selfSigned: -1, // unknown + isChain: chain.length > 1, + verified: verified, + caTrusted: false, + caName: null, + chainVerified: false + }; + } + return verified; + } - jsrsaSig.updateHex(buf.toString('hex')); + var chainVerified = chain.every(function(cert, i) { + var der = cert.toString('hex'); + // var pem = self._DERtoPEM(der, 'CERTIFICATE'); + var pem = KJUR.asn1.ASN1Util.getPEMStringFromHex(der, 'CERTIFICATE'); + var name = RootCerts.getTrusted(pem); - return jsrsaSig.verify(sig.toString('hex')); + var ncert = chain[i + 1]; + // The root cert, check if it's trusted: + if (!ncert || name) { + if (!name) { + return false; + } + chain.length = 0; + return true; + } + var nder = ncert.toString('hex'); + // var npem = self._DERtoPEM(nder, 'CERTIFICATE'); + var npem = KJUR.asn1.ASN1Util.getPEMStringFromHex(nder, 'CERTIFICATE'); + + // + // Get Next Certificate: + // + var ndata = new Buffer(nder, 'hex'); + var nc = rfc3280.Certificate.decode(ndata, 'der'); + + // + // Get Public Key from next certificate (via KJUR because it's a mess): + // + var js = new KJUR.crypto.Signature({ + alg: type + 'withRSA', + prov: 'cryptojs/jsrsa' + }); + js.initVerifyByCertificatePEM(npem); + var npubKey = js.pubKey; + + // + // Get Signature Value from current certificate: + // + var data = new Buffer(der, 'hex'); + var c = rfc3280.Certificate.decode(data, 'der'); + var sigAlg = PayPro.getAlgorithm(c.signatureAlgorithm.algorithm, 1); + var sig = c.signature.data; + + // + // Check Validity of Certificates + // + var validityVerified = PayPro.validateCertTime(c, nc); + + // + // Check the Issuer matches the Subject of the next certificate: + // + var issuerVerified = PayPro.validateCertIssuer(c, nc); + + // + // Verify current Certificate signature + // + + var jsrsaSig = new KJUR.crypto.Signature({ + alg: type + 'withRSA', + prov: 'cryptojs/jsrsa' + }); + jsrsaSig.initVerifyByPublicKey(npubKey); + + // Get the raw DER TBSCertificate + // from the DER Certificate: + var tbs = PayPro.getTBSCertificate(data); + + jsrsaSig.updateHex(tbs.toString('hex')); + + var sigVerified = jsrsaSig.verify(sig.toString('hex')); + + return validityVerified + && issuerVerified + && sigVerified; }); + + if (returnTrust) { + return { + selfSigned: 0, // no + isChain: true, + verified: verified, + caTrusted: !!caName, + caName: caName || null, + chainVerified: chainVerified + }; + } + + return verified && chainVerified; }; module.exports = PayPro; diff --git a/lib/common/PayPro.js b/lib/common/PayPro.js index ff0e6858e..2f62b8462 100644 --- a/lib/common/PayPro.js +++ b/lib/common/PayPro.js @@ -18,6 +18,117 @@ PayPro.PAYMENT_REQUEST_CONTENT_TYPE = "application/bitcoin-paymentrequest"; PayPro.PAYMENT_CONTENT_TYPE = "application/bitcoin-payment"; PayPro.PAYMENT_ACK_CONTENT_TYPE = "application/bitcoin-paymentack"; +// https://www.google.com/search?q=signatureAlgorithm+1.2.840.113549.1.1.1 +// http://msdn.microsoft.com/en-us/library/windows/desktop/aa379057(v=vs.85).aspx +PayPro.X509_ALGORITHM = { + '1.2.840.113549.1.1.1': 'RSA', + '1.2.840.113549.1.1.2': 'RSA_MD2', + '1.2.840.113549.1.1.4': 'RSA_MD5', + '1.2.840.113549.1.1.5': 'RSA_SHA1', + '1.2.840.113549.1.1.11': 'RSA_SHA256', + '1.2.840.113549.1.1.12': 'RSA_SHA384', + '1.2.840.113549.1.1.13': 'RSA_SHA512', + + '1.2.840.10045.4.3.2': 'ECDSA_SHA256', + '1.2.840.10045.4.3.3': 'ECDSA_SHA384', + '1.2.840.10045.4.3.4': 'ECDSA_SHA512' +}; + +PayPro.getAlgorithm = function(value, index) { + if (Array.isArray(value)) { + value = value.join('.'); + } + value = PayPro.X509_ALGORITHM[value]; + if (index != null) { + value = value.split('_'); + if (index === true) { + return { + cipher: value[0], + hash: value[1] + }; + } + return value[index]; + } + return value; +}; + +// Grab the raw DER To-Be-Signed Certificate +// from a DER Certificate to verify +PayPro.getTBSCertificate = function(data) { + // We start by slicing off the first SEQ of the + // Certificate (TBSCertificate is its own SEQ). + + // The first 10 bytes usually look like: + // [ 48, 130, 5, 32, 48, 130, 4, 8, 160, 3 ] + var start = 0; + var starts = 0; + for (var start = 0; start < data.length; start++) { + if (starts === 1 && data[start] === 48) { + break; + } + if (starts < 1 && data[start] === 48) { + starts++; + } + } + + // The bytes *after* the TBS (including the last TBS byte) will look like + // (note the 48 - the start of the sig, and the 122 - the end of the TBS): + // [ 122, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, 0, 3, ... ] + + // The certificate in these examples has a `start` of 4, and an `end` of + // 1040. The 4 bytes is the DER SEQ of the Certificate, right before the + // SEQ of the TBSCertificate. + var end = 0; + var ends = 0; + for (var end = data.length - 1; end > 0; end--) { + if (ends === 2 && data[end] === 48) { + break; + } + if (ends < 2 && data[end] === 0) { + ends++; + } + } + + // Return our raw DER TBSCertificate: + return data.slice(start, end); +}; + +// Check Validity of Certificates +PayPro.validateCertTime = function(c, nc) { + var validityVerified = true; + var now = Date.now(); + var cBefore = c.tbsCertificate.validity.notBefore.value; + var cAfter = c.tbsCertificate.validity.notAfter.value; + var nBefore = nc.tbsCertificate.validity.notBefore.value; + var nAfter = nc.tbsCertificate.validity.notAfter.value; + if (cBefore > now || cAfter < now || nBefore > now || nAfter < now) { + validityVerified = false; + } + return validityVerified; +}; + +// Check the Issuer matches the Subject of the next certificate: +PayPro.validateCertIssuer = function(c, nc) { + var issuer = c.tbsCertificate.issuer; + var subject = nc.tbsCertificate.subject; + var issuerVerified = issuer.type === subject.type && issuer.value.every(function(issuerArray, i) { + var subjectArray = subject.value[i]; + return issuerArray.every(function(issuerObject, i) { + var subjectObject = subjectArray[i]; + + var issuerObjectType = issuerObject.type.join('.'); + var subjectObjectType = subjectObject.type.join('.'); + + var issuerObjectValue = issuerObject.value.toString('hex'); + var subjectObjectValue = subjectObject.value.toString('hex'); + + return issuerObjectType === subjectObjectType + && issuerObjectValue === subjectObjectValue; + }); + }); + return issuerVerified; +}; + PayPro.RootCerts = RootCerts; PayPro.proto = {}; @@ -205,7 +316,7 @@ PayPro.prototype.deserialize = function(buf, messageType) { return this; }; -PayPro.prototype.sign = function(key) { +PayPro.prototype.sign = function(key, returnTrust) { if (this.messageType !== 'PaymentRequest') throw new Error('Signing can only be performed on a PaymentRequest'); @@ -214,7 +325,7 @@ PayPro.prototype.sign = function(key) { if (pki_type === 'SIN') { var sig = this.sinSign(key); } else if (pki_type === 'x509+sha1' || pki_type === 'x509+sha256') { - var sig = this.x509Sign(key); + var sig = this.x509Sign(key, returnTrust); } else if (pki_type === 'none') { return this; } else { @@ -226,7 +337,7 @@ PayPro.prototype.sign = function(key) { return this; }; -PayPro.prototype.verify = function() { +PayPro.prototype.verify = function(returnTrust) { if (this.messageType !== 'PaymentRequest') throw new Error('Verifying can only be performed on a PaymentRequest'); @@ -235,7 +346,7 @@ PayPro.prototype.verify = function() { if (pki_type === 'SIN') { return this.sinVerify(); } else if (pki_type === 'x509+sha1' || pki_type === 'x509+sha256') { - return this.x509Verify(); + return this.x509Verify(returnTrust); } else if (pki_type === 'none') { return true; } @@ -260,10 +371,12 @@ PayPro.prototype.sinVerify = function() { // Helpers +PayPro.PEMtoDER = PayPro.prototype._PEMtoDER = function(pem) { return this._PEMtoDERParam(pem); }; +PayPro.PEMtoDERParam = PayPro.prototype._PEMtoDERParam = function(pem, param) { if (Buffer.isBuffer(pem)) { pem = pem.toString(); @@ -281,6 +394,7 @@ PayPro.prototype._PEMtoDERParam = function(pem, param) { }).filter(Boolean); }; +PayPro.DERtoPEM = PayPro.prototype._DERtoPEM = function(der, type) { if (typeof der === 'string') { der = new Buffer(der, 'hex'); diff --git a/lib/common/RootCerts.js b/lib/common/RootCerts.js index 5c5629e64..71501d4c9 100644 --- a/lib/common/RootCerts.js +++ b/lib/common/RootCerts.js @@ -3719,14 +3719,14 @@ function getTrusted(pem) { pem = pem.replace(/-----BEGIN CERTIFICATE-----/g, ""); pem = pem.replace(/-----END CERTIFICATE-----/g, ""); pem = pem.replace(/\s+/g, ""); - if (!Object.prototype.hasOwnProperty.call(certs, pem)) return; - return certs[pem]; + if (!Object.prototype.hasOwnProperty.call(trusted, pem)) return; + return trusted[pem]; } function getCert(name) { name = name.replace(/^s+|s+$/g, ""); - if (!Object.prototype.hasOwnProperty.call(trusted, name)) return; - return trusted[name]; + if (!Object.prototype.hasOwnProperty.call(certs, name)) return; + return certs[name]; } exports.certs = certs; diff --git a/package.json b/package.json index e3e6d59a2..6ed515e3c 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,8 @@ "event-stream": "~3.1.5", "gulp-concat": "~2.2.0", "gulp": "~3.8.2", - "preconditions": "^1.0.7" + "preconditions": "^1.0.7", + "asn1.js": "0.4.1" }, "testling": { "harness": "mocha-bdd", diff --git a/test/index.html b/test/index.html index 93ec2d856..ce887480f 100644 --- a/test/index.html +++ b/test/index.html @@ -11,6 +11,9 @@ + + + diff --git a/test/test.PayPro.js b/test/test.PayPro.js index 70969c57b..e3628017e 100644 --- a/test/test.PayPro.js +++ b/test/test.PayPro.js @@ -5,6 +5,9 @@ var should = chai.should(); var expect = chai.expect; var bitcore = bitcore || require('../bitcore'); +var is_browser = typeof process == 'undefined' + || typeof process.versions === 'undefined'; + var PayPro = bitcore.PayPro; var Key = bitcore.Key; @@ -95,6 +98,184 @@ x509.pub = new Buffer(x509.pub, 'base64'); x509.der = new Buffer(x509.der, 'base64'); x509.pem = new Buffer(x509.pem, 'base64'); +// A test PaymentRequest (with a full cert chain) from test.bitpay.com: + +var bitpayRequest = new Buffer('' + + '0801120b783530392b7368613235361a89250aa40a3082052030820408a0' + + '03020102020727a49d05046d62300d06092a864886f70d01010b05003081' + + 'b4310b30090603550406130255533110300e060355040813074172697a6f' + + '6e61311330110603550407130a53636f74747364616c65311a3018060355' + + '040a1311476f44616464792e636f6d2c20496e632e312d302b060355040b' + + '1324687474703a2f2f63657274732e676f64616464792e636f6d2f726570' + + '6f7369746f72792f313330310603550403132a476f204461646479205365' + + '6375726520436572746966696361746520417574686f72697479202d2047' + + '32301e170d3134303432363132333532365a170d31363034323631323335' + + '32365a303a3121301f060355040b1318446f6d61696e20436f6e74726f6c' + + '2056616c6964617465643115301306035504030c0c2a2e6269747061792e' + + '636f6d30820122300d06092a864886f70d01010105000382010f00308201' + + '0a0282010100e2a5dd4aea959c1d0fb016e6e05bb7011e741cdc61918c61' + + 'f9625a2f682f485f0e862ea63db61cc9161753127504de800604df36b10f' + + '46cb17ab6cb99dba8aa45a36adfb901a2fc380c89e234bce18de6639b883' + + 'e9339801673efaee1f2df77eeb82f7c39c96a2f8ef4572b634c203d9be8f' + + 'd1e0036d32fb38b6b9b5ecd5a0684345c7e9ffc5d26bc6fd69aa6619f77b' + + 'adaa4bfb989478fb2f41aa92782e40b34ba9ac4549a4e6fda76b5fc4a581' + + '853bd0de5fb5a2c6dfdc12cdfadb54e9636a6d1223705924b8be566b81ac' + + '7921078cf590a146ae397a84908ef4fc83ff5715a44ab59e9258674d9011' + + '3bb607b8d81eb268e4c6ce849497c76521795b0873950203010001a38201' + + 'ae308201aa300f0603551d130101ff04053003010100301d0603551d2504' + + '16301406082b0601050507030106082b06010505070302300e0603551d0f' + + '0101ff0404030205a030360603551d1f042f302d302ba029a02786256874' + + '74703a2f2f63726c2e676f64616464792e636f6d2f676469673273312d34' + + '392e63726c30530603551d20044c304a3048060b6086480186fd6d010717' + + '013039303706082b06010505070201162b687474703a2f2f636572746966' + + '6963617465732e676f64616464792e636f6d2f7265706f7369746f72792f' + + '307606082b06010505070101046a3068302406082b060105050730018618' + + '687474703a2f2f6f6373702e676f64616464792e636f6d2f304006082b06' + + '0105050730028634687474703a2f2f6365727469666963617465732e676f' + + '64616464792e636f6d2f7265706f7369746f72792f67646967322e637274' + + '301f0603551d2304183016801440c2bd278ecc348330a233d7fb6cb3f0b4' + + '2c80ce30230603551d11041c301a820c2a2e6269747061792e636f6d820a' + + '6269747061792e636f6d301d0603551d0e0416041485454e3b4072e2f58e' + + '377438988b5229387e967a300d06092a864886f70d01010b050003820101' + + '002d0a7ef97f988905ebbbad4e9ffb690352535211d6792516119838b55f' + + '24ff9fa4e93b6187b8517cbb0477457d3378078ef66057abe41bcafeb142' + + 'ec52443a94b88114fa069f725c6198581d97af16352727f4f35e7f2110fa' + + 'a41a0511bcfdf8e3f4a3a310278c150b10f32a962c81e8f3d5374d9cb56d' + + '893027ff4fa4e3c3e6384c1f1557ceea6fca9cbc0c110748c08b82d8f0ed' + + '9a579637ee43a2d8fec3b5b04d1f3c8f1a3e2088da2274b6bc60948bbe74' + + '4a7f8b942b41f0ae9b4afaeefbb7e0f04a0587b52efb6ebfa2d970b9de56' + + 'a068575e4bf0cf824618dc17bbeaa2cdd25d65970a9f1a06fc9fffb466a1' + + '0c9568cd651795bc2c7996975027bdbaba0ad409308204d0308203b8a003' + + '020102020107300d06092a864886f70d01010b0500308183310b30090603' + + '550406130255533110300e060355040813074172697a6f6e613113301106' + + '03550407130a53636f74747364616c65311a3018060355040a1311476f44' + + '616464792e636f6d2c20496e632e3131302f06035504031328476f204461' + + '64647920526f6f7420436572746966696361746520417574686f72697479' + + '202d204732301e170d3131303530333037303030305a170d333130353033' + + '3037303030305a3081b4310b30090603550406130255533110300e060355' + + '040813074172697a6f6e61311330110603550407130a53636f7474736461' + + '6c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e' + + '312d302b060355040b1324687474703a2f2f63657274732e676f64616464' + + '792e636f6d2f7265706f7369746f72792f313330310603550403132a476f' + + '204461646479205365637572652043657274696669636174652041757468' + + '6f72697479202d20473230820122300d06092a864886f70d010101050003' + + '82010f003082010a0282010100b9e0cb10d4af76bdd49362eb3064b88108' + + '6cc304d962178e2fff3e65cf8fce62e63c521cda16454b55ab786b638362' + + '90ce0f696c99c81a148b4ccc4533ea88dc9ea3af2bfe80619d7957c4cf2e' + + 'f43f303c5d47fc9a16bcc3379641518e114b54f828bed08cbef030381ef3' + + 'b026f86647636dde7126478f384753d1461db4e3dc00ea45acbdbc71d9aa' + + '6f00dbdbcd303a794f5f4c47f81def5bc2c49d603bb1b24391d8a4334eea' + + 'b3d6274fad258aa5c6f4d5d0a6ae7405645788b54455d42d2a3a3ef8b8bd' + + 'e9320a029464c4163a50f14aaee77933af0c20077fe8df0439c269026c63' + + '52fa77c11bc87487c8b993185054354b694ebc3bd3492e1fdcc1d252fb02' + + '03010001a382011a30820116300f0603551d130101ff040530030101ff30' + + '0e0603551d0f0101ff040403020106301d0603551d0e0416041440c2bd27' + + '8ecc348330a233d7fb6cb3f0b42c80ce301f0603551d230418301680143a' + + '9a8507106728b6eff6bd05416e20c194da0fde303406082b060105050701' + + '0104283026302406082b060105050730018618687474703a2f2f6f637370' + + '2e676f64616464792e636f6d2f30350603551d1f042e302c302aa028a026' + + '8624687474703a2f2f63726c2e676f64616464792e636f6d2f6764726f6f' + + '742d67322e63726c30460603551d20043f303d303b0604551d2000303330' + + '3106082b06010505070201162568747470733a2f2f63657274732e676f64' + + '616464792e636f6d2f7265706f7369746f72792f300d06092a864886f70d' + + '01010b05000382010100087e6c9310c838b896a9904bffa15f4f04ef6c3e' + + '9c8806c9508fa673f757311bbebce42fdbf8bad35be0b4e7e679620e0ca2' + + 'd76a637331b5f5a848a43b082da25d90d7b47c254f115630c4b6449d7b2c' + + '9de55ee6ef0c61aabfe42a1bee849eb8837dc143ce44a713700d911ff4c8' + + '13ad8360d9d872a873241eb5ac220eca17896258441bab892501000fcdc4' + + '1b62db51b4d30f512a9bf4bc73fc76ce36a4cdd9d82ceaae9bf52ab290d1' + + '4d75188a3f8a4190237d5b4bfea403589b46b2c3606083f87d5041cec2a1' + + '90c3bbef022fd21554ee4415d90aaea78a33edb12d763626dc04eb9ff761' + + '1f15dc876fee469628ada1267d0a09a72e04a38dbcf8bc0430010a810930' + + '82047d30820365a00302010202031be715300d06092a864886f70d01010b' + + '05003063310b30090603550406130255533121301f060355040a13185468' + + '6520476f2044616464792047726f75702c20496e632e3131302f06035504' + + '0b1328476f20446164647920436c61737320322043657274696669636174' + + '696f6e20417574686f72697479301e170d3134303130313037303030305a' + + '170d3331303533303037303030305a308183310b30090603550406130255' + + '533110300e060355040813074172697a6f6e61311330110603550407130a' + + '53636f74747364616c65311a3018060355040a1311476f44616464792e63' + + '6f6d2c20496e632e3131302f06035504031328476f20446164647920526f' + + '6f7420436572746966696361746520417574686f72697479202d20473230' + + '820122300d06092a864886f70d01010105000382010f003082010a028201' + + '0100bf716208f1fa5934f71bc918a3f7804958e9228313a6c52043013b84' + + 'f1e685499f27eaf6841b4ea0b4db7098c73201b1053e074eeef4fa4f2f59' + + '3022e7ab19566be28007fcf316758039517be5f935b6744ea98d8213e4b6' + + '3fa90383faa2be8a156a7fde0bc3b6191405caeac3a804943b467c320df3' + + '006622c88d696d368c1118b7d3b21c60b438fa028cced3dd4607de0a3eeb' + + '5d7cc87cfbb02b53a4926269512505611a44818c2ca9439623dfac3a819a' + + '0e29c51ca9e95d1eb69e9e300a39cef18880fb4b5dcc32ec856243253402' + + '56270191b43b702a3f6eb1e89c88017d9fd4f9db536d609dbf2ce758abb8' + + '5f46fccec41b033c09eb49315c6946b3e0470203010001a3820117308201' + + '13300f0603551d130101ff040530030101ff300e0603551d0f0101ff0404' + + '03020106301d0603551d0e041604143a9a8507106728b6eff6bd05416e20' + + 'c194da0fde301f0603551d23041830168014d2c4b0d291d44c1171b361cb' + + '3da1fedda86ad4e3303406082b0601050507010104283026302406082b06' + + '0105050730018618687474703a2f2f6f6373702e676f64616464792e636f' + + '6d2f30320603551d1f042b30293027a025a0238621687474703a2f2f6372' + + '6c2e676f64616464792e636f6d2f6764726f6f742e63726c30460603551d' + + '20043f303d303b0604551d20003033303106082b06010505070201162568' + + '747470733a2f2f63657274732e676f64616464792e636f6d2f7265706f73' + + '69746f72792f300d06092a864886f70d01010b05000382010100590b53bd' + + '928611a7247bed5b31cf1d1f6c70c5b86ebe4ebbf6be9750e1307fba285c' + + '6294c2e37e33f7fb427685db951c8c225875090c886567390a1609c5a038' + + '97a4c523933fb418a601064491e3a76927b45a257f3ab732cddd84ff2a38' + + '2933a4dd67b285fea188201c5089c8dc2af64203374ce688dfd5af24f2b1' + + 'c3dfccb5ece0995eb74954203c94180cc71c521849a46de1b3580bc9d8ec' + + 'd9ae1c328e28700de2fea6179e840fbd5770b35ae91fa08653bbef7cff69' + + '0be048c3b7930bc80a54c4ac5d1467376ccaa52f310837aa6e6f8cbc9be2' + + '575d2481af97979c84ad6cac374c66f361911120e4be309f7aa42909b0e1' + + '345f6477184051df8c30a6af0a840830820400308202e8a0030201020201' + + '00300d06092a864886f70d01010505003063310b30090603550406130255' + + '533121301f060355040a131854686520476f2044616464792047726f7570' + + '2c20496e632e3131302f060355040b1328476f20446164647920436c6173' + + '7320322043657274696669636174696f6e20417574686f72697479301e17' + + '0d3034303632393137303632305a170d3334303632393137303632305a30' + + '63310b30090603550406130255533121301f060355040a13185468652047' + + '6f2044616464792047726f75702c20496e632e3131302f060355040b1328' + + '476f20446164647920436c61737320322043657274696669636174696f6e' + + '20417574686f7269747930820120300d06092a864886f70d010101050003' + + '82010d00308201080282010100de9dd7ea571849a15bebd75f4886eabedd' + + 'ffe4ef671cf46568b35771a05e77bbed9b49e970803d561863086fdaf2cc' + + 'd03f7f0254225410d8b281d4c0753d4b7fc777c33e78ab1a03b5206b2f6a' + + '2bb1c5887ec4bb1eb0c1d845276faa3758f78726d7d82df6a917b71f7236' + + '4ea6173f659892db2a6e5da2fe88e00bde7fe58d15e1ebcb3ad5e212a213' + + '2dd88eaf5f123da0080508b65ca565380445991ea3606074c541a572621b' + + '62c51f6f5f1a42be025165a8ae23186afc7803a94d7f80c3faab5afca140' + + 'a4ca1916feb2c8ef5e730dee77bd9af67998bcb10767a2150ddda058c644' + + '7b0a3e62285fba41075358cf117e3874c5f8ffb569908f8474ea971baf02' + + '0103a381c03081bd301d0603551d0e04160414d2c4b0d291d44c1171b361' + + 'cb3da1fedda86ad4e330818d0603551d230481853081828014d2c4b0d291' + + 'd44c1171b361cb3da1fedda86ad4e3a167a4653063310b30090603550406' + + '130255533121301f060355040a131854686520476f204461646479204772' + + '6f75702c20496e632e3131302f060355040b1328476f2044616464792043' + + '6c61737320322043657274696669636174696f6e20417574686f72697479' + + '820100300c0603551d13040530030101ff300d06092a864886f70d010105' + + '05000382010100324bf3b2ca3e91fc12c6a1078c8e77a03306145c901e18' + + 'f708a63d0a19f98780116e69e4961730ff3491637238eecc1c01a31d9428' + + 'a431f67ac454d7f6e5315803a2ccce62db944573b5bf45c924b5d58202ad' + + '2379698db8b64dcecf4cca3323e81c88aa9d8b416e16c920e5899ecd3bda' + + '70f77e992620145425ab6e7385e69b219d0a6c820ea8f8c20cfa101e6c96' + + 'ef870dc40f618badee832b95f88e92847239eb20ea83ed83cd976e08bceb' + + '4e26b6732be4d3f64cfe2671e26111744aff571a870f75482ecf516917a0' + + '02126195d5d140b2104ceec4ac1043a6a59e0ad595629a0dcf8882c5320c' + + 'e42b9f45e60d9f289cb1b92a5a57ad370faf1d7fdbbd9f22a1010a047465' + + '7374122008c0c9e714121976a914176d7c5d60da6f8c82de86671a1fb776' + + '028538ca88ac18c6f5d89f0520cafcd89f052a395061796d656e74207265' + + '717565737420666f722042697450617920696e766f69636520434d577075' + + '46736a676d51325a4c6979476663463157323068747470733a2f2f746573' + + '742e6269747061792e636f6d2f692f434d57707546736a676d51325a4c69' + + '794766634631572a80021566366ab78842a514c056ca7ecb76481262cac7' + + '4cc4c4ccdc82c4980bc3300de67836d61d3e06dc8c90798a7774c21c7ad4' + + 'fe634b85faa8719d6402411bb720396ae03cbb4e14f06f7894a66b208b99' + + 'f727fab35d32f4f2148294d24bea1b3f240c159d0fd3ee4a32e5f926bf7c' + + '05eb7a3f75e01d9af81254cfbb61606467750ea7e0a1536728358e0898d0' + + '6f57235e4096d2caf647ae58dff645be80c9b3555fa96c81efa07d421977' + + 'd26214ad4f1ff642a93d0925656aeab454fa0b60fcbb6c1bc570eb6e43e7' + + '613392f37900748635ae381534bfaa558792bc46028b9efce391423a9c12' + + '01f76292614b30a14272e837f3813045b035f3d42f4f76f48acd', + 'hex'); + describe('PayPro', function() { it('should be able to create class', function() { @@ -403,6 +584,14 @@ describe('PayPro', function() { var verify = paypro.verify(); verify.should.equal(true); + + var trust = paypro.verify(true); + trust.selfSigned.should.equal(1); + trust.isChain.should.equal(false); + trust.verified.should.equal(true); + trust.caTrusted.should.equal(false); + should.equal(null, trust.caName); + trust.chainVerified.should.equal(false); }); }); @@ -496,6 +685,14 @@ describe('PayPro', function() { var verify = paypro.x509Verify(); verify.should.equal(true); + + var trust = paypro.x509Verify(true); + trust.selfSigned.should.equal(1); + trust.isChain.should.equal(false); + trust.verified.should.equal(true); + trust.caTrusted.should.equal(false); + should.equal(null, trust.caName); + trust.chainVerified.should.equal(false); }); }); @@ -549,6 +746,108 @@ describe('PayPro', function() { var verify = paypro.x509Verify(); verify.should.equal(true); + + var trust = paypro.x509Verify(true); + trust.selfSigned.should.equal(1); + trust.isChain.should.equal(false); + trust.verified.should.equal(true); + trust.caTrusted.should.equal(false); + should.equal(null, trust.caName); + trust.chainVerified.should.equal(false); + }); + }); + + describe('#x509+sha256Verify ', function() { + it('should verify a real PaymentRequest', function() { + var data = PayPro.PaymentRequest.decode(bitpayRequest); + var pr = new PayPro(); + pr = pr.makePaymentRequest(data); + + // PaymentRequest + var ver = pr.get('payment_details_version'); + var pki_type = pr.get('pki_type'); + var pki_data = pr.get('pki_data'); + var details = pr.get('serialized_payment_details'); + var sig = pr.get('signature'); + + pki_data = PayPro.X509Certificates.decode(pki_data); + pki_data = pki_data.certificate; + + ver.should.equal(1); + pki_type.should.equal('x509+sha256'); + pki_data.length.should.equal(4); + sig.toString('hex').should.equal('' + + '1566366ab78842a514c056ca7ecb76481262cac74cc4c4ccdc' + + '82c4980bc3300de67836d61d3e06dc8c90798a7774c21c7ad4' + + 'fe634b85faa8719d6402411bb720396ae03cbb4e14f06f7894' + + 'a66b208b99f727fab35d32f4f2148294d24bea1b3f240c159d' + + '0fd3ee4a32e5f926bf7c05eb7a3f75e01d9af81254cfbb6160' + + '6467750ea7e0a1536728358e0898d06f57235e4096d2caf647' + + 'ae58dff645be80c9b3555fa96c81efa07d421977d26214ad4f' + + '1ff642a93d0925656aeab454fa0b60fcbb6c1bc570eb6e43e7' + + '613392f37900748635ae381534bfaa558792bc46028b9efce3' + + '91423a9c1201f76292614b30a14272e837f3813045b035f3d4' + + '2f4f76f48acd'); + + if (is_browser) { + var type = 'SHA256'; + var pem = PayPro.prototype._DERtoPEM(pki_data[0], 'CERTIFICATE'); + var buf = pr.serializeForSig(); + var jsrsaSig = new KJUR.crypto.Signature({ + alg: type + 'withRSA', + prov: 'cryptojs/jsrsa' + }); + var signedCert = pki_data[0]; + var der = signedCert.toString('hex'); + // var pem = PayPro.DERtoPEM(der, 'CERTIFICATE'); + var pem = KJUR.asn1.ASN1Util.getPEMStringFromHex(der, 'CERTIFICATE'); + jsrsaSig.initVerifyByCertificatePEM(pem); + jsrsaSig.updateHex(buf.toString('hex')); + jsrsaSig.verify(sig.toString('hex')).should.equal(true); + } else { + var crypto = require('crypto'); + var type = 'SHA256'; + var pem = PayPro.DERtoPEM(pki_data[0], 'CERTIFICATE'); + var buf = pr.serializeForSig(); + var verifier = crypto.createVerify('RSA-' + type); + verifier.update(buf); + verifier.verify(pem, sig).should.equal(true); + } + + // Verify Signature + var verified = pr.x509Verify(); + verified.should.equal(true); + + // Verify Signature with trust properties + var trust = pr.x509Verify(true); + trust.selfSigned.should.equal(0); + trust.isChain.should.equal(true); + trust.verified.should.equal(true); + trust.caTrusted.should.equal(true); + trust.caName.should.equal('Go Daddy Class 2 CA'); + trust.chainVerified.should.equal(true); + + // PaymentDetails + details = PayPro.PaymentDetails.decode(details); + var pd = new PayPro(); + pd = pd.makePaymentDetails(details); + var network = pd.get('network'); + var outputs = pd.get('outputs'); + var time = pd.get('time'); + var expires = pd.get('expires'); + var memo = pd.get('memo'); + var payment_url = pd.get('payment_url'); + var merchant_data = pd.get('merchant_data'); + + network.should.equal('test'); + outputs.length.should.equal(1); + outputs[0].amount.should.not.equal(undefined); + outputs[0].script.should.not.equal(undefined); + time.should.equal(1408645830); + expires.should.equal(1408646730); + memo.should.equal('Payment request for BitPay invoice CMWpuFsjgmQ2ZLiyGfcF1W'); + payment_url.should.equal('https://test.bitpay.com/i/CMWpuFsjgmQ2ZLiyGfcF1W'); + should.equal(null, merchant_data); }); });