diff --git a/lib/bcoin/bip70/asn1.js b/lib/bcoin/bip70/asn1.js new file mode 100644 index 00000000..1378968d --- /dev/null +++ b/lib/bcoin/bip70/asn1.js @@ -0,0 +1,436 @@ +/*! + * asn1.js - asn1 parsing for bcoin + * Copyright (c) 2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + * + * Parts of this software are based on asn1.js. + * https://github.com/indutny/asn1.js + * + * Copyright Fedor Indutny, 2013. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +'use strict'; + +var assert = require('assert'); +var BufferReader = require('../reader'); +var BufferWriter = require('../writer'); +var asn1 = exports; + +asn1.parseTag = function parseTag(p) { + var tag = p.readU8(); + var primitive = (tag & 0x20) === 0; + var oct; + + if ((tag & 0x1f) === 0x1f) { + oct = tag; + tag = 0; + while ((oct & 0x80) === 0x80) { + oct = p.readU8(); + tag <<= 7; + tag |= oct & 0x7f; + } + } else { + tag &= 0x1f; + } + + return { + primitive: primitive, + tag: tag, + len: asn1.parseLen(p, primitive) + }; +}; + +asn1.parseLen = function parseLen(p, primitive) { + var len = p.readU8(); + var num, i, j; + + // Indefinite form + if (!primitive && len === 0x80) + return null; + + // Definite form + if ((len & 0x80) === 0) { + // Short form + return len; + } + + // Long form + num = len & 0x7f; + assert(num < 4, 'length octect is too long'); + + len = 0; + for (i = 0; i < num; i++) { + len <<= 8; + j = p.readU8(); + len |= j; + } + + return len; +}; + +asn1.parseCert = function parseCert(data) { + var d = BufferReader(data); + var p; + + d.start(); + + p = BufferReader(asn1.parseSeq(d)); + + return { + tbs: asn1.parseTBS(p), + sigAlg: asn1.parseAlgIdent(p), + sig: asn1.parseBitstr(p), + raw: d.endData(true) + }; +}; + +asn1.parseTBS = function parseTBS(data) { + var d = BufferReader(data); + var p; + + d.start(); + + p = BufferReader(asn1.parseSeq(d)); + + return { + version: asn1.parseExplicitInt(p, 0, true), + serial: asn1.parseInt(p), + sig: asn1.parseAlgIdent(p), + issuer: asn1.parseName(p), + validity: asn1.parseValidity(p), + subject: asn1.parseName(p), + pubkey: asn1.parsePubkey(p), + raw: d.endData(true) + }; +}; + +asn1.parseSeq = function parseSeq(data) { + var p = BufferReader(data); + var tag = asn1.parseTag(p); + assert.equal(tag.tag, 0x10); // seq + return p.readBytes(tag.len, true); +}; + +asn1.parseInt = function parseInt(data, readNum) { + var p = BufferReader(data); + var tag = asn1.parseTag(p); + var num; + + assert.equal(tag.tag, 0x02); // int + + num = p.readBytes(tag.len, true); + + if (readNum) + return num.readUIntBE(0, num.length); + + return num; +}; + +asn1.parseExplicitInt = function parseExplicitInt(data, i, readNum) { + var p = BufferReader(data); + var off = p.offset; + var tag = asn1.parseTag(p); + if (tag.tag !== i) { + p.seek(-(p.offset - off)); + return -1; + } + return asn1.parseInt(p, readNum); +}; + +asn1.parseBitstr = function parseBitstr(data) { + var p = BufferReader(data); + var tag = asn1.parseTag(p); + assert.equal(tag.tag, 0x03); // bitstr + return asn1.alignBitstr(p.readBytes(tag.len, true)); +}; + +asn1.parseString = function parseString(data) { + var p = BufferReader(data); + var tag = asn1.parseTag(p); + switch (tag.tag) { + case 0x03: // bitstr + return asn1.alignBitstr(p.readBytes(tag.len, true)); + case 0x04: // octstr + case 0x12: // numstr + case 0x13: // prinstr + case 0x14: // t61str + case 0x15: // videostr + case 0x16: // ia5str + case 0x19: // graphstr + case 0x0c: // utf8str + case 0x1a: // iso646str + case 0x1b: // genstr + case 0x1c: // unistr + case 0x1d: // charstr + case 0x1e: // bmpstr + return p.readString('utf8', tag.len); + default: + assert(false, 'Bad string.'); + } +}; + +asn1.alignBitstr = function(data) { + var padding = data[0]; + var bits = (data.length - 1) * 8 - padding; + var buf = data.slice(1); + var shift = 8 - (bits % 8); + var i, out; + + if (shift === 8 || buf.length === 0) + return buf; + + out = new Buffer(buf.length); + out[0] = buf[0] >>> shift; + + for (i = 1; i < buf.length; i++) { + out[i] = buf[i - 1] << (8 - shift); + out[i] |= buf[i] >>> shift; + } + + return out; +}; + +asn1.parsePubkey = function parsePubkey(data) { + var p = BufferReader(data); + p = BufferReader(asn1.parseSeq(p)); + return { + alg: asn1.parseAlgIdent(p), + pubkey: asn1.parseBitstr(p) + }; +}; + +asn1.parseName = function parseName(data) { + var p = BufferReader(data); + var values = []; + var tag, type, value; + + p = BufferReader(asn1.parseSeq(p)); + + while (p.left()) { + tag = asn1.parseTag(p); + assert.equal(tag.tag, 0x11); // set + tag = asn1.parseTag(p); + assert.equal(tag.tag, 0x10); // seq + values.push({ + type: asn1.parseOID(p), + value: asn1.parseString(p) + }); + } + + return values; +}; + +asn1.parseValidity = function parseValidity(data) { + var p = BufferReader(data); + p = BufferReader(asn1.parseSeq(p)); + return { + notBefore: asn1.parseTime(p), + notAfter: asn1.parseTime(p) + }; +}; + +asn1.parseTime = function parseTime(data) { + var p = BufferReader(data); + var tag = asn1.parseTag(p); + var str = p.readString('ascii', tag.len); + var year, mon, day, hour, min, sec; + + switch (tag.tag) { + case 0x17: // utctime + year = str.slice(0, 2) | 0; + mon = str.slice(2, 4) | 0; + day = str.slice(4, 6) | 0; + hour = str.slice(6, 8) | 0; + min = str.slice(8, 10) | 0; + sec = str.slice(10, 12) | 0; + if (year < 70) + year = 2000 + year; + else + year = 1900 + year; + break; + case 0x18: // gentime + year = str.slice(0, 4) | 0; + mon = str.slice(4, 6) | 0; + day = str.slice(6, 8) | 0; + hour = str.slice(8, 10) | 0; + min = str.slice(10, 12) | 0; + sec = str.slice(12, 14) | 0; + break; + default: + assert(false); + break; + } + + return Date.UTC(year, mon - 1, day, hour, min, sec, 0) / 1000; +}; + +asn1.parseOID = function parseOID(data) { + var p = BufferReader(data); + var tag = asn1.parseTag(p); + var ids = []; + var ident = 0; + var subident = 0; + var objid, result, first, second; + + assert.equal(tag.tag, 0x06); // objid + + objid = p.readBytes(tag.len, true); + p = BufferReader(objid); + + while (p.left()) { + subident = p.readU8(); + ident <<= 7; + ident |= subident & 0x7f; + if ((subident & 0x80) === 0) { + ids.push(ident); + ident = 0; + } + } + + if (subident & 0x80) + ids.push(ident); + + first = (ids[0] / 40) | 0; + second = ids[0] % 40; + result = [first, second].concat(ids.slice(1)); + + return result.join('.'); +}; + +asn1.parseAlgIdent = function parseAlgIdent(data) { + var p = BufferReader(data); + var params = null; + var alg; + + p = BufferReader(asn1.parseSeq(p)); + + alg = asn1.parseOID(p); + + if (p.left() > 0) { + params = p.readBytes(asn1.parseTag(p).len, true); + if (params.length === 0) + params = null; + } + + return { + alg: alg, + params: params + }; +}; + +asn1.parseRSAPublic = function parseRSAPublic(data) { + var p = BufferReader(data); + p = BufferReader(asn1.parseSeq(p)); + return { + modulus: asn1.parseInt(p), + publicExponent: asn1.parseInt(p) + }; +}; + +asn1.parseRSAPrivate = function parseRSAPrivate(data) { + var p = BufferReader(data); + p = BufferReader(asn1.parseSeq(p)); + return { + version: asn1.parseExplicitInt(p, 0, true), + modulus: asn1.parseInt(p), + publicExponent: asn1.parseInt(p), + privateExponent: asn1.parseInt(p), + prime1: asn1.parseInt(p), + prime2: asn1.parseInt(p), + exponent1: asn1.parseInt(p), + exponent2: asn1.parseInt(p), + coefficient: asn1.parseInt(p) + }; +}; + +asn1.parsePEM = function parsePEM(pem) { + var buf = ''; + var chunks = []; + var s, tag, type; + + while (pem.length) { + if (s = /^-----BEGIN ([^\-]+)-----/.exec(pem)) { + pem = pem.substring(s[0].length); + tag = s[1]; + continue; + } + if (s = /^-----END ([^\-]+)-----/.exec(pem)) { + pem = pem.substring(s[0].length); + assert(tag === s[1], 'Tag mismatch.'); + buf = new Buffer(buf, 'base64'); + type = tag.split(' ')[0].toLowerCase(); + chunks.push({ tag: tag, type: type, data: buf }); + buf = ''; + tag = null; + continue; + } + if (s = /^[a-zA-Z0-9\+=\/]+/.exec(pem)) { + pem = pem.substring(s[0].length); + buf += s[0]; + continue; + } + if (s = /^\s+/.exec(pem)) { + pem = pem.substring(s[0].length); + continue; + } + throw new Error('PEM parse error.'); + } + + assert(chunks.length !== 0, 'PEM parse error.'); + assert(!tag, 'Un-ended tag.'); + assert(buf.length === 0, 'Trailing data.'); + + return chunks; +}; + +asn1.fromPEM = function fromPEM(pem) { + var chunks = asn1.parsePEM(pem); + var body = chunks[0]; + var extra = chunks[1]; + var params; + + if (extra) { + if (extra.tag.indexOf('PARAMETERS') !== -1) + params = extra.data; + } + + return { + type: body.type, + data: body.data, + params: params + }; +}; + +asn1.toPEM = function toPEM(der, type) { + var pem = ''; + var i; + + type = type.toUpperCase(); + der = der.toString('base64'); + + for (i = 0; i < der.length; i += 64) + pem += der.slice(i, i + 64) + '\r\n'; + + return '' + + '-----BEGIN ' + type + '-----\r\n' + + pem + + '-----END ' + type + '-----\r\n'; +}; diff --git a/lib/bcoin/bip70/index.js b/lib/bcoin/bip70/index.js new file mode 100644 index 00000000..c56dc505 --- /dev/null +++ b/lib/bcoin/bip70/index.js @@ -0,0 +1,534 @@ +/*! + * bip70.js - bip70 for bcoin + * Copyright (c) 2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('../env'); +var assert = require('assert'); +var utils = bcoin.utils; +var crypto = require('crypto'); +var x509 = require('./x509'); +var asn1 = require('./asn1'); +var protobuf = require('./protobuf'); +var ProtoReader = protobuf.ProtoReader; +var ProtoWriter = protobuf.ProtoWriter; + +function PaymentRequest(options) { + if (!(this instanceof PaymentRequest)) + return new PaymentRequest(options); + + this.version = -1; + this.pkiType = null; + this.pkiData = null; + this.paymentDetails = new PaymentDetails(); + this.signature = null; + + if (options) + this.fromOptions(options); +} + +PaymentRequest.prototype.fromOptions = function fromOptions(options) { + if (options.version != null) { + assert(utils.isNumber(options.version)); + this.version = options.version; + } + + if (options.pkiType != null) { + assert(typeof options.pkiType === 'string'); + this.pkiType = options.pkiType; + } + + if (options.pkiData) { + assert(Buffer.isBuffer(options.pkiData)); + this.pkiData = options.pkiData; + } + + if (options.paymentDetails) + this.paymentDetails.fromOptions(options.paymentDetails); + + if (options.signature) { + assert(Buffer.isBuffer(options.signature)); + this.signature = options.signature; + } + + if (options.chain) + this.setChain(this.pkiType, options.chain); + + return this; +}; + +PaymentRequest.fromOptions = function fromOptions(options) { + return new PaymentRequest().fromOptions(options); +}; + +PaymentRequest.prototype.fromRaw = function fromRaw(data) { + var p = new ProtoReader(data); + + this.version = p.readFieldU32(1, true); + this.pkiType = p.readFieldString(2, true); + this.pkiData = p.readFieldBytes(3, true); + this.paymentDetails.fromRaw(p.readFieldBytes(4)); + this.signature = p.readFieldBytes(5, true); + + return this; +}; + +PaymentRequest.fromRaw = function fromRaw(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return new PaymentRequest().fromRaw(data); +}; + +PaymentRequest.prototype.toRaw = function toRaw(writer) { + var p = new ProtoWriter(writer); + + if (this.version !== -1) + p.writeFieldU32(1, this.version); + + if (this.pkiType != null) + p.writeFieldString(2, this.pkiType); + + if (this.pkiData) + p.writeFieldBytes(3, this.pkiData); + + p.writeFieldBytes(4, this.paymentDetails.toRaw()); + + if (this.signature) + p.writeFieldBytes(5, this.signature); + + if (!writer) + p = p.render(); + + return p; +}; + +PaymentRequest.prototype.getAlgorithm = function getAlgorithm() { + var parts; + + if (!this.pkiType) + return; + + parts = this.pkiType.split('+'); + + if (parts.length !== 2) + return; + + if (parts[0] !== 'x509') + return; + + if (parts[1] !== 'sha1' && parts[1] !== 'sha256') + return; + + return { key: 'rsa', hash: parts[1] }; +}; + +PaymentRequest.prototype.signatureData = function signatureData() { + var signature = this.signature; + var data; + + this.signature = new Buffer(0); + + data = this.toRaw(); + + this.signature = signature; + + return data; +}; + +PaymentRequest.prototype.signatureHash = function signatureHash() { + var alg = this.getAlgorithm(); + assert(alg, 'No hash algorithm available.'); + return utils.hash(alg.hash, this.signatureData()); +}; + +PaymentRequest.prototype.setChain = function setChain(pkiType, chain) { + var p = new ProtoWriter(); + var i, cert; + + assert(pkiType === 'x509+sha1' || pkiType === 'x509+sha256'); + assert(Array.isArray(chain)); + + this.pkiType = pkiType; + + for (i = 0; i < chain.length; i++) { + cert = chain[i]; + if (typeof cert === 'string') + cert = asn1.fromPEM(cert).data; + assert(Buffer.isBuffer(cert), 'Bad cert format.'); + p.writeFieldBytes(1, cert); + } + + this.pkiData = p.render(); +}; + +PaymentRequest.prototype.getChain = function getChain() { + var chain = []; + var p; + + if (!this.pkiData) + return chain; + + p = new ProtoReader(this.pkiData); + + while (p.nextTag() === 1) + chain.push(p.readFieldBytes(1)); + + return chain; +}; + +PaymentRequest.prototype.sign = function sign(key) { + var alg, msg; + + if (!this.pkiType || this.pkiType === 'none') { + this.signature = null; + return; + } + + alg = this.getAlgorithm(); + assert(alg, 'No hash algorithm available.'); + + msg = this.signatureData(); + + this.signature = x509.sign(alg.hash, msg, key); +}; + +PaymentRequest.prototype.verify = function verify() { + var alg, msg, ver, der, pem; + + if (!this.pkiType || this.pkiType === 'none') + return true; + + if (!this.signature) + return false; + + alg = this.getAlgorithm(); + + if (!alg) + return false; + + msg = this.signatureData(); + + return x509.verifySubject(alg.hash, msg, this.signature, this.getChain()); +}; + +PaymentRequest.prototype.verifyChain = function verifyChain(ignoreTime) { + if (!this.pkiType || this.pkiType === 'none') + return true; + + return x509.verifyChain(this.getChain(), ignoreTime); +}; + +PaymentRequest.prototype.getCA = function getCA() { + var chain; + + if (!this.pkiType || this.pkiType === 'none') + return; + + chain = this.getChain(); + + if (chain.length === 0) + return; + + return x509.getTrusted(chain[chain.length - 1]); +}; + +function PaymentDetails(options) { + if (!(this instanceof PaymentDetails)) + return new PaymentDetails(options); + + this.network = null; + this.outputs = []; + this.time = utils.now(); + this.expires = -1; + this.memo = null; + this.paymentUrl = null; + this.merchantData = null; + + if (options) + this.fromOptions(options); +} + +PaymentDetails.prototype.fromOptions = function fromOptions(options) { + var i, output; + + if (options.network != null) { + assert(typeof options.network === 'string'); + this.network = options.network; + } + + if (options.outputs) { + assert(Array.isArray(options.outputs)); + for (i = 0; i < options.outputs.length; i++) { + output = new bcoin.output(options.outputs[i]); + this.outputs.push(output); + } + } + + if (options.time != null) { + assert(utils.isNumber(options.time)); + this.time = options.time; + } + + if (options.expires != null) { + assert(utils.isNumber(options.expires)); + this.expires = options.expires; + } + + if (options.memo != null) { + assert(typeof options.memo === 'string'); + this.memo = options.memo; + } + + if (options.paymentUrl != null) { + assert(typeof options.paymentUrl === 'string'); + this.paymentUrl = options.paymentUrl; + } + + if (options.merchantData) { + assert(Buffer.isBuffer(options.merchantData)); + this.merchantData = options.merchantData; + } + + return this; +}; + +PaymentDetails.fromOptions = function fromOptions(options) { + return new PaymentDetails().fromOptions(options); +}; + +PaymentDetails.prototype.fromRaw = function fromRaw(data) { + var p = new ProtoReader(data); + var network, op, output; + + this.network = p.readFieldString(1, true); + + while (p.nextTag() === 2) { + op = new ProtoReader(p.readFieldBytes(2)); + output = new bcoin.output(); + output.value = op.readFieldU64(1, true); + output.script.fromRaw(op.readFieldBytes(2, true)); + this.outputs.push(output); + } + + this.time = p.readFieldU64(3); + this.expires = p.readFieldU64(4, true); + this.memo = p.readFieldString(5, true); + this.paymentUrl = p.readFieldString(6, true); + this.merchantData = p.readFieldBytes(7, true); + + return this; +}; + +PaymentDetails.fromRaw = function fromRaw(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return new PaymentDetails().fromRaw(data); +}; + +PaymentDetails.prototype.toRaw = function toRaw(writer) { + var p = new ProtoWriter(writer); + var i, op, output; + + if (this.network != null) + p.writeFieldString(1, this.network); + + for (i = 0; i < this.outputs.length; i++) { + output = this.outputs[i]; + op = new ProtoWriter(); + op.writeFieldU64(1, output.value); + op.writeFieldBytes(2, output.script.toRaw()); + p.writeFieldBytes(2, op.render()); + } + + p.writeFieldU64(3, this.time); + + if (this.expires !== -1) + p.writeFieldU64(4, this.expires); + + if (this.memo != null) + p.writeFieldString(5, this.memo); + + if (this.paymentUrl != null) + p.writeFieldString(6, this.paymentUrl); + + if (this.merchantData) + p.writeFieldString(7, this.merchantData); + + if (!writer) + p = p.render(); + + return p; +}; + +function Payment(options) { + if (!(this instanceof Payment)) + return new Payment(options); + + this.merchantData = null; + this.transactions = []; + this.refundTo = []; + this.memo = null; + + if (options) + this.fromOptions(options); +} + +Payment.prototype.fromOptions = function fromOptions(options) { + var i, tx, output; + + if (options.merchantData) { + assert(Buffer.isBuffer(options.merchantData)); + this.merchantData = options.merchantData; + } + + if (options.transactions) { + assert(Array.isArray(options.transactions)); + for (i = 0; i < options.transactions.length; i++) { + tx = new bcoin.tx(options.transactions[i]); + this.transactions.push(tx); + } + } + + if (options.refundTo) { + assert(Array.isArray(options.refundTo)); + for (i = 0; i < options.refundTo.length; i++) { + output = new bcoin.output(options.refundTo[i]); + this.refundTo.push(output); + } + } + + if (options.memo != null) { + assert(typeof options.memo === 'string'); + this.memo = options.memo; + } + + return this; +}; + +Payment.fromOptions = function fromOptions(options) { + return new Payment().fromOptions(options); +}; + +Payment.prototype.fromRaw = function fromRaw(data) { + var p = new ProtoReader(data); + var tx, op, output; + + this.merchantData = p.readFieldBytes(1, true); + + while (p.nextTag() === 2) { + tx = bcoin.tx.fromRaw(p.readFieldBytes(2)); + this.transactions.push(tx); + } + + while (p.nextTag() === 3) { + op = new ProtoReader(p.readFieldBytes(3)); + output = new bcoin.output(); + output.value = op.readFieldU64(1, true); + output.script = bcoin.script.fromRaw(op.readFieldBytes(2, true)); + this.refundTo.push(output); + } + + this.memo = p.readFieldString(4, true); + + return this; +}; + +Payment.fromRaw = function fromRaw(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return new Payment().fromRaw(data); +}; + +Payment.prototype.toRaw = function toRaw(writer) { + var p = new ProtoWriter(writer); + var i, tx, op, output; + + if (this.merchantData) + p.writeFieldBytes(1, this.merchantData); + + for (i = 0; i < this.transactions.length; i++) { + tx = this.transactions[i]; + this.writeFieldBytes(2, tx.toRaw()); + } + + for (i = 0; i < this.refundTo.length; i++) { + op = new ProtoWriter(); + output = this.refundTo[i]; + op.writeFieldU64(1, output.value); + op.writeFieldBytes(2, output.script.toRaw()); + p.writeFieldBytes(3, op.render()); + } + + if (this.memo != null) + p.writeFieldString(4, this.memo); + + if (!writer) + p = p.render(); + + return p; +}; + +function PaymentACK(options) { + if (!(this instanceof PaymentACK)) + return new PaymentACK(options); + + this.payment = new Payment(); + this.memo = null; + + if (options) + this.fromOptions(options); +} + +PaymentACK.prototype.fromOptions = function fromOptions(options) { + if (options.payment) + this.payment.fromOptions(options.payment); + + if (options.memo != null) { + assert(typeof options.memo === 'string'); + this.memo = options.memo; + } + + return this; +}; + +PaymentACK.fromOptions = function fromOptions(options) { + return new PaymentACK().fromOptions(options); +}; + +PaymentACK.prototype.fromRaw = function fromRaw(data) { + var p = new ProtoReader(data); + + this.payment.fromRaw(p.readFieldBytes(1)); + this.memo = p.readFieldString(2, true); + + return this; +}; + +PaymentACK.fromRaw = function fromRaw(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return new PaymentACK().fromRaw(data); +}; + +PaymentACK.prototype.toRaw = function toRaw(writer) { + var p = new ProtoWriter(writer); + var i, tx, op, output; + + p.writeFieldBytes(1, this.payment.toRaw()); + + if (this.memo != null) + p.writeFieldString(2, this.memo); + + if (!writer) + p = p.render(); + + return p; +}; + +exports.PaymentRequest = PaymentRequest; +exports.PaymentDetails = PaymentDetails; +exports.Payment = Payment; +exports.PaymentACK = PaymentACK; diff --git a/lib/bcoin/bip70/protobuf.js b/lib/bcoin/bip70/protobuf.js new file mode 100644 index 00000000..9e9a83e0 --- /dev/null +++ b/lib/bcoin/bip70/protobuf.js @@ -0,0 +1,243 @@ +/*! + * protobuf.js - protobufs for bcoin + * Copyright (c) 2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var utils = require('../utils'); +var assert = utils.assert; +var BufferReader = require('../reader'); +var BufferWriter = require('../writer'); + +var wireType = { + VARINT: 0, + FIXED64: 1, + DELIMITED: 2, + START_GROUP: 3, + END_GROUP: 4, + FIXED32: 5 +}; + +function ProtoReader(data, zeroCopy) { + if (data instanceof ProtoReader) + return data; + if (!(this instanceof ProtoReader)) + return new ProtoReader(data, zeroCopy); + BufferReader.call(this, data, zeroCopy); +} + +utils.inherits(ProtoReader, BufferReader); + +ProtoReader.prototype.readVarint = function readVarint() { + var result = exports.readVarint(this.data, this.offset); + this.offset += result.size; + return result.value; +}; + +ProtoReader.prototype.readFieldValue = function readFieldValue(tag, opt) { + var field = this.readField(tag, opt); + if (!field) + return -1; + assert(field.value != null); + return field.value; +}; + +ProtoReader.prototype.readFieldU64 = function readFieldU64(tag, opt) { + var field = this.readField(tag, opt); + if (!field) + return -1; + assert(field.type === wireType.VARINT || field.type === wireType.FIXED64); + return field.value; +}; + +ProtoReader.prototype.readFieldU32 = function readFieldU32(tag, opt) { + var field = this.readField(tag, opt); + if (!field) + return -1; + assert(field.type === wireType.VARINT || field.type === wireType.FIXED32); + return field.value; +}; + +ProtoReader.prototype.readFieldBytes = function readFieldBytes(tag, opt) { + var field = this.readField(tag, opt); + if (!field) + return null; + assert(field.data); + return field.data; +}; + +ProtoReader.prototype.readFieldString = function readFieldString(tag, opt, enc) { + var field = this.readField(tag, opt); + if (!field) + return null; + assert(field.data); + return field.data.toString(enc || 'utf8'); +}; + +ProtoReader.prototype.nextTag = function nextTag() { + var field; + + if (this.left() === 0) + return -1; + + field = this.readField(); + + this.seek(-field.size); + + return field.tag; +}; + +ProtoReader.prototype.readField = function readField(tag, opt) { + var offset = this.offset; + var header = this.readVarint(); + var value, data, group, field; + + if (tag != null && (header >>> 3) !== tag) { + assert(opt, 'Non-optional field not present.'); + this.offset = offset; + return; + } + + switch (header & 7) { + case wireType.VARINT: + value = this.readVarint(); + break; + case wireType.FIXED64: + value = this.readU64N(); + break; + case wireType.DELIMITED: + data = this.readVarBytes(); + break; + case wireType.START_GROUP: + group = []; + for (;;) { + field = this.readField(); + if (field.type === wireType.END_GROUP) + break; + group.push(field); + } + break; + case wireType.END_GROUP: + assert(false, 'Unexpected end group.'); + break; + case wireType.FIXED32: + value = this.readU32(); + break; + default: + assert(false, 'Bad wire type.'); + break; + } + + return { + size: this.offset - offset, + header: header, + tag: header >>> 3, + type: header & 7, + value: value, + data: data, + group: group + }; +}; + +function ProtoWriter(options) { + if (options instanceof ProtoWriter) + return options; + + if (!(this instanceof ProtoWriter)) + return new ProtoWriter(options); + + BufferWriter.call(this, options); +} + +utils.inherits(ProtoWriter, BufferWriter); + +ProtoWriter.prototype.writeVarint = function writeVarint(num) { + var size = exports.sizeVarint(num); + var buf = new Buffer(size); + exports.writeVarint(buf, num, 0); + this.writeBytes(buf); +}; + +ProtoWriter.prototype.writeFieldValue = function writeFieldValue(tag, value) { + var header = (tag << 3) | wireType.VARINT; + this.writeVarint(header); + this.writeVarint(value); +}; + +ProtoWriter.prototype.writeFieldU64 = function writeFieldU64(tag, value) { + this.writeFieldValue(tag, value); +}; + +ProtoWriter.prototype.writeFieldU32 = function writeFieldU32(tag, value) { + this.writeFieldValue(tag, value); +}; + +ProtoWriter.prototype.writeFieldBytes = function writeFieldBytes(tag, data) { + var header = (tag << 3) | wireType.DELIMITED; + this.writeVarint(header); + this.writeVarint(data.length); + this.writeBytes(data); +}; + +ProtoWriter.prototype.writeFieldString = function writeFieldString(tag, data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc || 'utf8'); + this.writeFieldBytes(tag, data); +}; + +exports.readVarint = function readVarint(data, off) { + var num = 0; + var ch = 0x80; + var size = 0; + + while (ch & 0x80) { + if (off >= data.length) { + num = 0; + break; + } + ch = data[off++]; + assert(size + 1 < 6, 'Number exceeds 2^53-1.'); + num += (ch & 0x7f) * Math.pow(2, 7 * size); + size++; + } + + return { size: size, value: num }; +}; + +exports.writeVarint = function writeVarint(data, num, off) { + var ch; + + assert(utils.isSafeInteger(num), 'Number exceeds 2^53-1.'); + + do { + assert(off < data.length); + ch = num & 0x7f; + num -= num % 0x80; + num /= 0x80; + if (num !== 0) + ch |= 0x80; + data[off] = ch; + off++; + } while (num > 0); + + return off; +}; + +exports.sizeVarint = function sizeVarint(num) { + var size = 0; + + assert(utils.isSafeInteger(num), 'Number exceeds 2^53-1.'); + + do { + num -= num % 0x80; + num /= 0x80; + size++; + } while (num > 0); + + return size; +}; + +exports.ProtoReader = ProtoReader; +exports.ProtoWriter = ProtoWriter; diff --git a/lib/bcoin/bip70/x509.js b/lib/bcoin/bip70/x509.js new file mode 100644 index 00000000..41a70624 --- /dev/null +++ b/lib/bcoin/bip70/x509.js @@ -0,0 +1,227 @@ +/*! + * x509.js - x509 handling for bcoin + * Copyright (c) 2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var assert = require('assert'); +var crypto = require('crypto'); +var asn1 = require('./asn1'); +var utils = require('../utils'); +var x509 = exports; + +x509.certs = []; +x509.trusted = {}; + +x509.getTrusted = function getTrusted(cert) { + var hash; + + if (!Buffer.isBuffer(cert)) + cert = cert.raw; + + hash = utils.hash256(cert).toString('hex'); + + return x509.trusted[hash]; +}; + +x509.setTrust = function setTrust(certs) { + var keys = Object.keys(certs); + var i, key, cert, hash, pem; + + for (i = 0; i < keys.length; i++) { + key = keys[i]; + cert = certs[key]; + + if (typeof cert === 'string') { + pem = asn1.fromPEM(cert); + assert(pem.type === 'certificate', 'Must add certificates to trust.'); + cert = pem.data; + } + + assert(Buffer.isBuffer(cert), 'Certificates must be PEM or DER.'); + + hash = utils.hash256(cert).toString('hex'); + + cert = { + name: key, + fingerprint: hash, + cert: asn1.parseCert(cert) + }; + + x509.certs.push(cert); + x509.trusted[hash] = cert; + } +}; + +/* + * 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 + */ + +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.getKeyAlgorithm = function getKeyAlgorithm(cert) { + var alg = cert.tbs.pubkey.alg.alg; + return x509.oid[alg]; +}; + +x509.getSigAlgorithm = function getSigAlgorithm(cert) { + var alg = cert.sigAlg.alg; + return x509.oid[alg]; +}; + +x509.parse = function parse(der) { + try { + return asn1.parseCert(der); + } catch (e) { + ; + } +}; + +x509.getPublicKey = function getPublicKey(cert) { + var alg = x509.getKeyAlgorithm(cert); + var key, params, pem; + + if (!alg) + return; + + key = cert.tbs.pubkey.pubkey; + params = cert.tbs.pubkey.alg.params; + + pem = asn1.toPEM(key, alg.key + ' PUBLIC KEY'); + + if (params) + pem += asn1.toPEM(params, alg.key + ' PARAMETERS'); + + return pem; +}; + +x509.verifyTime = function verifyTime(cert) { + var time = child.tbs.validity; + var now = Math.floor(Date.now() / 1000); + return now > time.notBefore && now < time.notAfter; +}; + +x509.verifySubject = function verifySubject(hash, msg, sig, chain) { + var cert, key, alg; + + if (chain.length === 0) + return false; + + cert = x509.parse(chain[0]); + + if (!cert) + return false; + + key = x509.getPublicKey(cert); + + if (!key) + return false; + + alg = x509.getKeyAlgorithm(cert); + + if (!alg) + return false; + + return x509.verify(alg.key, hash, msg, sig, key); +}; + +x509.verifyChain = function verifyChain(chain, ignoreTime) { + var i, child, parent, alg, key, sig, msg; + + if (chain.length < 2) + return false; + + for (i = 1; i < chain.length; i++) { + child = chain[i - 1]; + parent = chain[i]; + + child = x509.parse(child); + + if (!child) + return false; + + parent = x509.parse(parent); + + if (!parent) + return false; + + if (!ignoreTime) { + if (!x509.verifyTime(child)) + return false; + + if (!x509.verifyTime(parent)) + return false; + } + + alg = x509.getSigAlgorithm(child); + + if (!alg || !alg.hash) + return false; + + key = x509.getPublicKey(parent); + + if (!key) + return false; + + sig = child.sig; + msg = child.tbs.raw; + + if (!x509.verify(alg.key, alg.hash, msg, sig, key)) + return false; + + if (x509.getTrusted(parent)) + return true; + } + + if (exports.certs.length === 0) + return true; + + return false; +}; + +x509.verify = function verify(alg, hash, msg, sig, key) { + var algo = alg.toUpperCase() + '-' + hash.toUpperCase(); + var verify; + + try { + verify = crypto.createVerify(algo); + verify.update(msg); + return verify.verify(key, sig); + } catch (e) { + return false; + } +}; + +x509.sign = function sign(alg, hash, msg, key) { + var algo = alg.toUpperCase() + '-' + hash.toUpperCase(); + var sig = crypto.createSign(algo); + sig.update(msg); + return sig.sign(key); +}; + +x509.asn1 = asn1;