diff --git a/lib/bip70/payment.js b/lib/bip70/payment.js index eebfaaf9..af7a28b9 100644 --- a/lib/bip70/payment.js +++ b/lib/bip70/payment.js @@ -15,6 +15,16 @@ var PaymentDetails = require('./paymentdetails'); var ProtoReader = protobuf.ProtoReader; var ProtoWriter = protobuf.ProtoWriter; +/** + * Represents a BIP70 payment. + * @constructor + * @param {Object?} options + * @property {Buffer} merchantData + * @property {TX[]} transactions + * @property {Output[]} refundTo + * @property {String|null} memo + */ + function Payment(options) { if (!(this instanceof Payment)) return new Payment(options); @@ -28,6 +38,13 @@ function Payment(options) { this.fromOptions(options); } +/** + * Inject properties from options. + * @private + * @param {Object} options + * @returns {Payment} + */ + Payment.prototype.fromOptions = function fromOptions(options) { var i, tx, output; @@ -58,13 +75,43 @@ Payment.prototype.fromOptions = function fromOptions(options) { return this; }; +/** + * Instantiate payment from options. + * @param {Object} options + * @returns {Payment} + */ + Payment.fromOptions = function fromOptions(options) { return new Payment().fromOptions(options); }; +/** + * Set payment details. + * @method + * @alias Payment#setData + * @param {Object} data + * @param {String?} enc + */ + Payment.prototype.setData = PaymentDetails.prototype.setData; + +/** + * Get payment details. + * @method + * @alias Payment#getData + * @param {String?} enc + * @returns {String|Object|null} + */ + Payment.prototype.getData = PaymentDetails.prototype.getData; +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + * @returns {Payment} + */ + Payment.prototype.fromRaw = function fromRaw(data) { var br = new ProtoReader(data); var tx, op, output; @@ -89,12 +136,23 @@ Payment.prototype.fromRaw = function fromRaw(data) { return this; }; +/** + * Instantiate payment from serialized data. + * @param {Buffer} data + * @returns {Payment} + */ + Payment.fromRaw = function fromRaw(data, enc) { if (typeof data === 'string') data = new Buffer(data, enc); return new Payment().fromRaw(data); }; +/** + * Serialize the payment (protobuf). + * @returns {Buffer} + */ + Payment.prototype.toRaw = function toRaw() { var bw = new ProtoWriter(); var i, tx, op, output; @@ -121,4 +179,8 @@ Payment.prototype.toRaw = function toRaw() { return bw.render(); }; +/* + * Expose + */ + module.exports = Payment; diff --git a/lib/bip70/paymentack.js b/lib/bip70/paymentack.js index 47350b95..76ff95d9 100644 --- a/lib/bip70/paymentack.js +++ b/lib/bip70/paymentack.js @@ -12,6 +12,13 @@ var Payment = require('./payment'); var ProtoReader = protobuf.ProtoReader; var ProtoWriter = protobuf.ProtoWriter; +/** + * Represents a BIP70 payment ack. + * @param {Object?} options + * @property {Payment} payment + * @property {String|null} memo + */ + function PaymentACK(options) { if (!(this instanceof PaymentACK)) return new PaymentACK(options); @@ -23,6 +30,13 @@ function PaymentACK(options) { this.fromOptions(options); } +/** + * Inject properties from options. + * @private + * @param {Object} options + * @returns {PaymentACK} + */ + PaymentACK.prototype.fromOptions = function fromOptions(options) { if (options.payment) this.payment.fromOptions(options.payment); @@ -35,10 +49,23 @@ PaymentACK.prototype.fromOptions = function fromOptions(options) { return this; }; +/** + * Instantiate payment ack from options. + * @param {Object} options + * @returns {PaymentACK} + */ + PaymentACK.fromOptions = function fromOptions(options) { return new PaymentACK().fromOptions(options); }; +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + * @returns {PaymentACK} + */ + PaymentACK.prototype.fromRaw = function fromRaw(data) { var br = new ProtoReader(data); @@ -48,12 +75,23 @@ PaymentACK.prototype.fromRaw = function fromRaw(data) { return this; }; +/** + * Instantiate payment ack from serialized data. + * @param {Buffer} data + * @returns {PaymentACK} + */ + PaymentACK.fromRaw = function fromRaw(data, enc) { if (typeof data === 'string') data = new Buffer(data, enc); return new PaymentACK().fromRaw(data); }; +/** + * Serialize the payment ack (protobuf). + * @returns {Buffer} + */ + PaymentACK.prototype.toRaw = function toRaw() { var bw = new ProtoWriter(); @@ -65,4 +103,8 @@ PaymentACK.prototype.toRaw = function toRaw() { return bw.render(); }; +/* + * Expose + */ + module.exports = PaymentACK; diff --git a/lib/bip70/paymentdetails.js b/lib/bip70/paymentdetails.js index 3745f336..b1970127 100644 --- a/lib/bip70/paymentdetails.js +++ b/lib/bip70/paymentdetails.js @@ -13,6 +13,18 @@ var protobuf = require('../utils/protobuf'); var ProtoReader = protobuf.ProtoReader; var ProtoWriter = protobuf.ProtoWriter; +/** + * Represents BIP70 payment details. + * @param {Object?} options + * @property {String|null} network + * @property {Output[]} outputs + * @property {Number} time + * @property {Number} expires + * @property {String|null} memo + * @property {String|null} paymentUrl + * @property {Buffer|null} merchantData + */ + function PaymentDetails(options) { if (!(this instanceof PaymentDetails)) return new PaymentDetails(options); @@ -29,6 +41,13 @@ function PaymentDetails(options) { this.fromOptions(options); } +/** + * Inject properties from options. + * @private + * @param {Object} options + * @returns {PaymentDetails} + */ + PaymentDetails.prototype.fromOptions = function fromOptions(options) { var i, output; @@ -71,16 +90,33 @@ PaymentDetails.prototype.fromOptions = function fromOptions(options) { return this; }; +/** + * Instantiate payment details from options. + * @param {Object} options + * @returns {PaymentDetails} + */ + PaymentDetails.fromOptions = function fromOptions(options) { return new PaymentDetails().fromOptions(options); }; +/** + * Test whether the payment is expired. + * @returns {Boolean} + */ + PaymentDetails.prototype.isExpired = function isExpired() { if (this.expires === -1) return false; return util.now() > this.expires; }; +/** + * Set payment details. + * @param {Object} data + * @param {String?} enc + */ + PaymentDetails.prototype.setData = function setData(data, enc) { if (data == null || Buffer.isBuffer(data)) { this.merchantData = data; @@ -96,6 +132,12 @@ PaymentDetails.prototype.setData = function setData(data, enc) { this.merchantData = new Buffer(data, enc); }; +/** + * Get payment details. + * @param {String?} enc + * @returns {String|Object|null} + */ + PaymentDetails.prototype.getData = function getData(enc) { var data = this.merchantData; @@ -118,6 +160,13 @@ PaymentDetails.prototype.getData = function getData(enc) { return data.toString(enc); }; +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + * @returns {PaymentDetails} + */ + PaymentDetails.prototype.fromRaw = function fromRaw(data) { var br = new ProtoReader(data); var op, output; @@ -141,12 +190,23 @@ PaymentDetails.prototype.fromRaw = function fromRaw(data) { return this; }; +/** + * Instantiate payment details from serialized data. + * @param {Buffer} data + * @returns {PaymentDetails} + */ + PaymentDetails.fromRaw = function fromRaw(data, enc) { if (typeof data === 'string') data = new Buffer(data, enc); return new PaymentDetails().fromRaw(data); }; +/** + * Serialize the payment details (protobuf). + * @returns {Buffer} + */ + PaymentDetails.prototype.toRaw = function toRaw() { var bw = new ProtoWriter(); var i, op, output; @@ -179,4 +239,8 @@ PaymentDetails.prototype.toRaw = function toRaw() { return bw.render(); }; +/* + * Expose + */ + module.exports = PaymentDetails; diff --git a/lib/bip70/paymentrequest.js b/lib/bip70/paymentrequest.js index 31a6bcef..cb018d6d 100644 --- a/lib/bip70/paymentrequest.js +++ b/lib/bip70/paymentrequest.js @@ -16,6 +16,16 @@ var PaymentDetails = require('./paymentdetails'); var ProtoReader = protobuf.ProtoReader; var ProtoWriter = protobuf.ProtoWriter; +/** + * Represents a BIP70 payment request. + * @param {Object?} options + * @property {Number} version + * @property {String|null} pkiType + * @property {Buffer|null} pkiData + * @property {PaymentDetails} paymentDetails + * @property {Buffer|null} signature + */ + function PaymentRequest(options) { if (!(this instanceof PaymentRequest)) return new PaymentRequest(options); @@ -30,6 +40,13 @@ function PaymentRequest(options) { this.fromOptions(options); } +/** + * Inject properties from options. + * @private + * @param {Object} options + * @returns {PaymentRequest} + */ + PaymentRequest.prototype.fromOptions = function fromOptions(options) { if (options.version != null) { assert(util.isNumber(options.version)); @@ -60,10 +77,23 @@ PaymentRequest.prototype.fromOptions = function fromOptions(options) { return this; }; +/** + * Instantiate payment request from options. + * @param {Object} options + * @returns {PaymentRequest} + */ + PaymentRequest.fromOptions = function fromOptions(options) { return new PaymentRequest().fromOptions(options); }; +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + * @returns {PaymentRequest} + */ + PaymentRequest.prototype.fromRaw = function fromRaw(data) { var br = new ProtoReader(data); @@ -76,12 +106,23 @@ PaymentRequest.prototype.fromRaw = function fromRaw(data) { return this; }; +/** + * Instantiate payment request from serialized data. + * @param {Buffer} data + * @returns {PaymentRequest} + */ + PaymentRequest.fromRaw = function fromRaw(data, enc) { if (typeof data === 'string') data = new Buffer(data, enc); return new PaymentRequest().fromRaw(data); }; +/** + * Serialize the payment request (protobuf). + * @returns {Buffer} + */ + PaymentRequest.prototype.toRaw = function toRaw() { var bw = new ProtoWriter(); @@ -102,6 +143,11 @@ PaymentRequest.prototype.toRaw = function toRaw() { return bw.render(); }; +/** + * Get payment request signature algorithm. + * @returns {Object|null} + */ + PaymentRequest.prototype.getAlgorithm = function getAlgorithm() { var parts; @@ -119,9 +165,14 @@ PaymentRequest.prototype.getAlgorithm = function getAlgorithm() { if (parts[1] !== 'sha1' && parts[1] !== 'sha256') return; - return { key: parts[0], hash: parts[1] }; + return new Algorithm(parts[0], parts[1]); }; +/** + * Serialize payment request for sighash. + * @returns {Buffer} + */ + PaymentRequest.prototype.signatureData = function signatureData() { var signature = this.signature; var data; @@ -135,12 +186,22 @@ PaymentRequest.prototype.signatureData = function signatureData() { return data; }; +/** + * Get signature hash. + * @returns {Hash} + */ + PaymentRequest.prototype.signatureHash = function signatureHash() { var alg = this.getAlgorithm(); assert(alg, 'No hash algorithm available.'); return crypto.hash(alg.hash, this.signatureData()); }; +/** + * Set x509 certificate chain. + * @param {Buffer[]} chain + */ + PaymentRequest.prototype.setChain = function setChain(chain) { var bw = new ProtoWriter(); var i, cert, pem; @@ -162,6 +223,11 @@ PaymentRequest.prototype.setChain = function setChain(chain) { this.pkiData = bw.render(); }; +/** + * Get x509 certificate chain. + * @returns {Buffer[]} + */ + PaymentRequest.prototype.getChain = function getChain() { var chain = []; var br; @@ -177,6 +243,12 @@ PaymentRequest.prototype.getChain = function getChain() { return chain; }; +/** + * Sign payment request (chain must be set). + * @param {Buffer} key + * @param {Buffer[]?} chain + */ + PaymentRequest.prototype.sign = function sign(key, chain) { var alg, msg; @@ -195,6 +267,11 @@ PaymentRequest.prototype.sign = function sign(key, chain) { this.signature = x509.signSubject(alg.hash, msg, key, chain); }; +/** + * Verify payment request signature. + * @returns {Boolean} + */ + PaymentRequest.prototype.verify = function verify() { var alg, msg, sig, chain; @@ -216,6 +293,11 @@ PaymentRequest.prototype.verify = function verify() { return x509.verifySubject(alg.hash, msg, sig, chain); }; +/** + * Verify x509 certificate chain. + * @returns {Boolean} + */ + PaymentRequest.prototype.verifyChain = function verifyChain() { if (!this.pkiType || this.pkiType === 'none') return true; @@ -223,6 +305,11 @@ PaymentRequest.prototype.verifyChain = function verifyChain() { return x509.verifyChain(this.getChain()); }; +/** + * Get root certificate authority. + * @returns {Object|null} + */ + PaymentRequest.prototype.getCA = function getCA() { var chain, root; @@ -239,11 +326,32 @@ PaymentRequest.prototype.getCA = function getCA() { if (!root) return; - return { - name: x509.getCAName(root), - trusted: x509.isTrusted(root), - cert: root - }; + return new CA(root); }; +/** + * Algorithm + * @constructor + */ + +function Algorithm(key, hash) { + this.key = key; + this.hash = hash; +} + +/** + * CA + * @constructor + */ + +function CA(root) { + this.name = x509.getCAName(root); + this.trusted = x509.isTrusted(root); + this.cert = root; +} + +/* + * Expose + */ + module.exports = PaymentRequest; diff --git a/lib/bip70/x509.js b/lib/bip70/x509.js index c7e19b30..17a2641d 100644 --- a/lib/bip70/x509.js +++ b/lib/bip70/x509.js @@ -14,6 +14,71 @@ var crypto = require('../crypto/crypto'); var pk = require('./pk'); var x509 = exports; +/** + * Map of trusted root certs. + * @type {Object} + */ + +x509.trusted = {}; + +/** + * Whether to allow untrusted root + * certs during verification. + * @type {Boolean} + */ + +x509.allowUntrusted = false; + +/** + * OID to algorithm map for PKI. + * @const {Object} + * @see https://www.ietf.org/rfc/rfc2459.txt + * @see https://tools.ietf.org/html/rfc3279 + * @see http://oid-info.com/get/1.2.840.10040.4 + * @see http://oid-info.com/get/1.2.840.113549.1.1 + * @see http://oid-info.com/get/1.2.840.10045.4.3 + */ + +x509.oid = { + '1.2.840.10040.4.1' : { key: 'dsa', hash: null }, + '1.2.840.10040.4.2' : { key: 'dsa', hash: null }, + '1.2.840.10040.4.3' : { key: 'dsa', hash: 'sha1' }, + '1.2.840.113549.1.1.1' : { key: 'rsa', hash: null }, + '1.2.840.113549.1.1.2' : { key: 'rsa', hash: 'md2' }, + '1.2.840.113549.1.1.3' : { key: 'rsa', hash: 'md4' }, + '1.2.840.113549.1.1.4' : { key: 'rsa', hash: 'md5' }, + '1.2.840.113549.1.1.5' : { key: 'rsa', hash: 'sha1' }, + '1.2.840.113549.1.1.11': { key: 'rsa', hash: 'sha256' }, + '1.2.840.113549.1.1.12': { key: 'rsa', hash: 'sha384' }, + '1.2.840.113549.1.1.13': { key: 'rsa', hash: 'sha512' }, + '1.2.840.113549.1.1.14': { key: 'rsa', hash: 'sha224' }, + '1.2.840.10045.2.1' : { key: 'ecdsa', hash: null }, + '1.2.840.10045.4.1' : { key: 'ecdsa', hash: 'sha1' }, + '1.2.840.10045.4.3.1' : { key: 'ecdsa', hash: 'sha224' }, + '1.2.840.10045.4.3.2' : { key: 'ecdsa', hash: 'sha256' }, + '1.2.840.10045.4.3.3' : { key: 'ecdsa', hash: 'sha384' }, + '1.2.840.10045.4.3.4' : { key: 'ecdsa', hash: 'sha512' } +}; + +/** + * OID to curve name map for ECDSA. + * @type {Object} + */ + +x509.curves = { + '1.3.132.0.33': 'p224', + '1.2.840.10045.3.1.7': 'p256', + '1.3.132.0.34': 'p384', + '1.3.132.0.35': 'p521' +}; + +/** + * Retrieve cert value by OID. + * @param {Object} cert + * @param {String} oid + * @returns {String} + */ + x509.getSubjectOID = function getSubjectOID(cert, oid) { var subject = cert.tbs.subject; var i, entry; @@ -25,6 +90,13 @@ x509.getSubjectOID = function getSubjectOID(cert, oid) { } }; +/** + * Try to retrieve CA name by checking + * for a few different OIDs. + * @param {Object} cert + * @returns {String} + */ + x509.getCAName = function getCAName(cert) { // This seems to work the best in practice // for getting a human-readable and @@ -41,8 +113,12 @@ x509.getCAName = function getCAName(cert) { || 'Unknown'; }; -x509.trusted = {}; -x509.allowUntrusted = false; +/** + * Test whether a cert is trusted by hashing + * and looking it up in the trusted map. + * @param {Object} cert + * @returns {Buffer} + */ x509.isTrusted = function isTrusted(cert) { var fingerprint = crypto.sha256(cert.raw); @@ -50,6 +126,11 @@ x509.isTrusted = function isTrusted(cert) { return x509.trusted[hash] === true; }; +/** + * Add root certificates to the trusted map. + * @param {Buffer[]} certs + */ + x509.setTrust = function setTrust(certs) { var i, cert, pem, hash; @@ -85,52 +166,34 @@ x509.setTrust = function setTrust(certs) { } }; -/* - * https://www.ietf.org/rfc/rfc2459.txt - * https://tools.ietf.org/html/rfc3279 - * http://oid-info.com/get/1.2.840.10040.4 - * http://oid-info.com/get/1.2.840.113549.1.1 - * http://oid-info.com/get/1.2.840.10045.4.3 +/** + * Retrieve key algorithm from cert. + * @param {Object} cert + * @returns {Object} */ -x509.oid = { - '1.2.840.10040.4.1' : { key: 'dsa', hash: null }, - '1.2.840.10040.4.2' : { key: 'dsa', hash: null }, - '1.2.840.10040.4.3' : { key: 'dsa', hash: 'sha1' }, - '1.2.840.113549.1.1.1' : { key: 'rsa', hash: null }, - '1.2.840.113549.1.1.2' : { key: 'rsa', hash: 'md2' }, - '1.2.840.113549.1.1.3' : { key: 'rsa', hash: 'md4' }, - '1.2.840.113549.1.1.4' : { key: 'rsa', hash: 'md5' }, - '1.2.840.113549.1.1.5' : { key: 'rsa', hash: 'sha1' }, - '1.2.840.113549.1.1.11': { key: 'rsa', hash: 'sha256' }, - '1.2.840.113549.1.1.12': { key: 'rsa', hash: 'sha384' }, - '1.2.840.113549.1.1.13': { key: 'rsa', hash: 'sha512' }, - '1.2.840.113549.1.1.14': { key: 'rsa', hash: 'sha224' }, - '1.2.840.10045.2.1' : { key: 'ecdsa', hash: null }, - '1.2.840.10045.4.1' : { key: 'ecdsa', hash: 'sha1' }, - '1.2.840.10045.4.3.1' : { key: 'ecdsa', hash: 'sha224' }, - '1.2.840.10045.4.3.2' : { key: 'ecdsa', hash: 'sha256' }, - '1.2.840.10045.4.3.3' : { key: 'ecdsa', hash: 'sha384' }, - '1.2.840.10045.4.3.4' : { key: 'ecdsa', hash: 'sha512' } -}; - -x509.curves = { - '1.3.132.0.33': 'p224', - '1.2.840.10045.3.1.7': 'p256', - '1.3.132.0.34': 'p384', - '1.3.132.0.35': 'p521' -}; - x509.getKeyAlgorithm = function getKeyAlgorithm(cert) { var alg = cert.tbs.pubkey.alg.alg; return x509.oid[alg]; }; +/** + * Retrieve signature algorithm from cert. + * @param {Object} cert + * @returns {Object} + */ + x509.getSigAlgorithm = function getSigAlgorithm(cert) { var alg = cert.sigAlg.alg; return x509.oid[alg]; }; +/** + * Lookup curve based on key parameters. + * @param {Buffer} params + * @returns {Object} + */ + x509.getCurve = function getCurve(params) { var oid; @@ -146,6 +209,12 @@ x509.getCurve = function getCurve(params) { return x509.curves[oid]; }; +/** + * Parse a DER formatted cert. + * @param {Buffer} der + * @returns {Object|null} + */ + x509.parse = function parse(der) { try { return ASN1.parseCert(der); @@ -154,6 +223,12 @@ x509.parse = function parse(der) { } }; +/** + * Get cert public key. + * @param {Object} cert + * @returns {Object|null} + */ + x509.getPublicKey = function getPublicKey(cert) { var alg = x509.getKeyAlgorithm(cert); var key, params, curve; @@ -175,12 +250,25 @@ x509.getPublicKey = function getPublicKey(cert) { }; }; +/** + * Verify cert expiration time. + * @param {Object} cert + * @returns {Boolean} + */ + x509.verifyTime = function verifyTime(cert) { var time = cert.tbs.validity; var now = util.now(); return now > time.notBefore && now < time.notAfter; }; +/** + * Get signature key info from cert chain. + * @param {Buffer} key + * @param {Buffer[]} chain + * @returns {Object} + */ + x509.getSigningKey = function getSigningKey(key, chain) { var cert, pub, curve; @@ -213,35 +301,65 @@ x509.getSigningKey = function getSigningKey(key, chain) { return key; }; +/** + * Sign a hash with the chain signing key. + * @param {String} hash + * @param {Buffer} msg + * @param {Buffer} key + * @param {Buffer[]} chain + * @returns {Buffer} + */ + x509.signSubject = function signSubject(hash, msg, key, chain) { var priv = x509.getSigningKey(key, chain); return pk.sign(hash, msg, priv); }; +/** + * Get chain verification key. + * @param {Buffer[]} chain + * @returns {Object|null} + */ + x509.getVerifyKey = function getVerifyKey(chain) { var cert, key; if (chain.length === 0) - return false; + return; cert = x509.parse(chain[0]); if (!cert) - return false; + return; key = x509.getPublicKey(cert); if (!key) - return false; + return; return key; }; +/** + * Verify a sighash against chain verification key. + * @param {String} hash + * @param {Buffer} msg + * @param {Buffer} sig + * @param {Buffer[]} chain + * @returns {Boolean} + */ + x509.verifySubject = function verifySubject(hash, msg, sig, chain) { var key = x509.getVerifyKey(chain); return pk.verify(hash, msg, sig, key); }; +/** + * Parse certificate chain. + * @param {Buffer[]} chain + * @returns {Object[]} + */ + x509.parseChain = function parseChain(chain) { var certs = []; var i, cert; @@ -258,6 +376,12 @@ x509.parseChain = function parseChain(chain) { return certs; }; +/** + * Verify all expiration times in a certificate chain. + * @param {Object[]} chain + * @returns {Boolean} + */ + x509.verifyTimes = function verifyTimes(chain) { var i, cert; @@ -270,6 +394,13 @@ x509.verifyTimes = function verifyTimes(chain) { return true; }; +/** + * Verify that at least one parent + * cert in the chain is trusted. + * @param {Object[]} chain + * @returns {Boolean} + */ + x509.verifyTrust = function verifyTrust(chain) { var i, cert; @@ -294,6 +425,11 @@ x509.verifyTrust = function verifyTrust(chain) { return false; }; +/** + * Verify certificate chain. + * @param {Object[]} certs + */ + x509.verifyChain = function verifyChain(certs) { var chain = x509.parseChain(certs); var i, child, parent, alg, key, sig, msg; @@ -331,6 +467,10 @@ x509.verifyChain = function verifyChain(certs) { return x509.verifyTrust(chain); }; +/* + * Helpers + */ + function isHash(data) { if (typeof data === 'string') return util.isHex(data) && data.length === 64; @@ -341,4 +481,8 @@ function isHash(data) { return false; } +/* + * Load trusted certs. + */ + x509.setTrust(require('../../etc/certs.json'));