You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
443 lines
14 KiB
443 lines
14 KiB
'use strict';
|
|
|
|
var protobufjs = require('protobufjs/dist/ProtoBuf');
|
|
var RootCerts = require('./rootcerts');
|
|
var PublicKey = require('../publickey');
|
|
var PrivateKey = require('../privatekey');
|
|
var Signature = require('../crypto/signature');
|
|
var ECDSA = require('../crypto/ecdsa');
|
|
var sha256sha256 = require('../crypto/hash').sha256sha256;
|
|
var varintBufNum = require('../encoding/bufferwriter').varintBufNum;
|
|
|
|
// BIP 70 - payment protocol
|
|
function PaymentProtocol() {
|
|
this.messageType = null;
|
|
this.message = null;
|
|
}
|
|
|
|
PaymentProtocol.PAYMENT_REQUEST_MAX_SIZE = 50000;
|
|
PaymentProtocol.PAYMENT_MAX_SIZE = 50000;
|
|
PaymentProtocol.PAYMENT_ACK_MAX_SIZE = 60000;
|
|
PaymentProtocol.PAYMENT_REQUEST_CONTENT_TYPE = "application/bitcoin-paymentrequest";
|
|
PaymentProtocol.PAYMENT_CONTENT_TYPE = "application/bitcoin-payment";
|
|
PaymentProtocol.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
|
|
PaymentProtocol.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'
|
|
};
|
|
|
|
PaymentProtocol.getAlgorithm = function(value, index) {
|
|
if (Array.isArray(value)) {
|
|
value = value.join('.');
|
|
}
|
|
value = PaymentProtocol.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
|
|
PaymentProtocol.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
|
|
PaymentProtocol.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:
|
|
PaymentProtocol.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;
|
|
};
|
|
|
|
PaymentProtocol.RootCerts = RootCerts;
|
|
|
|
PaymentProtocol.proto = {};
|
|
|
|
PaymentProtocol.proto.Output = "message Output {\
|
|
optional uint64 amount = 1 [default = 0];\
|
|
optional bytes script = 2;\
|
|
}\n";
|
|
|
|
PaymentProtocol.proto.PaymentDetails = "message PaymentDetails {\
|
|
optional string network = 1 [default = \"main\"];\
|
|
repeated Output outputs = 2;\
|
|
required uint64 time = 3;\
|
|
optional uint64 expires = 4;\
|
|
optional string memo = 5;\
|
|
optional string payment_url = 6;\
|
|
optional bytes merchant_data = 7;\
|
|
}\n";
|
|
|
|
PaymentProtocol.proto.PaymentRequest = "message PaymentRequest {\
|
|
optional uint32 payment_details_version = 1 [default = 1];\
|
|
optional string pki_type = 2 [default = \"none\"];\
|
|
optional bytes pki_data = 3;\
|
|
required bytes serialized_payment_details = 4;\
|
|
optional bytes signature = 5;\
|
|
}\n";
|
|
|
|
PaymentProtocol.proto.Payment = "message Payment {\
|
|
optional bytes merchant_data = 1;\
|
|
repeated bytes transactions = 2;\
|
|
repeated Output refund_to = 3;\
|
|
optional string memo = 4;\
|
|
}\n";
|
|
|
|
PaymentProtocol.proto.PaymentACK = "message PaymentACK {\
|
|
required Payment payment = 1;\
|
|
optional string memo = 2;\
|
|
}\n";
|
|
|
|
PaymentProtocol.proto.X509Certificates = "message X509Certificates {\
|
|
repeated bytes certificate = 1;\
|
|
}\n";
|
|
|
|
PaymentProtocol.proto.all = "";
|
|
PaymentProtocol.proto.all = PaymentProtocol.proto.all + PaymentProtocol.proto.Output;
|
|
PaymentProtocol.proto.all = PaymentProtocol.proto.all + PaymentProtocol.proto.PaymentDetails;
|
|
PaymentProtocol.proto.all = PaymentProtocol.proto.all + PaymentProtocol.proto.PaymentRequest;
|
|
PaymentProtocol.proto.all = PaymentProtocol.proto.all + PaymentProtocol.proto.Payment;
|
|
PaymentProtocol.proto.all = PaymentProtocol.proto.all + PaymentProtocol.proto.PaymentACK;
|
|
PaymentProtocol.proto.all = PaymentProtocol.proto.all + PaymentProtocol.proto.X509Certificates;
|
|
|
|
PaymentProtocol.builder = protobufjs.loadProto(PaymentProtocol.proto.all);
|
|
|
|
PaymentProtocol.Output = PaymentProtocol.builder.build("Output");
|
|
PaymentProtocol.PaymentDetails = PaymentProtocol.builder.build("PaymentDetails");
|
|
PaymentProtocol.PaymentRequest = PaymentProtocol.builder.build("PaymentRequest");
|
|
PaymentProtocol.Payment = PaymentProtocol.builder.build("Payment");
|
|
PaymentProtocol.PaymentACK = PaymentProtocol.builder.build("PaymentACK");
|
|
PaymentProtocol.X509Certificates = PaymentProtocol.builder.build("X509Certificates");
|
|
|
|
PaymentProtocol.prototype.makeOutput = function(obj) {
|
|
this.messageType = 'Output';
|
|
this.message = new PaymentProtocol.Output();
|
|
this.setObj(obj);
|
|
return this;
|
|
};
|
|
|
|
PaymentProtocol.prototype.makePaymentDetails = function(obj) {
|
|
this.messageType = 'PaymentDetails';
|
|
this.message = new PaymentProtocol.PaymentDetails();
|
|
this.setObj(obj);
|
|
return this;
|
|
};
|
|
|
|
PaymentProtocol.prototype.makePaymentRequest = function(obj) {
|
|
this.messageType = 'PaymentRequest';
|
|
this.message = new PaymentProtocol.PaymentRequest();
|
|
this.setObj(obj);
|
|
return this;
|
|
};
|
|
|
|
PaymentProtocol.prototype.makePayment = function(obj) {
|
|
this.messageType = 'Payment';
|
|
this.message = new PaymentProtocol.Payment();
|
|
this.setObj(obj);
|
|
return this;
|
|
};
|
|
|
|
PaymentProtocol.prototype.makePaymentACK = function(obj) {
|
|
this.messageType = 'PaymentACK';
|
|
this.message = new PaymentProtocol.PaymentACK();
|
|
this.setObj(obj);
|
|
return this;
|
|
};
|
|
|
|
PaymentProtocol.prototype.makeX509Certificates = function(obj) {
|
|
this.messageType = 'X509Certificates';
|
|
this.message = new PaymentProtocol.X509Certificates();
|
|
this.setObj(obj);
|
|
return this;
|
|
};
|
|
|
|
PaymentProtocol.prototype.isValidSize = function() {
|
|
var s = this.serialize();
|
|
if (this.messageType == 'PaymentRequest')
|
|
return s.length < PaymentProtocol.PAYMENT_REQUEST_MAX_SIZE;
|
|
if (this.messageType == 'Payment')
|
|
return s.length < PaymentProtocol.PAYMENT_MAX_SIZE;
|
|
if (this.messageType == 'PaymentACK')
|
|
return s.length < PaymentProtocol.PAYMENT_ACK_MAX_SIZE;
|
|
return true;
|
|
};
|
|
|
|
PaymentProtocol.prototype.getContentType = function() {
|
|
if (this.messageType == 'PaymentRequest')
|
|
return PaymentProtocol.PAYMENT_REQUEST_CONTENT_TYPE;
|
|
|
|
if (this.messageType == 'Payment')
|
|
return PaymentProtocol.PAYMENT_CONTENT_TYPE;
|
|
|
|
if (this.messageType == 'PaymentACK')
|
|
return PaymentProtocol.PAYMENT_ACK_CONTENT_TYPE;
|
|
|
|
throw new Error('No known content type for this message type');
|
|
};
|
|
|
|
PaymentProtocol.prototype.set = function(key, val) {
|
|
this.message.set(key, val);
|
|
return this;
|
|
};
|
|
|
|
PaymentProtocol.prototype.get = function(key) {
|
|
var v = this.message.get(key);
|
|
|
|
if (v === null)
|
|
return v;
|
|
|
|
//protobuf supports longs, javascript naturally does not
|
|
//convert longs (see long.js, e.g. require('long')) to Numbers
|
|
if (typeof v.low !== 'undefined' && typeof v.high !== 'undefined')
|
|
return v.toInt();
|
|
|
|
if (typeof v.toBuffer !== 'undefined') {
|
|
var maybebuf = v.toBuffer();
|
|
return Buffer.isBuffer(maybebuf) ? maybebuf : new Buffer(new Uint8Array(maybebuf));
|
|
}
|
|
|
|
return v;
|
|
};
|
|
|
|
PaymentProtocol.prototype.setObj = function(obj) {
|
|
for (var key in obj) {
|
|
if (obj.hasOwnProperty(key)) {
|
|
var val = obj[key];
|
|
this.message.set(key, val);
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
PaymentProtocol.prototype.serializeForSig = function() {
|
|
if (this.messageType !== 'PaymentRequest')
|
|
throw new Error('serializeForSig is only for PaymentRequest');
|
|
|
|
var save = this.message.get('signature');
|
|
this.message.set('signature', new Buffer([]));
|
|
var buf = this.serialize();
|
|
this.message.set('signature', save);
|
|
return buf;
|
|
};
|
|
|
|
PaymentProtocol.prototype.serialize = function() {
|
|
//protobufjs returns either a Buffer or an ArrayBuffer
|
|
//but we always want a Buffer (which browserify understands, browser or no)
|
|
var maybebuf = this.message.toBuffer();
|
|
var buf = (Buffer.isBuffer(maybebuf)) ? maybebuf : new Buffer(new Uint8Array(maybebuf));
|
|
return buf;
|
|
};
|
|
|
|
PaymentProtocol.prototype.deserialize = function(buf, messageType) {
|
|
this.messageType = messageType || this.messageType;
|
|
if (!this.messageType)
|
|
throw new Error('Must specify messageType');
|
|
this.message = PaymentProtocol[this.messageType].decode(buf);
|
|
return this;
|
|
};
|
|
|
|
PaymentProtocol.prototype.sign = function(key, returnTrust) {
|
|
if (this.messageType !== 'PaymentRequest')
|
|
throw new Error('Signing can only be performed on a PaymentRequest');
|
|
|
|
var pki_type = this.get('pki_type');
|
|
|
|
if (pki_type === 'SIN') {
|
|
var sig = this.sinSign(key);
|
|
} else if (pki_type === 'x509+sha1' || pki_type === 'x509+sha256') {
|
|
var sig = this.x509Sign(key, returnTrust);
|
|
} else if (pki_type === 'none') {
|
|
return this;
|
|
} else {
|
|
throw new Error('Unsupported pki_type');
|
|
}
|
|
|
|
this.set('signature', sig);
|
|
|
|
return this;
|
|
};
|
|
|
|
PaymentProtocol.prototype.verify = function(returnTrust) {
|
|
if (this.messageType !== 'PaymentRequest')
|
|
throw new Error('Verifying can only be performed on a PaymentRequest');
|
|
|
|
var pki_type = this.get('pki_type');
|
|
|
|
if (pki_type === 'SIN') {
|
|
return this.sinVerify();
|
|
} else if (pki_type === 'x509+sha1' || pki_type === 'x509+sha256') {
|
|
return this.x509Verify(returnTrust);
|
|
} else if (pki_type === 'none') {
|
|
return true;
|
|
}
|
|
|
|
throw new Error('Unsupported pki_type');
|
|
};
|
|
|
|
function magicHash(str) {
|
|
var magicBytes = new Buffer('Bitcoin Signed Message:\n');
|
|
var prefix1 = varintBufNum(magicBytes.length);
|
|
var message = new Buffer(str);
|
|
var prefix2 = varintBufNum(message.length);
|
|
var buf = Buffer.concat([prefix1, magicBytes, prefix2, message]);
|
|
var hash = sha256sha256(buf);
|
|
return hash;
|
|
};
|
|
|
|
//default signing function for prototype.sign
|
|
PaymentProtocol.prototype.sinSign = function(privateKey) {
|
|
if ( !(privateKey instanceof PrivateKey) ) {
|
|
throw new TypeError('Expects an instance of PrivateKey');
|
|
}
|
|
var pubkey = privateKey.toPublicKey().toBuffer();
|
|
this.set('pki_data', pubkey);
|
|
var buf = this.serializeForSig();
|
|
var hash = magicHash(buf);
|
|
var signature = ECDSA.sign(hash, privateKey);
|
|
return signature.toDER();
|
|
};
|
|
|
|
//default verify function
|
|
PaymentProtocol.prototype.sinVerify = function() {
|
|
var sig = this.get('signature');
|
|
var pubkey = this.get('pki_data');
|
|
var buf = this.serializeForSig();
|
|
var hash = magicHash(buf);
|
|
var publicKey = PublicKey.fromBuffer(pubkey);
|
|
var signature = new Signature().fromString(sig);
|
|
var verified = ECDSA.verify(hash, signature, publicKey);
|
|
return verified;
|
|
};
|
|
|
|
// Helpers
|
|
|
|
PaymentProtocol.PEMtoDER =
|
|
PaymentProtocol.prototype._PEMtoDER = function(pem) {
|
|
return this._PEMtoDERParam(pem);
|
|
};
|
|
|
|
PaymentProtocol.PEMtoDERParam =
|
|
PaymentProtocol.prototype._PEMtoDERParam = function(pem, param) {
|
|
if (Buffer.isBuffer(pem)) {
|
|
pem = pem.toString();
|
|
}
|
|
var start = new RegExp('(?=-----BEGIN ' + (param || '[^-]+') + '-----)', 'i');
|
|
var end = new RegExp('^-----END ' + (param || '[^-]+') + '-----$', 'gmi');
|
|
pem = pem.replace(end, '');
|
|
var parts = pem.split(start);
|
|
return parts.map(function(part) {
|
|
var type = /-----BEGIN ([^-]+)-----/.exec(part)[1];
|
|
part = part.replace(/-----BEGIN ([^-]+)-----/g, '');
|
|
part = part.replace(/\s+/g, '');
|
|
if (!param || type !== param) return;
|
|
return new Buffer(part, 'base64');
|
|
}).filter(Boolean);
|
|
};
|
|
|
|
PaymentProtocol.DERtoPEM =
|
|
PaymentProtocol.prototype._DERtoPEM = function(der, type) {
|
|
if (typeof der === 'string') {
|
|
der = new Buffer(der, 'hex');
|
|
}
|
|
var type = type || 'PRIVACY-ENHANCED MESSAGE';
|
|
der = der.toString('base64');
|
|
der = der.replace(/(.{64})/g, '$1\r\n');
|
|
der = der.replace(/\r\n$/, '');
|
|
return ''
|
|
+ '-----BEGIN ' + type + '-----\r\n'
|
|
+ der
|
|
+ '\r\n-----END ' + type + '-----\r\n';
|
|
};
|
|
|
|
// Expose RootCerts
|
|
PaymentProtocol.getTrusted = RootCerts.getTrusted;
|
|
PaymentProtocol.getCert = RootCerts.getCert;
|
|
PaymentProtocol.parsePEM = RootCerts.parsePEM;
|
|
PaymentProtocol.certs = RootCerts.certs;
|
|
PaymentProtocol.trusted = RootCerts.trusted;
|
|
|
|
module.exports = PaymentProtocol;
|
|
|