[6a3a178] | 1 | // Copyright 2017 Joyent, Inc.
|
---|
| 2 |
|
---|
| 3 | module.exports = {
|
---|
| 4 | read: read,
|
---|
| 5 | write: write
|
---|
| 6 | };
|
---|
| 7 |
|
---|
| 8 | var assert = require('assert-plus');
|
---|
| 9 | var Buffer = require('safer-buffer').Buffer;
|
---|
| 10 | var Key = require('../key');
|
---|
| 11 | var PrivateKey = require('../private-key');
|
---|
| 12 | var utils = require('../utils');
|
---|
| 13 | var SSHBuffer = require('../ssh-buffer');
|
---|
| 14 | var Dhe = require('../dhe');
|
---|
| 15 |
|
---|
| 16 | var supportedAlgos = {
|
---|
| 17 | 'rsa-sha1' : 5,
|
---|
| 18 | 'rsa-sha256' : 8,
|
---|
| 19 | 'rsa-sha512' : 10,
|
---|
| 20 | 'ecdsa-p256-sha256' : 13,
|
---|
| 21 | 'ecdsa-p384-sha384' : 14
|
---|
| 22 | /*
|
---|
| 23 | * ed25519 is hypothetically supported with id 15
|
---|
| 24 | * but the common tools available don't appear to be
|
---|
| 25 | * capable of generating/using ed25519 keys
|
---|
| 26 | */
|
---|
| 27 | };
|
---|
| 28 |
|
---|
| 29 | var supportedAlgosById = {};
|
---|
| 30 | Object.keys(supportedAlgos).forEach(function (k) {
|
---|
| 31 | supportedAlgosById[supportedAlgos[k]] = k.toUpperCase();
|
---|
| 32 | });
|
---|
| 33 |
|
---|
| 34 | function read(buf, options) {
|
---|
| 35 | if (typeof (buf) !== 'string') {
|
---|
| 36 | assert.buffer(buf, 'buf');
|
---|
| 37 | buf = buf.toString('ascii');
|
---|
| 38 | }
|
---|
| 39 | var lines = buf.split('\n');
|
---|
| 40 | if (lines[0].match(/^Private-key-format\: v1/)) {
|
---|
| 41 | var algElems = lines[1].split(' ');
|
---|
| 42 | var algoNum = parseInt(algElems[1], 10);
|
---|
| 43 | var algoName = algElems[2];
|
---|
| 44 | if (!supportedAlgosById[algoNum])
|
---|
| 45 | throw (new Error('Unsupported algorithm: ' + algoName));
|
---|
| 46 | return (readDNSSECPrivateKey(algoNum, lines.slice(2)));
|
---|
| 47 | }
|
---|
| 48 |
|
---|
| 49 | // skip any comment-lines
|
---|
| 50 | var line = 0;
|
---|
| 51 | /* JSSTYLED */
|
---|
| 52 | while (lines[line].match(/^\;/))
|
---|
| 53 | line++;
|
---|
| 54 | // we should now have *one single* line left with our KEY on it.
|
---|
| 55 | if ((lines[line].match(/\. IN KEY /) ||
|
---|
| 56 | lines[line].match(/\. IN DNSKEY /)) && lines[line+1].length === 0) {
|
---|
| 57 | return (readRFC3110(lines[line]));
|
---|
| 58 | }
|
---|
| 59 | throw (new Error('Cannot parse dnssec key'));
|
---|
| 60 | }
|
---|
| 61 |
|
---|
| 62 | function readRFC3110(keyString) {
|
---|
| 63 | var elems = keyString.split(' ');
|
---|
| 64 | //unused var flags = parseInt(elems[3], 10);
|
---|
| 65 | //unused var protocol = parseInt(elems[4], 10);
|
---|
| 66 | var algorithm = parseInt(elems[5], 10);
|
---|
| 67 | if (!supportedAlgosById[algorithm])
|
---|
| 68 | throw (new Error('Unsupported algorithm: ' + algorithm));
|
---|
| 69 | var base64key = elems.slice(6, elems.length).join();
|
---|
| 70 | var keyBuffer = Buffer.from(base64key, 'base64');
|
---|
| 71 | if (supportedAlgosById[algorithm].match(/^RSA-/)) {
|
---|
| 72 | // join the rest of the body into a single base64-blob
|
---|
| 73 | var publicExponentLen = keyBuffer.readUInt8(0);
|
---|
| 74 | if (publicExponentLen != 3 && publicExponentLen != 1)
|
---|
| 75 | throw (new Error('Cannot parse dnssec key: ' +
|
---|
| 76 | 'unsupported exponent length'));
|
---|
| 77 |
|
---|
| 78 | var publicExponent = keyBuffer.slice(1, publicExponentLen+1);
|
---|
| 79 | publicExponent = utils.mpNormalize(publicExponent);
|
---|
| 80 | var modulus = keyBuffer.slice(1+publicExponentLen);
|
---|
| 81 | modulus = utils.mpNormalize(modulus);
|
---|
| 82 | // now, make the key
|
---|
| 83 | var rsaKey = {
|
---|
| 84 | type: 'rsa',
|
---|
| 85 | parts: []
|
---|
| 86 | };
|
---|
| 87 | rsaKey.parts.push({ name: 'e', data: publicExponent});
|
---|
| 88 | rsaKey.parts.push({ name: 'n', data: modulus});
|
---|
| 89 | return (new Key(rsaKey));
|
---|
| 90 | }
|
---|
| 91 | if (supportedAlgosById[algorithm] === 'ECDSA-P384-SHA384' ||
|
---|
| 92 | supportedAlgosById[algorithm] === 'ECDSA-P256-SHA256') {
|
---|
| 93 | var curve = 'nistp384';
|
---|
| 94 | var size = 384;
|
---|
| 95 | if (supportedAlgosById[algorithm].match(/^ECDSA-P256-SHA256/)) {
|
---|
| 96 | curve = 'nistp256';
|
---|
| 97 | size = 256;
|
---|
| 98 | }
|
---|
| 99 |
|
---|
| 100 | var ecdsaKey = {
|
---|
| 101 | type: 'ecdsa',
|
---|
| 102 | curve: curve,
|
---|
| 103 | size: size,
|
---|
| 104 | parts: [
|
---|
| 105 | {name: 'curve', data: Buffer.from(curve) },
|
---|
| 106 | {name: 'Q', data: utils.ecNormalize(keyBuffer) }
|
---|
| 107 | ]
|
---|
| 108 | };
|
---|
| 109 | return (new Key(ecdsaKey));
|
---|
| 110 | }
|
---|
| 111 | throw (new Error('Unsupported algorithm: ' +
|
---|
| 112 | supportedAlgosById[algorithm]));
|
---|
| 113 | }
|
---|
| 114 |
|
---|
| 115 | function elementToBuf(e) {
|
---|
| 116 | return (Buffer.from(e.split(' ')[1], 'base64'));
|
---|
| 117 | }
|
---|
| 118 |
|
---|
| 119 | function readDNSSECRSAPrivateKey(elements) {
|
---|
| 120 | var rsaParams = {};
|
---|
| 121 | elements.forEach(function (element) {
|
---|
| 122 | if (element.split(' ')[0] === 'Modulus:')
|
---|
| 123 | rsaParams['n'] = elementToBuf(element);
|
---|
| 124 | else if (element.split(' ')[0] === 'PublicExponent:')
|
---|
| 125 | rsaParams['e'] = elementToBuf(element);
|
---|
| 126 | else if (element.split(' ')[0] === 'PrivateExponent:')
|
---|
| 127 | rsaParams['d'] = elementToBuf(element);
|
---|
| 128 | else if (element.split(' ')[0] === 'Prime1:')
|
---|
| 129 | rsaParams['p'] = elementToBuf(element);
|
---|
| 130 | else if (element.split(' ')[0] === 'Prime2:')
|
---|
| 131 | rsaParams['q'] = elementToBuf(element);
|
---|
| 132 | else if (element.split(' ')[0] === 'Exponent1:')
|
---|
| 133 | rsaParams['dmodp'] = elementToBuf(element);
|
---|
| 134 | else if (element.split(' ')[0] === 'Exponent2:')
|
---|
| 135 | rsaParams['dmodq'] = elementToBuf(element);
|
---|
| 136 | else if (element.split(' ')[0] === 'Coefficient:')
|
---|
| 137 | rsaParams['iqmp'] = elementToBuf(element);
|
---|
| 138 | });
|
---|
| 139 | // now, make the key
|
---|
| 140 | var key = {
|
---|
| 141 | type: 'rsa',
|
---|
| 142 | parts: [
|
---|
| 143 | { name: 'e', data: utils.mpNormalize(rsaParams['e'])},
|
---|
| 144 | { name: 'n', data: utils.mpNormalize(rsaParams['n'])},
|
---|
| 145 | { name: 'd', data: utils.mpNormalize(rsaParams['d'])},
|
---|
| 146 | { name: 'p', data: utils.mpNormalize(rsaParams['p'])},
|
---|
| 147 | { name: 'q', data: utils.mpNormalize(rsaParams['q'])},
|
---|
| 148 | { name: 'dmodp',
|
---|
| 149 | data: utils.mpNormalize(rsaParams['dmodp'])},
|
---|
| 150 | { name: 'dmodq',
|
---|
| 151 | data: utils.mpNormalize(rsaParams['dmodq'])},
|
---|
| 152 | { name: 'iqmp',
|
---|
| 153 | data: utils.mpNormalize(rsaParams['iqmp'])}
|
---|
| 154 | ]
|
---|
| 155 | };
|
---|
| 156 | return (new PrivateKey(key));
|
---|
| 157 | }
|
---|
| 158 |
|
---|
| 159 | function readDNSSECPrivateKey(alg, elements) {
|
---|
| 160 | if (supportedAlgosById[alg].match(/^RSA-/)) {
|
---|
| 161 | return (readDNSSECRSAPrivateKey(elements));
|
---|
| 162 | }
|
---|
| 163 | if (supportedAlgosById[alg] === 'ECDSA-P384-SHA384' ||
|
---|
| 164 | supportedAlgosById[alg] === 'ECDSA-P256-SHA256') {
|
---|
| 165 | var d = Buffer.from(elements[0].split(' ')[1], 'base64');
|
---|
| 166 | var curve = 'nistp384';
|
---|
| 167 | var size = 384;
|
---|
| 168 | if (supportedAlgosById[alg] === 'ECDSA-P256-SHA256') {
|
---|
| 169 | curve = 'nistp256';
|
---|
| 170 | size = 256;
|
---|
| 171 | }
|
---|
| 172 | // DNSSEC generates the public-key on the fly (go calculate it)
|
---|
| 173 | var publicKey = utils.publicFromPrivateECDSA(curve, d);
|
---|
| 174 | var Q = publicKey.part['Q'].data;
|
---|
| 175 | var ecdsaKey = {
|
---|
| 176 | type: 'ecdsa',
|
---|
| 177 | curve: curve,
|
---|
| 178 | size: size,
|
---|
| 179 | parts: [
|
---|
| 180 | {name: 'curve', data: Buffer.from(curve) },
|
---|
| 181 | {name: 'd', data: d },
|
---|
| 182 | {name: 'Q', data: Q }
|
---|
| 183 | ]
|
---|
| 184 | };
|
---|
| 185 | return (new PrivateKey(ecdsaKey));
|
---|
| 186 | }
|
---|
| 187 | throw (new Error('Unsupported algorithm: ' + supportedAlgosById[alg]));
|
---|
| 188 | }
|
---|
| 189 |
|
---|
| 190 | function dnssecTimestamp(date) {
|
---|
| 191 | var year = date.getFullYear() + ''; //stringify
|
---|
| 192 | var month = (date.getMonth() + 1);
|
---|
| 193 | var timestampStr = year + month + date.getUTCDate();
|
---|
| 194 | timestampStr += '' + date.getUTCHours() + date.getUTCMinutes();
|
---|
| 195 | timestampStr += date.getUTCSeconds();
|
---|
| 196 | return (timestampStr);
|
---|
| 197 | }
|
---|
| 198 |
|
---|
| 199 | function rsaAlgFromOptions(opts) {
|
---|
| 200 | if (!opts || !opts.hashAlgo || opts.hashAlgo === 'sha1')
|
---|
| 201 | return ('5 (RSASHA1)');
|
---|
| 202 | else if (opts.hashAlgo === 'sha256')
|
---|
| 203 | return ('8 (RSASHA256)');
|
---|
| 204 | else if (opts.hashAlgo === 'sha512')
|
---|
| 205 | return ('10 (RSASHA512)');
|
---|
| 206 | else
|
---|
| 207 | throw (new Error('Unknown or unsupported hash: ' +
|
---|
| 208 | opts.hashAlgo));
|
---|
| 209 | }
|
---|
| 210 |
|
---|
| 211 | function writeRSA(key, options) {
|
---|
| 212 | // if we're missing parts, add them.
|
---|
| 213 | if (!key.part.dmodp || !key.part.dmodq) {
|
---|
| 214 | utils.addRSAMissing(key);
|
---|
| 215 | }
|
---|
| 216 |
|
---|
| 217 | var out = '';
|
---|
| 218 | out += 'Private-key-format: v1.3\n';
|
---|
| 219 | out += 'Algorithm: ' + rsaAlgFromOptions(options) + '\n';
|
---|
| 220 | var n = utils.mpDenormalize(key.part['n'].data);
|
---|
| 221 | out += 'Modulus: ' + n.toString('base64') + '\n';
|
---|
| 222 | var e = utils.mpDenormalize(key.part['e'].data);
|
---|
| 223 | out += 'PublicExponent: ' + e.toString('base64') + '\n';
|
---|
| 224 | var d = utils.mpDenormalize(key.part['d'].data);
|
---|
| 225 | out += 'PrivateExponent: ' + d.toString('base64') + '\n';
|
---|
| 226 | var p = utils.mpDenormalize(key.part['p'].data);
|
---|
| 227 | out += 'Prime1: ' + p.toString('base64') + '\n';
|
---|
| 228 | var q = utils.mpDenormalize(key.part['q'].data);
|
---|
| 229 | out += 'Prime2: ' + q.toString('base64') + '\n';
|
---|
| 230 | var dmodp = utils.mpDenormalize(key.part['dmodp'].data);
|
---|
| 231 | out += 'Exponent1: ' + dmodp.toString('base64') + '\n';
|
---|
| 232 | var dmodq = utils.mpDenormalize(key.part['dmodq'].data);
|
---|
| 233 | out += 'Exponent2: ' + dmodq.toString('base64') + '\n';
|
---|
| 234 | var iqmp = utils.mpDenormalize(key.part['iqmp'].data);
|
---|
| 235 | out += 'Coefficient: ' + iqmp.toString('base64') + '\n';
|
---|
| 236 | // Assume that we're valid as-of now
|
---|
| 237 | var timestamp = new Date();
|
---|
| 238 | out += 'Created: ' + dnssecTimestamp(timestamp) + '\n';
|
---|
| 239 | out += 'Publish: ' + dnssecTimestamp(timestamp) + '\n';
|
---|
| 240 | out += 'Activate: ' + dnssecTimestamp(timestamp) + '\n';
|
---|
| 241 | return (Buffer.from(out, 'ascii'));
|
---|
| 242 | }
|
---|
| 243 |
|
---|
| 244 | function writeECDSA(key, options) {
|
---|
| 245 | var out = '';
|
---|
| 246 | out += 'Private-key-format: v1.3\n';
|
---|
| 247 |
|
---|
| 248 | if (key.curve === 'nistp256') {
|
---|
| 249 | out += 'Algorithm: 13 (ECDSAP256SHA256)\n';
|
---|
| 250 | } else if (key.curve === 'nistp384') {
|
---|
| 251 | out += 'Algorithm: 14 (ECDSAP384SHA384)\n';
|
---|
| 252 | } else {
|
---|
| 253 | throw (new Error('Unsupported curve'));
|
---|
| 254 | }
|
---|
| 255 | var base64Key = key.part['d'].data.toString('base64');
|
---|
| 256 | out += 'PrivateKey: ' + base64Key + '\n';
|
---|
| 257 |
|
---|
| 258 | // Assume that we're valid as-of now
|
---|
| 259 | var timestamp = new Date();
|
---|
| 260 | out += 'Created: ' + dnssecTimestamp(timestamp) + '\n';
|
---|
| 261 | out += 'Publish: ' + dnssecTimestamp(timestamp) + '\n';
|
---|
| 262 | out += 'Activate: ' + dnssecTimestamp(timestamp) + '\n';
|
---|
| 263 |
|
---|
| 264 | return (Buffer.from(out, 'ascii'));
|
---|
| 265 | }
|
---|
| 266 |
|
---|
| 267 | function write(key, options) {
|
---|
| 268 | if (PrivateKey.isPrivateKey(key)) {
|
---|
| 269 | if (key.type === 'rsa') {
|
---|
| 270 | return (writeRSA(key, options));
|
---|
| 271 | } else if (key.type === 'ecdsa') {
|
---|
| 272 | return (writeECDSA(key, options));
|
---|
| 273 | } else {
|
---|
| 274 | throw (new Error('Unsupported algorithm: ' + key.type));
|
---|
| 275 | }
|
---|
| 276 | } else if (Key.isKey(key)) {
|
---|
| 277 | /*
|
---|
| 278 | * RFC3110 requires a keyname, and a keytype, which we
|
---|
| 279 | * don't really have a mechanism for specifying such
|
---|
| 280 | * additional metadata.
|
---|
| 281 | */
|
---|
| 282 | throw (new Error('Format "dnssec" only supports ' +
|
---|
| 283 | 'writing private keys'));
|
---|
| 284 | } else {
|
---|
| 285 | throw (new Error('key is not a Key or PrivateKey'));
|
---|
| 286 | }
|
---|
| 287 | }
|
---|