├── .npmignore ├── .gitignore ├── README.md ├── test ├── ecdsa_public.pem ├── ecdsa_private.pem ├── rsa_public.pem ├── rsa_public_encrypted.pem ├── dsa_public.pem ├── dsa_private.pem ├── rsa_private.pem ├── rsa_private_encrypted.pem ├── header.test.js ├── convert.test.js ├── signer.test.js ├── parser.test.js └── verify.test.js ├── lib ├── index.js ├── verify.js ├── utils.js ├── parser.js └── signer.js ├── LICENSE └── package.json /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .nyc_output 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @peertube/http-signature 2 | 3 | Fork of [node-http-signature](https://github.com/joyent/node-http-signature) with [hs2019 support](https://github.com/joyent/node-http-signature/pull/116). 4 | -------------------------------------------------------------------------------- /test/ecdsa_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvqAkfJTX8Ai2HOpazSfZWi5OcAkT 3 | ak7abkqD3E8CgZSfKZN2WJhTlREUjk10KhAOPkUqJMhaJ65kFNBE4Py/xw== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /test/ecdsa_private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEICEEsq6Rxv0c5nUIA0w6QuhGeDSo6uuJ3bPMr6LLwLFIoAoGCCqGSM49 3 | AwEHoUQDQgAEvqAkfJTX8Ai2HOpazSfZWi5OcAkTak7abkqD3E8CgZSfKZN2WJhT 4 | lREUjk10KhAOPkUqJMhaJ65kFNBE4Py/xw== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /test/rsa_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3 3 | 6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6 4 | Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw 5 | oYi+1hqp1fIekaxsyQIDAQAB 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /test/rsa_public_encrypted.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoX3N2q0OzQjA5tu2LKKtBXbZZ 3 | DJjqQzXnFUKOWJc5xo7D8xDWzETOirJLoR6qGf+1rYU+jAyR0YtW9km1wyluzeuM 4 | TerUUioeaDGCo1Sq+euSqb6BQH6xdLFaTHlynuSafgjiEsKp0QNcFWyWFrCH+mJF 5 | v0YyNiv5QX78asG8VwIDAQAB 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /test/dsa_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBtjCCASsGByqGSM44BAEwggEeAoGBANEGuyc1OxdY8DGytI3uVReA0fEW96A5 3 | r45h/R7LZSKtpqVG0QMJk4BauV4DYLNk7/Md57kr6VdKdByl3+i3BUv20mqv6MNj 4 | 8Qd2CVI6xt6FbA6T1ZeFuFCdzzRGU7knRqMAsDkLzRR5M54E803OknxJGth99hYF 5 | gnOKpP34Ajn/AhUAphFod3KtsyksNRtA0WBnUYLs4bUCgYBAF00g76ui09g4sJJ+ 6 | dCrnDJRrGHOzH8oXtuVHuvImN74e5Qasp+qeZSdC9SY+ytwQfg9liV+gQ7FYmpfy 7 | wxm7mKn+NnUTraSZQ39t6D8YTXqPQK6FVNSHy3U1LJzU0sxlvnMdttHeDJBlV0cZ 8 | bKiz1w7yeMihO+PNUPSzleaPJAOBhAACgYBGdoKyqTWjFY1Ab7cGW1acImtpuQyD 9 | iuiJntTQbDyhxXirf5vpw3jdcnVAxIKMCqYEvISBWxzdi0r2ICOXp/cVfDEGt78G 10 | I0C0qpHIFUCGS8mWH2y294+nPrF2dOKORssEtpuFU4ifCNuOo/ovgq6zEK69vwYa 11 | zKCiWPeQQ/2gbg== 12 | -----END PUBLIC KEY----- 13 | -------------------------------------------------------------------------------- /test/dsa_private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN DSA PRIVATE KEY----- 2 | MIIBuwIBAAKBgQDRBrsnNTsXWPAxsrSN7lUXgNHxFvegOa+OYf0ey2UiraalRtED 3 | CZOAWrleA2CzZO/zHee5K+lXSnQcpd/otwVL9tJqr+jDY/EHdglSOsbehWwOk9WX 4 | hbhQnc80RlO5J0ajALA5C80UeTOeBPNNzpJ8SRrYffYWBYJziqT9+AI5/wIVAKYR 5 | aHdyrbMpLDUbQNFgZ1GC7OG1AoGAQBdNIO+rotPYOLCSfnQq5wyUaxhzsx/KF7bl 6 | R7ryJje+HuUGrKfqnmUnQvUmPsrcEH4PZYlfoEOxWJqX8sMZu5ip/jZ1E62kmUN/ 7 | beg/GE16j0CuhVTUh8t1NSyc1NLMZb5zHbbR3gyQZVdHGWyos9cO8njIoTvjzVD0 8 | s5XmjyQCgYBGdoKyqTWjFY1Ab7cGW1acImtpuQyDiuiJntTQbDyhxXirf5vpw3jd 9 | cnVAxIKMCqYEvISBWxzdi0r2ICOXp/cVfDEGt78GI0C0qpHIFUCGS8mWH2y294+n 10 | PrF2dOKORssEtpuFU4ifCNuOo/ovgq6zEK69vwYazKCiWPeQQ/2gbgIVAIFFq/st 11 | tPr/+Kzq2rK0lbJRJcvb 12 | -----END DSA PRIVATE KEY----- 13 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Joyent, Inc. 2 | 3 | var parser = require('./parser'); 4 | var signer = require('./signer'); 5 | var verify = require('./verify'); 6 | var utils = require('./utils'); 7 | 8 | 9 | 10 | ///--- API 11 | 12 | module.exports = { 13 | 14 | parse: parser.parseRequest, 15 | parseRequest: parser.parseRequest, 16 | 17 | sign: signer.signRequest, 18 | signRequest: signer.signRequest, 19 | createSigner: signer.createSigner, 20 | isSigner: signer.isSigner, 21 | 22 | sshKeyToPEM: utils.sshKeyToPEM, 23 | sshKeyFingerprint: utils.fingerprint, 24 | pemToRsaSSHKey: utils.pemToRsaSSHKey, 25 | 26 | verify: verify.verifySignature, 27 | verifySignature: verify.verifySignature, 28 | verifyHMAC: verify.verifyHMAC 29 | }; 30 | -------------------------------------------------------------------------------- /test/rsa_private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF 3 | NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F 4 | UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB 5 | AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA 6 | QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK 7 | kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg 8 | f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u 9 | 412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc 10 | mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7 11 | kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA 12 | gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW 13 | G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI 14 | 7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Joyent, Inc. All rights reserved. 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to 4 | deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 6 | sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 17 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 18 | IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@peertube/http-signature", 3 | "description": "PeerTube fork of reference implementation of Joyent's HTTP Signature scheme.", 4 | "version": "1.7.0", 5 | "license": "MIT", 6 | "author": "Joyent, Inc", 7 | "contributors": [ 8 | "Mark Cavage ", 9 | "David I. Lehn ", 10 | "Patrick Mooney " 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/Chocobozzz/node-http-signature.git" 15 | }, 16 | "homepage": "https://github.com/Chocobozzz/node-http-signature/", 17 | "bugs": "https://github.com/Chocobozzz/node-http-signature/issues", 18 | "keywords": [ 19 | "https", 20 | "request" 21 | ], 22 | "engines": { 23 | "node": ">=0.10" 24 | }, 25 | "main": "lib/index.js", 26 | "files": [ 27 | "lib" 28 | ], 29 | "scripts": { 30 | "test": "tap test/*.js" 31 | }, 32 | "dependencies": { 33 | "assert-plus": "^1.0.0", 34 | "jsprim": "^1.2.2", 35 | "sshpk": "^1.14.1" 36 | }, 37 | "devDependencies": { 38 | "tap": "^16.2.0", 39 | "uuid": "^8.3.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/rsa_private_encrypted.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIC3TBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQISkT2UHxhpiUCAggA 3 | MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBjoylMNYUwmXeqkoJYzJdOBIIC 4 | gMi4cPwI9yUYb1MTSC067Qp8dlamntAD+T4Ol3rfx8qEU0hDawWkpmroZGzh8USb 5 | uHpDTLx0yPlxEw262Ismx1xsrRH1nhu3PvDi0KC50Qgn3+W3tqYjUmNCt4dFSk9l 6 | i7MgpvPHzVaUWryE5TK2iWKW5/QWfJ0FMXS+vzDOdmALaYhiQ2qAwZzyB4DLUFQE 7 | zAy2k1EttkJjkXG/MHkGMJ0uJWD+SCfdi9PykFswNk2Ew4EvGVEKL8/1RTaLFezQ 8 | 3llS4o1TaLPsPawiAZcLNAeS1dvOpikBiDSidMb3dXatjBV19uYYXd5KzJQ1jlq6 9 | hD6K6dStqekk+TGamsQr2XBf9nsn/n60c05IVogDBDhewaLpjh+tSew0iBHrG6z0 10 | z7//kyjx5v8HzqTZ/7VErTkef4scnnWLQ+/uzgK2RgBIakgxWZz2/0zN/8MhoM/B 11 | 7Es8rkB1yjDFUwaNFcjaaTMkb2++CA8PuUxKNXiGEmQ3nylT3WL9kO+fllyNG4M+ 12 | WwaPDMQAsW02f+I51Jm3LB8QycQKsyHL6Ran35aAFrF3ANsf6dzp3oIcQpdt1j1y 13 | pTWQwQ+8GlP5lTtQMdzyH+3HE/HpgkgaAMuYFfK6afByE9X8Br11S7pmZ60v0kzK 14 | 75RDwYo2m8zBLF3hQslSvNHJIDQ9u3KgdOz7xeqoMaKLSUF0JfoGNbT8qq09zE2A 15 | 2dtrwsNxA0O7jHURgvhl/SjlW0bg6fRqfKD6d8oDY085mq5N02wx1Rx9b3MLi6ok 16 | wW6cELTZy3C+NdMgA6EqYAvn4iH+tXoLKKdAMlAPu5ari6wXF9TjctjYE6NNzGcp 17 | B9AZhX+7Y9hSe5CFSIF6DE4= 18 | -----END ENCRYPTED PRIVATE KEY----- 19 | -------------------------------------------------------------------------------- /test/header.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Joyent, Inc. All rights reserved. 2 | 3 | var crypto = require('crypto'); 4 | var fs = require('fs'); 5 | var http = require('http'); 6 | 7 | var test = require('tap').test; 8 | var uuid = require('uuid').v4; 9 | 10 | var httpSignature = require('../lib/index'); 11 | 12 | 13 | 14 | ///--- Globals 15 | 16 | var hmacKey = null; 17 | var httpOptions = null; 18 | var rsaPrivate = null; 19 | var signOptions = null; 20 | var server = null; 21 | var socket = null; 22 | 23 | 24 | 25 | ///--- Tests 26 | 27 | 28 | test('setup', function(t) { 29 | rsaPrivate = fs.readFileSync(__dirname + '/rsa_private.pem', 'ascii'); 30 | t.ok(rsaPrivate); 31 | 32 | socket = '/tmp/.' + uuid(); 33 | 34 | server = http.createServer(function(req, res) { 35 | res.writeHead(200); 36 | res.end(); 37 | }); 38 | 39 | server.listen(socket, function() { 40 | hmacKey = uuid(); 41 | httpOptions = { 42 | socketPath: socket, 43 | path: '/', 44 | method: 'HEAD', 45 | headers: { 46 | 'content-length': '0', 47 | 'x-foo': 'false' 48 | } 49 | }; 50 | 51 | signOptions = { 52 | key: rsaPrivate, 53 | keyId: 'unitTest', 54 | }; 55 | 56 | t.end(); 57 | }); 58 | }); 59 | 60 | 61 | 62 | test('header with 0 value', function(t) { 63 | var req = http.request(httpOptions, function(res) { 64 | t.end(); 65 | }); 66 | var opts = { 67 | keyId: 'unit', 68 | key: rsaPrivate, 69 | headers: ['date', 'request-line', 'content-length'] 70 | }; 71 | 72 | t.ok(httpSignature.sign(req, opts)); 73 | t.ok(req.getHeader('Authorization')); 74 | console.log('> ' + req.getHeader('Authorization')); 75 | req.end(); 76 | }); 77 | 78 | test('header with boolean-mungable value', function(t) { 79 | var req = http.request(httpOptions, function(res) { 80 | t.end(); 81 | }); 82 | var opts = { 83 | keyId: 'unit', 84 | key: rsaPrivate, 85 | headers: ['date', 'x-foo'] 86 | }; 87 | 88 | t.ok(httpSignature.sign(req, opts)); 89 | t.ok(req.getHeader('Authorization')); 90 | console.log('> ' + req.getHeader('Authorization')); 91 | req.end(); 92 | }); 93 | 94 | test('tear down', function(t) { 95 | server.on('close', function() { 96 | t.end(); 97 | }); 98 | server.close(); 99 | }); 100 | -------------------------------------------------------------------------------- /lib/verify.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Joyent, Inc. 2 | 3 | var assert = require('assert-plus'); 4 | var crypto = require('crypto'); 5 | var sshpk = require('sshpk'); 6 | var utils = require('./utils'); 7 | 8 | var HASH_ALGOS = utils.HASH_ALGOS; 9 | var PK_ALGOS = utils.PK_ALGOS; 10 | var InvalidAlgorithmError = utils.InvalidAlgorithmError; 11 | var HttpSignatureError = utils.HttpSignatureError; 12 | var validateAlgorithm = utils.validateAlgorithm; 13 | 14 | ///--- Exported API 15 | 16 | module.exports = { 17 | /** 18 | * Verify RSA/DSA signature against public key. You are expected to pass in 19 | * an object that was returned from `parse()`. 20 | * 21 | * @param {Object} parsedSignature the object you got from `parse`. 22 | * @param {String} pubkey RSA/DSA private key PEM. 23 | * @return {Boolean} true if valid, false otherwise. 24 | * @throws {TypeError} if you pass in bad arguments. 25 | * @throws {InvalidAlgorithmError} 26 | */ 27 | verifySignature: function verifySignature(parsedSignature, pubkey) { 28 | assert.object(parsedSignature, 'parsedSignature'); 29 | if (typeof (pubkey) === 'string' || Buffer.isBuffer(pubkey)) 30 | pubkey = sshpk.parseKey(pubkey); 31 | assert.ok(sshpk.Key.isKey(pubkey, [1, 1]), 'pubkey must be a sshpk.Key'); 32 | 33 | var alg = validateAlgorithm(parsedSignature.algorithm, pubkey.type); 34 | if (alg[0] === 'hmac' || alg[0] !== pubkey.type) 35 | return false; 36 | 37 | var v = pubkey.createVerify(alg[1]); 38 | v.update(parsedSignature.signingString); 39 | return (v.verify(parsedSignature.params.signature, 'base64')); 40 | }, 41 | 42 | /** 43 | * Verify HMAC against shared secret. You are expected to pass in an object 44 | * that was returned from `parse()`. 45 | * 46 | * @param {Object} parsedSignature the object you got from `parse`. 47 | * @param {String} or {Buffer} secret HMAC shared secret. 48 | * @return {Boolean} true if valid, false otherwise. 49 | * @throws {TypeError} if you pass in bad arguments. 50 | * @throws {InvalidAlgorithmError} 51 | */ 52 | verifyHMAC: function verifyHMAC(parsedSignature, secret) { 53 | assert.object(parsedSignature, 'parsedHMAC'); 54 | assert(typeof (secret) === 'string' || Buffer.isBuffer(secret)); 55 | 56 | var alg = validateAlgorithm(parsedSignature.algorithm); 57 | if (alg[0] !== 'hmac') 58 | return (false); 59 | 60 | var hashAlg = alg[1].toUpperCase(); 61 | 62 | var hmac = crypto.createHmac(hashAlg, secret); 63 | hmac.update(parsedSignature.signingString); 64 | 65 | /* 66 | * Now double-hash to avoid leaking timing information - there's 67 | * no easy constant-time compare in JS, so we use this approach 68 | * instead. See for more info: 69 | * https://www.isecpartners.com/blog/2011/february/double-hmac- 70 | * verification.aspx 71 | */ 72 | var h1 = crypto.createHmac(hashAlg, secret); 73 | h1.update(hmac.digest()); 74 | h1 = h1.digest(); 75 | var h2 = crypto.createHmac(hashAlg, secret); 76 | h2.update(Buffer.from(parsedSignature.params.signature, 'base64')); 77 | h2 = h2.digest(); 78 | 79 | /* Node 0.8 returns strings from .digest(). */ 80 | if (typeof (h1) === 'string') 81 | return (h1 === h2); 82 | /* And node 0.10 lacks the .equals() method on Buffers. */ 83 | if (Buffer.isBuffer(h1) && !h1.equals) 84 | return (h1.toString('binary') === h2.toString('binary')); 85 | 86 | return (h1.equals(h2)); 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Joyent, Inc. All rights reserved. 2 | 3 | var assert = require('assert-plus'); 4 | var sshpk = require('sshpk'); 5 | var util = require('util'); 6 | 7 | var HASH_ALGOS = { 8 | 'sha1': true, 9 | 'sha256': true, 10 | 'sha512': true 11 | }; 12 | 13 | var PK_ALGOS = { 14 | 'rsa': true, 15 | 'dsa': true, 16 | 'ecdsa': true, 17 | 'ed25519': true 18 | }; 19 | 20 | var HEADER = { 21 | AUTH: 'authorization', 22 | SIG: 'signature' 23 | }; 24 | 25 | function HttpSignatureError(message, caller) { 26 | if (Error.captureStackTrace) 27 | Error.captureStackTrace(this, caller || HttpSignatureError); 28 | 29 | this.message = message; 30 | this.name = caller.name; 31 | } 32 | util.inherits(HttpSignatureError, Error); 33 | 34 | function InvalidAlgorithmError(message) { 35 | HttpSignatureError.call(this, message, InvalidAlgorithmError); 36 | } 37 | util.inherits(InvalidAlgorithmError, HttpSignatureError); 38 | 39 | /** 40 | * @param algorithm {String} the algorithm of the signature 41 | * @param publicKeyType {String?} fallback algorithm (public key type) for 42 | * hs2019 43 | * @returns {[string, string]} 44 | */ 45 | function validateAlgorithm(algorithm, publicKeyType) { 46 | assert.string(algorithm, 'algorithm'); 47 | assert.optionalString(publicKeyType, 'publicKeyType'); 48 | 49 | var alg = algorithm.toLowerCase().split('-'); 50 | 51 | if (alg[0] === 'hs2019') { 52 | if (publicKeyType === 'ed25519') { 53 | return validateAlgorithm('ed25519-sha512') 54 | } else if (publicKeyType !== undefined) { 55 | return validateAlgorithm(publicKeyType + '-sha256') 56 | } 57 | 58 | return ['hs2019', 'sha256']; 59 | } 60 | 61 | if (alg.length !== 2) { 62 | throw (new InvalidAlgorithmError(alg[0].toUpperCase() + ' is not a ' + 63 | 'valid algorithm')); 64 | } 65 | 66 | if (alg[0] !== 'hmac' && !PK_ALGOS[alg[0]]) { 67 | throw (new InvalidAlgorithmError(alg[0].toUpperCase() + ' type keys ' + 68 | 'are not supported')); 69 | } 70 | 71 | if (!HASH_ALGOS[alg[1]]) { 72 | throw (new InvalidAlgorithmError(alg[1].toUpperCase() + ' is not a ' + 73 | 'supported hash algorithm')); 74 | } 75 | 76 | return (alg); 77 | } 78 | 79 | ///--- API 80 | 81 | module.exports = { 82 | HEADER: HEADER, 83 | 84 | HASH_ALGOS: HASH_ALGOS, 85 | PK_ALGOS: PK_ALGOS, 86 | 87 | HttpSignatureError: HttpSignatureError, 88 | InvalidAlgorithmError: InvalidAlgorithmError, 89 | 90 | validateAlgorithm: validateAlgorithm, 91 | 92 | /** 93 | * Converts an OpenSSH public key (rsa only) to a PKCS#8 PEM file. 94 | * 95 | * The intent of this module is to interoperate with OpenSSL only, 96 | * specifically the node crypto module's `verify` method. 97 | * 98 | * @param {String} key an OpenSSH public key. 99 | * @return {String} PEM encoded form of the RSA public key. 100 | * @throws {TypeError} on bad input. 101 | * @throws {Error} on invalid ssh key formatted data. 102 | */ 103 | sshKeyToPEM: function sshKeyToPEM(key) { 104 | assert.string(key, 'ssh_key'); 105 | 106 | var k = sshpk.parseKey(key, 'ssh'); 107 | return (k.toString('pem')); 108 | }, 109 | 110 | 111 | /** 112 | * Generates an OpenSSH fingerprint from an ssh public key. 113 | * 114 | * @param {String} key an OpenSSH public key. 115 | * @return {String} key fingerprint. 116 | * @throws {TypeError} on bad input. 117 | * @throws {Error} if what you passed doesn't look like an ssh public key. 118 | */ 119 | fingerprint: function fingerprint(key) { 120 | assert.string(key, 'ssh_key'); 121 | 122 | var k = sshpk.parseKey(key, 'ssh'); 123 | return (k.fingerprint('md5').toString('hex')); 124 | }, 125 | 126 | /** 127 | * Converts a PKGCS#8 PEM file to an OpenSSH public key (rsa) 128 | * 129 | * The reverse of the above function. 130 | */ 131 | pemToRsaSSHKey: function pemToRsaSSHKey(pem, comment) { 132 | assert.equal('string', typeof (pem), 'typeof pem'); 133 | 134 | var k = sshpk.parseKey(pem, 'pem'); 135 | k.comment = comment; 136 | return (k.toString('ssh')); 137 | } 138 | }; 139 | -------------------------------------------------------------------------------- /test/convert.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Joyent, Inc. All rights reserved. 2 | 3 | var test = require('tap').test; 4 | 5 | var sshKeyFingerprint = require('../lib/index').sshKeyFingerprint; 6 | var sshKeyToPEM = require('../lib/index').sshKeyToPEM; 7 | var pemToRsaSSHKey = require('../lib/index').pemToRsaSSHKey; 8 | 9 | 10 | 11 | ///--- Globals 12 | var SSH_1024 = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAvad19ePSDckmgmo6Unqmd8' + 13 | 'n2G7o1794VN3FazVhV09yooXIuUhA+7OmT7ChiHueayxSubgL2MrO/HvvF/GGVUs/t3e0u4' + 14 | '5YwRC51EVhyDuqthVJWjKrYxgDMbHru8fc1oV51l0bKdmvmJWbA/VyeJvstoX+eiSGT3Jge' + 15 | 'egSMVtc= mark@foo.local'; 16 | var PEM_1024 = '-----BEGIN PUBLIC KEY-----\n' + 17 | 'MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQC9p3X149INySaCajpSeqZ3yfYb\n' + 18 | 'ujXv3hU3cVrNWFXT3Kihci5SED7s6ZPsKGIe55rLFK5uAvYys78e+8X8YZVSz+3d\n' + 19 | '7S7jljBELnURWHIO6q2FUlaMqtjGAMxseu7x9zWhXnWXRsp2a+YlZsD9XJ4m+y2h\n' + 20 | 'f56JIZPcmB56BIxW1wIBIw==\n' + 21 | '-----END PUBLIC KEY-----\n'; 22 | 23 | var SSH_2048 = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAr+isTwMYqwCAcY0Yb2F0pF' + 24 | '+/F4/wxGzcrLR2PrgoBXwjj/TnEA3tJ7v08Rru3lAd/O59B6TbXOsYbQ+2Syd82Dm8L3SJR' + 25 | 'NlZJ6DZUOAwnTOoNgkfH2CsbGS84aTPTeXjmMsw52GvQ9yWFDUglHzMIzK2iSHWNl1dAaBE' + 26 | 'iddifGmrpUTPJ5Tt7l8YS4jdaBf6klS+3CvL6xET/RjZhKGtrrgsRRYUB2XVtgQhKDu7PtD' + 27 | 'dlpy4+VISdVhZSlXFnBhya/1KxLS5UFHSAdOjdxzW1bh3cPzNtuPXZaiWUHvyIWpGVCzj5N' + 28 | 'yeDXcc7n0E20yx9ZDkAITuI8X49rnQzuCN5Q== mark@bluesnoop.local'; 29 | var PEM_2048 = '-----BEGIN PUBLIC KEY-----\n' + 30 | 'MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEAr+isTwMYqwCAcY0Yb2F0\n' + 31 | 'pF+/F4/wxGzcrLR2PrgoBXwjj/TnEA3tJ7v08Rru3lAd/O59B6TbXOsYbQ+2Syd8\n' + 32 | '2Dm8L3SJRNlZJ6DZUOAwnTOoNgkfH2CsbGS84aTPTeXjmMsw52GvQ9yWFDUglHzM\n' + 33 | 'IzK2iSHWNl1dAaBEiddifGmrpUTPJ5Tt7l8YS4jdaBf6klS+3CvL6xET/RjZhKGt\n' + 34 | 'rrgsRRYUB2XVtgQhKDu7PtDdlpy4+VISdVhZSlXFnBhya/1KxLS5UFHSAdOjdxzW\n' + 35 | '1bh3cPzNtuPXZaiWUHvyIWpGVCzj5NyeDXcc7n0E20yx9ZDkAITuI8X49rnQzuCN\n' + 36 | '5QIBIw==\n' + 37 | '-----END PUBLIC KEY-----\n'; 38 | 39 | var SSH_4096 = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAgEAsWUdvcKBBjW4GJ8Uyo0S8U' + 40 | 'FFZbg5bqeRWPHcR2eIbo/k7M54PmWFqNL3YCIR8cRsvsFuYObnVaY01p1p/9+tpN4ezaHS5' + 41 | '9glhADTSva3uLrYuWA1FCKFi6/rXn9WkM5diSVrrTXzaQE8ZsVRA5QG6AeWhC3x/HNbiJOG' + 42 | 'd9u0xrzYnyjrhO6x7eCnSz/AtNURLyWHbZ9Q0VEY5UVQsfAmmAAownMTth1m7KRG/KgM1Oz' + 43 | '9Dc+IUHYf0pjxFLQVQgqPnOLsj8OIJEt9SbZR33n66UJezbsbm0uJ+ophA3W/OacvHzCmoL' + 44 | 'm9PaCwYEZ2pIlYlhkGGu6CFpfXhYUne61WAV8xR8pDXaIL7BqLRJZKlxPzrg9Iu278V9XeL' + 45 | 'CnandXIGpaKwC5p7N/K6JoLB+nI1xd4X1NIftaBouxmYTXJy1VK2DKkD+KyvUPtN7EXnC4G' + 46 | 'E4eDn9nibIj35GjfiDXrxcPPaJhSVzqvIIt55XcAnUEEVtiKtxICKwTSbvsojML5hL/gdeu' + 47 | 'MWnMxj1nsZzTgSurD2OFaQ22k5HGu9aC+duNvvgjXWou7BsS/vH1QbP8GbIvYKlO5xNIj9z' + 48 | 'kjINP3nCX4K1+IpW3PDkgS/DleUhUlvhxb10kc4af+9xViAGkV71WqNcoY+PAETvEbDbYpg' + 49 | 'VEBd4mwFJLl/DT2Nlbj9q0= mark@bluesnoop.local'; 50 | var PEM_4096 = '-----BEGIN PUBLIC KEY-----\n' + 51 | 'MIICIDANBgkqhkiG9w0BAQEFAAOCAg0AMIICCAKCAgEAsWUdvcKBBjW4GJ8Uyo0S\n' + 52 | '8UFFZbg5bqeRWPHcR2eIbo/k7M54PmWFqNL3YCIR8cRsvsFuYObnVaY01p1p/9+t\n' + 53 | 'pN4ezaHS59glhADTSva3uLrYuWA1FCKFi6/rXn9WkM5diSVrrTXzaQE8ZsVRA5QG\n' + 54 | '6AeWhC3x/HNbiJOGd9u0xrzYnyjrhO6x7eCnSz/AtNURLyWHbZ9Q0VEY5UVQsfAm\n' + 55 | 'mAAownMTth1m7KRG/KgM1Oz9Dc+IUHYf0pjxFLQVQgqPnOLsj8OIJEt9SbZR33n6\n' + 56 | '6UJezbsbm0uJ+ophA3W/OacvHzCmoLm9PaCwYEZ2pIlYlhkGGu6CFpfXhYUne61W\n' + 57 | 'AV8xR8pDXaIL7BqLRJZKlxPzrg9Iu278V9XeLCnandXIGpaKwC5p7N/K6JoLB+nI\n' + 58 | '1xd4X1NIftaBouxmYTXJy1VK2DKkD+KyvUPtN7EXnC4GE4eDn9nibIj35GjfiDXr\n' + 59 | 'xcPPaJhSVzqvIIt55XcAnUEEVtiKtxICKwTSbvsojML5hL/gdeuMWnMxj1nsZzTg\n' + 60 | 'SurD2OFaQ22k5HGu9aC+duNvvgjXWou7BsS/vH1QbP8GbIvYKlO5xNIj9zkjINP3\n' + 61 | 'nCX4K1+IpW3PDkgS/DleUhUlvhxb10kc4af+9xViAGkV71WqNcoY+PAETvEbDbYp\n' + 62 | 'gVEBd4mwFJLl/DT2Nlbj9q0CASM=\n' + 63 | '-----END PUBLIC KEY-----\n'; 64 | 65 | var DSA_1024 = 'ssh-dss AAAAB3NzaC1kc3MAAACBAKK5sckoM05sOPajUcTWG0zPTvyRmj6' + 66 | 'YQ1g2IgezUUrXgY+2PPy07+JrQi8SN9qr/CBP+0q0Ec48qVFf9LlkUBwu9Jf5HTUVNiKNj3c' + 67 | 'SRPFH8HqZn+nxhVsOLhnHWxgDQ8OOm48Ma61NcYVo2B0Ne8cUs8xSqLqba2EG9ze87FQZAAA' + 68 | 'AFQCVP/xpiAofZRD8L4QFwxOW9krikQAAAIACNv0EmKr+nIA13fjhpiqbYYyVXYOiWM4cmOD' + 69 | 'G/d1J8/vR4YhWHWPbAEw7LD0DEwDIHLlRZr/1jsHbFcwt4tzRs95fyHzpucpGhocmjWx43qt' + 70 | 'xEhDeJrxPlkIXHakciAEhoo+5YeRSSgRse5PrZDosdr5fA+DADs8tnto5Glf5owAAAIBHcEF' + 71 | '5ytvCRiKbsWKOgeMZ7JT/XGX+hMhS7aaJ2IspKj7YsWada1yBwoM6yYHtlpnGsq/PoPaZU8K' + 72 | '40f47psV6OhSh+/O/jgqLS/Ur2c0mQQqIb7vvkc7he/SPOQAqyDmyYFBuazuSf2s9Uy2hfvj' + 73 | 'Wgb6X+vN9W8SOb2668IL7Vg== mark@bluesnoop.local'; 74 | var DSA_1024_PEM = '-----BEGIN PUBLIC KEY-----\n' + 75 | 'MIIBtjCCASsGByqGSM44BAEwggEeAoGBAKK5sckoM05sOPajUcTWG0zPTvyRmj6Y\n' + 76 | 'Q1g2IgezUUrXgY+2PPy07+JrQi8SN9qr/CBP+0q0Ec48qVFf9LlkUBwu9Jf5HTUV\n' + 77 | 'NiKNj3cSRPFH8HqZn+nxhVsOLhnHWxgDQ8OOm48Ma61NcYVo2B0Ne8cUs8xSqLqb\n' + 78 | 'a2EG9ze87FQZAhUAlT/8aYgKH2UQ/C+EBcMTlvZK4pECgYACNv0EmKr+nIA13fjh\n' + 79 | 'piqbYYyVXYOiWM4cmODG/d1J8/vR4YhWHWPbAEw7LD0DEwDIHLlRZr/1jsHbFcwt\n' + 80 | '4tzRs95fyHzpucpGhocmjWx43qtxEhDeJrxPlkIXHakciAEhoo+5YeRSSgRse5Pr\n' + 81 | 'ZDosdr5fA+DADs8tnto5Glf5owOBhAACgYBHcEF5ytvCRiKbsWKOgeMZ7JT/XGX+\n' + 82 | 'hMhS7aaJ2IspKj7YsWada1yBwoM6yYHtlpnGsq/PoPaZU8K40f47psV6OhSh+/O/\n' + 83 | 'jgqLS/Ur2c0mQQqIb7vvkc7he/SPOQAqyDmyYFBuazuSf2s9Uy2hfvjWgb6X+vN9\n' + 84 | 'W8SOb2668IL7Vg==\n' + 85 | '-----END PUBLIC KEY-----\n'; 86 | 87 | ///--- Tests 88 | 89 | test('1024b pem to rsa ssh key', function(t) { 90 | t.equal(pemToRsaSSHKey(PEM_1024, 'mark@foo.local'), SSH_1024); 91 | t.end(); 92 | }); 93 | 94 | test('2048b pem to rsa ssh key', function(t) { 95 | t.equal(pemToRsaSSHKey(PEM_2048, 'mark@bluesnoop.local'), SSH_2048); 96 | t.end(); 97 | }); 98 | 99 | test('4096b pem to rsa ssh key', function(t) { 100 | t.equal(pemToRsaSSHKey(PEM_4096, 'mark@bluesnoop.local'), SSH_4096); 101 | t.end(); 102 | }); 103 | 104 | test('1024b rsa ssh key', function(t) { 105 | t.equal(sshKeyToPEM(SSH_1024), PEM_1024); 106 | t.end(); 107 | }); 108 | 109 | test('2048b rsa ssh key', function(t) { 110 | t.equal(sshKeyToPEM(SSH_2048), PEM_2048); 111 | t.end(); 112 | }); 113 | 114 | 115 | test('4096b rsa ssh key', function(t) { 116 | t.equal(sshKeyToPEM(SSH_4096), PEM_4096); 117 | t.end(); 118 | }); 119 | 120 | 121 | test('1024b dsa ssh key', function(t) { 122 | t.equal(sshKeyToPEM(DSA_1024), DSA_1024_PEM); 123 | t.end(); 124 | }); 125 | 126 | test('fingerprint', function(t) { 127 | var fp = sshKeyFingerprint(SSH_1024); 128 | t.equal(fp, '59:a4:61:0e:38:18:9f:0f:28:58:2a:27:f7:65:c5:87'); 129 | t.end(); 130 | }); 131 | 132 | 133 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Joyent, Inc. All rights reserved. 2 | 3 | var assert = require('assert-plus'); 4 | var util = require('util'); 5 | var utils = require('./utils'); 6 | 7 | 8 | 9 | ///--- Globals 10 | 11 | var HASH_ALGOS = utils.HASH_ALGOS; 12 | var PK_ALGOS = utils.PK_ALGOS; 13 | var HttpSignatureError = utils.HttpSignatureError; 14 | var InvalidAlgorithmError = utils.InvalidAlgorithmError; 15 | var validateAlgorithm = utils.validateAlgorithm; 16 | 17 | var State = { 18 | New: 0, 19 | Params: 1 20 | }; 21 | 22 | var ParamsState = { 23 | Name: 0, 24 | Quote: 1, 25 | Value: 2, 26 | Comma: 3, 27 | Number: 4 28 | }; 29 | 30 | ///--- Specific Errors 31 | 32 | 33 | function ExpiredRequestError(message) { 34 | HttpSignatureError.call(this, message, ExpiredRequestError); 35 | } 36 | util.inherits(ExpiredRequestError, HttpSignatureError); 37 | 38 | 39 | function InvalidHeaderError(message) { 40 | HttpSignatureError.call(this, message, InvalidHeaderError); 41 | } 42 | util.inherits(InvalidHeaderError, HttpSignatureError); 43 | 44 | 45 | function InvalidParamsError(message) { 46 | HttpSignatureError.call(this, message, InvalidParamsError); 47 | } 48 | util.inherits(InvalidParamsError, HttpSignatureError); 49 | 50 | 51 | function MissingHeaderError(message) { 52 | HttpSignatureError.call(this, message, MissingHeaderError); 53 | } 54 | util.inherits(MissingHeaderError, HttpSignatureError); 55 | 56 | function StrictParsingError(message) { 57 | HttpSignatureError.call(this, message, StrictParsingError); 58 | } 59 | util.inherits(StrictParsingError, HttpSignatureError); 60 | 61 | ///--- Exported API 62 | 63 | module.exports = { 64 | 65 | /** 66 | * Parses the 'Authorization' header out of an http.ServerRequest object. 67 | * 68 | * Note that this API will fully validate the Authorization header, and throw 69 | * on any error. It will not however check the signature, or the keyId format 70 | * as those are specific to your environment. You can use the options object 71 | * to pass in extra constraints. 72 | * 73 | * As a response object you can expect this: 74 | * 75 | * { 76 | * "scheme": "Signature", 77 | * "params": { 78 | * "keyId": "foo", 79 | * "algorithm": "rsa-sha256", 80 | * "headers": [ 81 | * "date" or "x-date", 82 | * "digest" 83 | * ], 84 | * "signature": "base64" 85 | * }, 86 | * "signingString": "ready to be passed to crypto.verify()" 87 | * } 88 | * 89 | * @param {Object} request an http.ServerRequest. 90 | * @param {Object} options an optional options object with: 91 | * - clockSkew: allowed clock skew in seconds (default 300). 92 | * - headers: required header names (def: date or x-date) 93 | * - algorithms: algorithms to support (default: all). 94 | * - strict: should enforce latest spec parsing 95 | * (default: false). 96 | * @return {Object} parsed out object (see above). 97 | * @throws {TypeError} on invalid input. 98 | * @throws {InvalidHeaderError} on an invalid Authorization header error. 99 | * @throws {InvalidParamsError} if the params in the scheme are invalid. 100 | * @throws {MissingHeaderError} if the params indicate a header not present, 101 | * either in the request headers from the params, 102 | * or not in the params from a required header 103 | * in options. 104 | * @throws {StrictParsingError} if old attributes are used in strict parsing 105 | * mode. 106 | * @throws {ExpiredRequestError} if the value of date or x-date exceeds skew. 107 | */ 108 | parseRequest: function parseRequest(request, options) { 109 | assert.object(request, 'request'); 110 | assert.object(request.headers, 'request.headers'); 111 | if (options === undefined) { 112 | options = {}; 113 | } 114 | assert.object(options, 'options'); 115 | assert.optionalFinite(options.clockSkew, 'options.clockSkew'); 116 | 117 | var headers = [request.headers['x-date'] ? 'x-date' : 'date']; 118 | if (options.headers !== undefined) { 119 | assert.arrayOfString(headers, 'options.headers'); 120 | headers = options.headers; 121 | } 122 | 123 | var authzHeaderName = options.authorizationHeaderName; 124 | var authz = request.headers[authzHeaderName] || 125 | request.headers[utils.HEADER.AUTH] || request.headers[utils.HEADER.SIG]; 126 | 127 | if (!authz) { 128 | var errHeader = authzHeaderName ? authzHeaderName : 129 | utils.HEADER.AUTH + ' or ' + utils.HEADER.SIG; 130 | 131 | throw new MissingHeaderError('no ' + errHeader + ' header ' + 132 | 'present in the request'); 133 | } 134 | 135 | options.clockSkew = options.clockSkew || 300; 136 | 137 | 138 | var i = 0; 139 | var state = authz === request.headers[utils.HEADER.SIG] ? 140 | State.Params : State.New; 141 | var substate = ParamsState.Name; 142 | var tmpName = ''; 143 | var tmpValue = ''; 144 | 145 | var parsed = { 146 | scheme: authz === request.headers[utils.HEADER.SIG] ? 'Signature' : '', 147 | params: {}, 148 | signingString: '' 149 | }; 150 | 151 | for (i = 0; i < authz.length; i++) { 152 | var c = authz.charAt(i); 153 | 154 | switch (Number(state)) { 155 | 156 | case State.New: 157 | if (c !== ' ') parsed.scheme += c; 158 | else state = State.Params; 159 | break; 160 | 161 | case State.Params: 162 | switch (Number(substate)) { 163 | 164 | case ParamsState.Name: 165 | var code = c.charCodeAt(0); 166 | // restricted name of A-Z / a-z 167 | if ((code >= 0x41 && code <= 0x5a) || // A-Z 168 | (code >= 0x61 && code <= 0x7a)) { // a-z 169 | tmpName += c; 170 | } else if (c === '=') { 171 | if (tmpName.length === 0) 172 | throw new InvalidHeaderError('bad param format'); 173 | substate = ParamsState.Quote; 174 | } else { 175 | throw new InvalidHeaderError('bad param format'); 176 | } 177 | break; 178 | 179 | case ParamsState.Quote: 180 | if (c === '"') { 181 | tmpValue = ''; 182 | substate = ParamsState.Value; 183 | } else { 184 | //number 185 | substate = ParamsState.Number; 186 | code = c.charCodeAt(0); 187 | if (code < 0x30 || code > 0x39) { //character not in 0-9 188 | throw new InvalidHeaderError('bad param format'); 189 | } 190 | tmpValue = c; 191 | } 192 | break; 193 | 194 | case ParamsState.Value: 195 | if (c === '"') { 196 | parsed.params[tmpName] = tmpValue; 197 | substate = ParamsState.Comma; 198 | } else { 199 | tmpValue += c; 200 | } 201 | break; 202 | 203 | case ParamsState.Number: 204 | if (c === ',') { 205 | parsed.params[tmpName] = parseInt(tmpValue, 10); 206 | tmpName = ''; 207 | substate = ParamsState.Name; 208 | } else { 209 | code = c.charCodeAt(0); 210 | if (code < 0x30 || code > 0x39) { //character not in 0-9 211 | throw new InvalidHeaderError('bad param format'); 212 | } 213 | tmpValue += c; 214 | } 215 | break; 216 | 217 | 218 | case ParamsState.Comma: 219 | if (c === ',') { 220 | tmpName = ''; 221 | substate = ParamsState.Name; 222 | } else { 223 | throw new InvalidHeaderError('bad param format'); 224 | } 225 | break; 226 | 227 | default: 228 | throw new Error('Invalid substate'); 229 | } 230 | break; 231 | 232 | default: 233 | throw new Error('Invalid substate'); 234 | } 235 | 236 | } 237 | 238 | if (!parsed.params.headers || parsed.params.headers === '') { 239 | if (request.headers['x-date']) { 240 | parsed.params.headers = ['x-date']; 241 | } else { 242 | parsed.params.headers = ['date']; 243 | } 244 | } else { 245 | parsed.params.headers = parsed.params.headers.split(' '); 246 | } 247 | 248 | // Minimally validate the parsed object 249 | if (!parsed.scheme || parsed.scheme !== 'Signature') 250 | throw new InvalidHeaderError('scheme was not "Signature"'); 251 | 252 | if (!parsed.params.keyId) 253 | throw new InvalidHeaderError('keyId was not specified'); 254 | 255 | if (!parsed.params.algorithm) 256 | throw new InvalidHeaderError('algorithm was not specified'); 257 | 258 | if (!parsed.params.signature) 259 | throw new InvalidHeaderError('signature was not specified'); 260 | 261 | // Check the algorithm against the official list 262 | try { 263 | validateAlgorithm(parsed.params.algorithm); 264 | } catch (e) { 265 | if (e instanceof InvalidAlgorithmError) 266 | throw (new InvalidParamsError(parsed.params.algorithm + ' is not ' + 267 | 'supported')); 268 | else 269 | throw (e); 270 | } 271 | 272 | // Build the signingString 273 | for (i = 0; i < parsed.params.headers.length; i++) { 274 | var h = parsed.params.headers[i].toLowerCase(); 275 | parsed.params.headers[i] = h; 276 | 277 | if (h === 'request-line') { 278 | if (!options.strict) { 279 | /* 280 | * We allow headers from the older spec drafts if strict parsing isn't 281 | * specified in options. 282 | */ 283 | parsed.signingString += 284 | request.method + ' ' + request.url + ' HTTP/' + request.httpVersion; 285 | } else { 286 | /* Strict parsing doesn't allow older draft headers. */ 287 | throw (new StrictParsingError('request-line is not a valid header ' + 288 | 'with strict parsing enabled.')); 289 | } 290 | } else if (h === '(request-target)') { 291 | parsed.signingString += 292 | '(request-target): ' + request.method.toLowerCase() + ' ' + 293 | request.url; 294 | } else if (h === '(keyid)') { 295 | parsed.signingString += '(keyid): ' + parsed.params.keyId; 296 | } else if (h === '(algorithm)') { 297 | parsed.signingString += '(algorithm): ' + parsed.params.algorithm; 298 | } else if (h === '(opaque)') { 299 | var opaque = parsed.params.opaque; 300 | if (opaque === undefined) { 301 | throw new MissingHeaderError('opaque param was not in the ' + 302 | authzHeaderName + ' header'); 303 | } 304 | parsed.signingString += '(opaque): ' + opaque; 305 | } else if (h === '(created)') { 306 | parsed.signingString += '(created): ' + parsed.params.created; 307 | } else if (h === '(expires)') { 308 | parsed.signingString += '(expires): ' + parsed.params.expires; 309 | } else { 310 | var value = request.headers[h]; 311 | if (value === undefined) 312 | throw new MissingHeaderError(h + ' was not in the request'); 313 | parsed.signingString += h + ': ' + value; 314 | } 315 | 316 | if ((i + 1) < parsed.params.headers.length) 317 | parsed.signingString += '\n'; 318 | } 319 | 320 | // Check against the constraints 321 | var date; 322 | var skew; 323 | if (request.headers.date || request.headers['x-date']) { 324 | if (request.headers['x-date']) { 325 | date = new Date(request.headers['x-date']); 326 | } else { 327 | date = new Date(request.headers.date); 328 | } 329 | var now = new Date(); 330 | skew = Math.abs(now.getTime() - date.getTime()); 331 | 332 | if (skew > options.clockSkew * 1000) { 333 | throw new ExpiredRequestError('clock skew of ' + 334 | (skew / 1000) + 335 | 's was greater than ' + 336 | options.clockSkew + 's'); 337 | } 338 | } 339 | 340 | if (parsed.params.created) { 341 | skew = parsed.params.created - Math.floor(Date.now() / 1000); 342 | if (skew > options.clockSkew) { 343 | throw new ExpiredRequestError('Created lies in the future (with ' + 344 | 'skew ' + skew + 's greater than allowed ' + options.clockSkew + 345 | 's'); 346 | } 347 | } 348 | 349 | if (parsed.params.expires) { 350 | var expiredSince = Math.floor(Date.now() / 1000) - parsed.params.expires; 351 | if (expiredSince > options.clockSkew) { 352 | throw new ExpiredRequestError('Request expired with skew ' + 353 | expiredSince + 's greater than allowed ' + options.clockSkew + 's'); 354 | } 355 | } 356 | 357 | headers.forEach(function (hdr) { 358 | // Remember that we already checked any headers in the params 359 | // were in the request, so if this passes we're good. 360 | if (parsed.params.headers.indexOf(hdr.toLowerCase()) < 0) 361 | throw new MissingHeaderError(hdr + ' was not a signed header'); 362 | }); 363 | 364 | parsed.params.algorithm = parsed.params.algorithm.toLowerCase(); 365 | if (options.algorithms) { 366 | if (options.algorithms.indexOf(parsed.params.algorithm) === -1) 367 | throw new InvalidParamsError(parsed.params.algorithm + 368 | ' is not a supported algorithm'); 369 | } 370 | 371 | parsed.algorithm = parsed.params.algorithm.toUpperCase(); 372 | parsed.keyId = parsed.params.keyId; 373 | parsed.opaque = parsed.params.opaque; 374 | return parsed; 375 | } 376 | 377 | }; 378 | -------------------------------------------------------------------------------- /test/signer.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Joyent, Inc. All rights reserved. 2 | 3 | var crypto = require('crypto'); 4 | var fs = require('fs'); 5 | var http = require('http'); 6 | var sshpk = require('sshpk'); 7 | 8 | var test = require('tap').test; 9 | var uuid = require('uuid').v4; 10 | 11 | var httpSignature = require('../lib/index'); 12 | 13 | 14 | 15 | ///--- Globals 16 | 17 | var hmacKey = null; 18 | var httpOptions = null; 19 | var rsaPrivate = null; 20 | var rsaPrivateEncrypted = null; 21 | var dsaPrivate = null; 22 | var ecdsaPrivate = null; 23 | var ed25519Private = null; 24 | var signOptions = null; 25 | var server = null; 26 | var socket = null; 27 | 28 | 29 | 30 | ///--- Tests 31 | 32 | 33 | test('setup', function(t) { 34 | rsaPrivate = fs.readFileSync(__dirname + '/rsa_private.pem', 'ascii'); 35 | rsaPrivateEncrypted = fs.readFileSync(__dirname + '/rsa_private_encrypted.pem', 'ascii'); 36 | dsaPrivate = fs.readFileSync(__dirname + '/dsa_private.pem', 'ascii'); 37 | ecdsaPrivate = fs.readFileSync(__dirname + '/ecdsa_private.pem', 'ascii'); 38 | 39 | { 40 | const { privateKey } = crypto.generateKeyPairSync('ed25519', { 41 | publicKeyEncoding: { 42 | type: 'spki', 43 | format: 'pem' 44 | }, 45 | privateKeyEncoding: { 46 | type: 'pkcs8', 47 | format: 'pem' 48 | } 49 | }); 50 | 51 | ed25519Private = privateKey; 52 | } 53 | 54 | t.ok(rsaPrivate); 55 | t.ok(rsaPrivateEncrypted); 56 | t.ok(dsaPrivate); 57 | t.ok(ecdsaPrivate); 58 | t.ok(ed25519Private); 59 | 60 | socket = '/tmp/.' + uuid(); 61 | 62 | server = http.createServer(function(req, res) { 63 | res.writeHead(200); 64 | res.end(); 65 | }); 66 | 67 | server.listen(socket, function() { 68 | hmacKey = uuid(); 69 | httpOptions = { 70 | socketPath: socket, 71 | path: '/', 72 | method: 'GET', 73 | headers: {} 74 | }; 75 | 76 | signOptions = { 77 | key: rsaPrivate, 78 | keyId: 'unitTest' 79 | }; 80 | 81 | t.end(); 82 | }); 83 | }); 84 | 85 | 86 | test('defaults', function(t) { 87 | var req = http.request(httpOptions, function(res) { 88 | t.end(); 89 | }); 90 | req._stringToSign = null; 91 | t.ok(httpSignature.sign(req, signOptions)); 92 | var authz = req.getHeader('Authorization'); 93 | t.ok(authz); 94 | 95 | t.equal(typeof (req._stringToSign), 'string'); 96 | t.ok(req._stringToSign.match(/^date: [^\n]*$/)); 97 | 98 | var key = sshpk.parsePrivateKey(rsaPrivate); 99 | var sig = key.createSign().update(req._stringToSign).sign(); 100 | t.ok(authz.indexOf(sig.toString()) !== -1); 101 | 102 | console.log('> ' + authz); 103 | req.end(); 104 | }); 105 | 106 | test('with custom authorizationHeaderName', function(t) { 107 | var req = http.request(httpOptions, function(res) { 108 | t.end(); 109 | }); 110 | req._stringToSign = null; 111 | var opts = Object.create(signOptions); 112 | opts.authorizationHeaderName = 'x-auths'; 113 | t.ok(httpSignature.sign(req, opts)); 114 | var authz = req.getHeader('x-auths'); 115 | t.ok(authz); 116 | 117 | t.equal(typeof (req._stringToSign), 'string'); 118 | t.ok(req._stringToSign.match(/^date: [^\n]*$/)); 119 | 120 | var key = sshpk.parsePrivateKey(rsaPrivate); 121 | var sig = key.createSign().update(req._stringToSign).sign(); 122 | t.ok(authz.indexOf(sig.toString()) !== -1); 123 | 124 | console.log('> ' + authz); 125 | req.end(); 126 | }); 127 | 128 | 129 | test('request line strict unspecified', function(t) { 130 | var req = http.request(httpOptions, function(res) { 131 | t.end(); 132 | }); 133 | var opts = { 134 | keyId: 'unit', 135 | key: rsaPrivate, 136 | headers: ['date', 'request-line'] 137 | }; 138 | 139 | req._stringToSign = null; 140 | t.ok(httpSignature.sign(req, opts)); 141 | t.ok(req.getHeader('Authorization')); 142 | t.equal(typeof (req._stringToSign), 'string'); 143 | t.ok(req._stringToSign.match(/^date: [^\n]*\nGET \/ HTTP\/1.1$/)); 144 | 145 | console.log('> ' + req.getHeader('Authorization')); 146 | req.end(); 147 | }); 148 | 149 | test('request line strict false', function(t) { 150 | var req = http.request(httpOptions, function(res) { 151 | t.end(); 152 | }); 153 | var opts = { 154 | keyId: 'unit', 155 | key: rsaPrivate, 156 | headers: ['date', 'request-line'], 157 | strict: false 158 | }; 159 | 160 | t.ok(httpSignature.sign(req, opts)); 161 | t.ok(req.getHeader('Authorization')); 162 | t.ok(!req.hasOwnProperty('_stringToSign')); 163 | t.ok(req._stringToSign === undefined); 164 | console.log('> ' + req.getHeader('Authorization')); 165 | req.end(); 166 | }); 167 | 168 | test('request line strict true', function(t) { 169 | var req = http.request(httpOptions, function(res) { 170 | t.end(); 171 | }); 172 | var opts = { 173 | keyId: 'unit', 174 | key: rsaPrivate, 175 | headers: ['date', 'request-line'], 176 | strict: true 177 | }; 178 | 179 | t.throws(function() { 180 | httpSignature.sign(req, opts) 181 | }); 182 | req.end(); 183 | }); 184 | 185 | test('request target', function(t) { 186 | var req = http.request(httpOptions, function(res) { 187 | t.end(); 188 | }); 189 | var opts = { 190 | keyId: 'unit', 191 | key: rsaPrivate, 192 | headers: ['date', '(request-target)'] 193 | }; 194 | 195 | req._stringToSign = null; 196 | t.ok(httpSignature.sign(req, opts)); 197 | t.ok(req.getHeader('Authorization')); 198 | t.equal(typeof (req._stringToSign), 'string'); 199 | t.ok(req._stringToSign.match(/^date: [^\n]*\n\(request-target\): get \/$/)); 200 | console.log('> ' + req.getHeader('Authorization')); 201 | req.end(); 202 | }); 203 | 204 | test('keyid', function(t) { 205 | var req = http.request(httpOptions, function(res) { 206 | t.end(); 207 | }); 208 | var opts = { 209 | keyId: 'unit', 210 | key: rsaPrivate, 211 | headers: ['date', '(keyid)'] 212 | }; 213 | 214 | req._stringToSign = null; 215 | t.ok(httpSignature.sign(req, opts)); 216 | t.ok(req.getHeader('Authorization')); 217 | t.equal(typeof (req._stringToSign), 'string'); 218 | t.ok(req._stringToSign.match(/^date: [^\n]*\n\(keyid\): unit$/)); 219 | console.log('> ' + req.getHeader('Authorization')); 220 | req.end(); 221 | }); 222 | 223 | test('signing algorithm', function(t) { 224 | var req = http.request(httpOptions, function(res) { 225 | t.end(); 226 | }); 227 | var opts = { 228 | algorithm: 'rsa-sha256', 229 | keyId: 'unit', 230 | key: rsaPrivate, 231 | headers: ['date', '(algorithm)'] 232 | }; 233 | 234 | req._stringToSign = null; 235 | t.ok(httpSignature.sign(req, opts)); 236 | t.ok(req.getHeader('Authorization')); 237 | t.equal(typeof (opts.algorithm), 'string'); 238 | t.equal(opts.algorithm, 'rsa-sha256'); 239 | t.equal(typeof (req._stringToSign), 'string'); 240 | t.ok(req._stringToSign.match(/^date: [^\n]*\n\(algorithm\): [^\n]*$/)); 241 | console.log('> ' + req.getHeader('Authorization')); 242 | req.end(); 243 | }); 244 | 245 | test('signing with unspecified algorithm', function(t) { 246 | var req = http.request(httpOptions, function(res) { 247 | t.end(); 248 | }); 249 | var opts = { 250 | keyId: 'unit', 251 | key: rsaPrivate, 252 | headers: ['date', '(algorithm)'] 253 | }; 254 | 255 | req._stringToSign = null; 256 | t.ok(httpSignature.sign(req, opts)); 257 | t.ok(req.getHeader('Authorization')); 258 | t.equal(typeof (opts.algorithm), 'string'); 259 | t.equal(typeof (req._stringToSign), 'string'); 260 | t.ok(req._stringToSign.match(/^date: [^\n]*\n\(algorithm\): [^\n]*$/)); 261 | console.log('> ' + req.getHeader('Authorization')); 262 | req.end(); 263 | }); 264 | 265 | test('hide algorithm (unspecified algorithm)', function(t) { 266 | var req = http.request(httpOptions, function(res) { 267 | t.end(); 268 | }); 269 | var opts = { 270 | keyId: 'unit', 271 | key: rsaPrivate, 272 | headers: ['date', '(algorithm)'], 273 | hideAlgorithm: true, 274 | }; 275 | 276 | req._stringToSign = null; 277 | t.ok(httpSignature.sign(req, opts)); 278 | t.ok(req.getHeader('Authorization')); 279 | t.equal(typeof (opts.algorithm), 'string'); 280 | t.equal(opts.algorithm, 'hs2019'); 281 | t.equal(typeof (req._stringToSign), 'string'); 282 | t.ok(req._stringToSign.match(/^date: [^\n]*\n\(algorithm\): [^\n]*$/)); 283 | console.log('> ' + req.getHeader('Authorization')); 284 | req.end(); 285 | }); 286 | 287 | test('signing opaque param', function(t) { 288 | var req = http.request(httpOptions, function(res) { 289 | t.end(); 290 | }); 291 | var opts = { 292 | keyId: 'unit', 293 | key: rsaPrivate, 294 | opaque: 'opaque', 295 | headers: ['date', '(opaque)'] 296 | }; 297 | 298 | req._stringToSign = null; 299 | t.ok(httpSignature.sign(req, opts)); 300 | t.ok(req.getHeader('Authorization')); 301 | t.equal(typeof (opts.algorithm), 'string'); 302 | t.equal(typeof (req._stringToSign), 'string'); 303 | t.ok(req._stringToSign.match(/^date: [^\n]*\n\(opaque\): opaque$/)); 304 | console.log('> ' + req.getHeader('Authorization')); 305 | req.end(); 306 | }); 307 | 308 | test('signing with key protected with passphrase', function(t) { 309 | var req = http.request(httpOptions, function(res) { 310 | t.end(); 311 | }); 312 | var opts = { 313 | keyId: 'unit', 314 | key: rsaPrivateEncrypted, 315 | keyPassphrase: '123', 316 | headers: ['date', '(algorithm)'] 317 | }; 318 | 319 | req._stringToSign = null; 320 | t.ok(httpSignature.sign(req, opts)); 321 | t.ok(req.getHeader('Authorization')); 322 | t.equal(typeof (opts.algorithm), 'string'); 323 | t.equal(typeof (req._stringToSign), 'string'); 324 | t.ok(req._stringToSign.match(/^date: [^\n]*\n\(algorithm\): [^\n]*$/)); 325 | console.log('> ' + req.getHeader('Authorization')); 326 | req.end(); 327 | }); 328 | 329 | test('request-target with dsa key', function(t) { 330 | var req = http.request(httpOptions, function(res) { 331 | t.end(); 332 | }); 333 | var opts = { 334 | keyId: 'unit', 335 | key: dsaPrivate, 336 | headers: ['date', '(request-target)'] 337 | }; 338 | 339 | t.ok(httpSignature.sign(req, opts)); 340 | t.ok(req.getHeader('Authorization')); 341 | console.log('> ' + req.getHeader('Authorization')); 342 | req.end(); 343 | }); 344 | 345 | test('request-target with ecdsa key', function(t) { 346 | var req = http.request(httpOptions, function(res) { 347 | t.end(); 348 | }); 349 | var opts = { 350 | keyId: 'unit', 351 | key: ecdsaPrivate, 352 | headers: ['date', '(request-target)'] 353 | }; 354 | 355 | t.ok(httpSignature.sign(req, opts)); 356 | t.ok(req.getHeader('Authorization')); 357 | console.log('> ' + req.getHeader('Authorization')); 358 | req.end(); 359 | }); 360 | 361 | test('hmac', function(t) { 362 | var req = http.request(httpOptions, function(res) { 363 | t.end(); 364 | }); 365 | var opts = { 366 | keyId: 'unit', 367 | key: uuid(), 368 | algorithm: 'hmac-sha1' 369 | }; 370 | 371 | t.ok(httpSignature.sign(req, opts)); 372 | t.ok(req.getHeader('Authorization')); 373 | console.log('> ' + req.getHeader('Authorization')); 374 | req.end(); 375 | }); 376 | 377 | test('createSigner with RSA key', function(t) { 378 | var s = httpSignature.createSigner({ 379 | keyId: 'foo', 380 | key: rsaPrivate, 381 | algorithm: 'rsa-sha1' 382 | }); 383 | s.writeTarget('get', '/'); 384 | var date = s.writeDateHeader(); 385 | s.sign(function (err, authz) { 386 | t.error(err); 387 | console.log('> ' + authz); 388 | var req = http.request(httpOptions, function(res) { 389 | t.end(); 390 | }); 391 | req.setHeader('date', date); 392 | req.setHeader('authorization', authz); 393 | req.end(); 394 | }); 395 | }); 396 | 397 | test('createSigner with RSA key, auto algo', function(t) { 398 | var s = httpSignature.createSigner({ 399 | keyId: 'foo', 400 | key: rsaPrivate 401 | }); 402 | s.writeTarget('get', '/'); 403 | var date = s.writeDateHeader(); 404 | s.sign(function (err, authz) { 405 | t.error(err); 406 | var req = http.request(httpOptions, function(res) { 407 | t.end(); 408 | }); 409 | req.setHeader('date', date); 410 | req.setHeader('authorization', authz); 411 | req.end(); 412 | }); 413 | }); 414 | 415 | test('createSigner with RSA key, auto algo, passphrase', function(t) { 416 | var s = httpSignature.createSigner({ 417 | keyId: 'foo', 418 | key: rsaPrivateEncrypted, 419 | keyPassphrase: '123' 420 | }); 421 | s.writeTarget('get', '/'); 422 | var date = s.writeDateHeader(); 423 | s.sign(function (err, authz) { 424 | t.error(err); 425 | var req = http.request(httpOptions, function(res) { 426 | t.end(); 427 | }); 428 | req.setHeader('date', date); 429 | req.setHeader('authorization', authz); 430 | req.end(); 431 | }); 432 | }); 433 | 434 | test('createSigner with HMAC key', function(t) { 435 | var s = httpSignature.createSigner({ 436 | keyId: 'foo', 437 | key: hmacKey, 438 | algorithm: 'hmac-sha256' 439 | }); 440 | var date = s.writeDateHeader(); 441 | s.writeTarget('get', '/'); 442 | s.writeHeader('x-some-header', 'bar'); 443 | s.sign(function (err, authz) { 444 | t.error(err); 445 | var req = http.request(httpOptions, function(res) { 446 | t.end(); 447 | }); 448 | req.setHeader('date', date); 449 | req.setHeader('authorization', authz); 450 | req.setHeader('x-some-header', 'bar'); 451 | req.end(); 452 | }); 453 | }); 454 | 455 | test('createSigner with sign function', function(t) { 456 | var date; 457 | var s = httpSignature.createSigner({ 458 | sign: function (data, cb) { 459 | t.ok(typeof (data) === 'string'); 460 | var m = data.match(/^date: (.+)$/); 461 | t.ok(m); 462 | t.equal(m[1], date); 463 | cb(null, { 464 | keyId: 'foo', 465 | algorithm: 'hmac-sha256', 466 | signature: 'fakesig' 467 | }); 468 | } 469 | }); 470 | date = s.writeDateHeader(); 471 | s.sign(function (err, authz) { 472 | t.error(err); 473 | t.ok(authz.match(/fakesig/)); 474 | var req = http.request(httpOptions, function(res) { 475 | t.end(); 476 | }); 477 | req.setHeader('date', date); 478 | req.setHeader('authorization', authz); 479 | req.end(); 480 | }); 481 | }); 482 | 483 | test('ed25519', function(t) { 484 | var req = http.request(httpOptions, function(res) { 485 | t.end(); 486 | }); 487 | var opts = { 488 | keyId: 'unit', 489 | key: ed25519Private, 490 | algorithm: 'ed25519-sha512' 491 | }; 492 | 493 | t.ok(httpSignature.sign(req, opts)); 494 | t.ok(req.getHeader('Authorization')); 495 | console.log('> ' + req.getHeader('Authorization')); 496 | req.end(); 497 | }); 498 | 499 | test('tear down', function(t) { 500 | server.on('close', function() { 501 | t.end(); 502 | }); 503 | server.close(); 504 | }); 505 | -------------------------------------------------------------------------------- /lib/signer.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Joyent, Inc. All rights reserved. 2 | 3 | var assert = require('assert-plus'); 4 | var crypto = require('crypto'); 5 | var util = require('util'); 6 | var sshpk = require('sshpk'); 7 | var jsprim = require('jsprim'); 8 | var utils = require('./utils'); 9 | 10 | var sprintf = require('util').format; 11 | 12 | var HASH_ALGOS = utils.HASH_ALGOS; 13 | var PK_ALGOS = utils.PK_ALGOS; 14 | var InvalidAlgorithmError = utils.InvalidAlgorithmError; 15 | var HttpSignatureError = utils.HttpSignatureError; 16 | var validateAlgorithm = utils.validateAlgorithm; 17 | 18 | ///--- Globals 19 | 20 | var AUTHZ_PARAMS = [ 'keyId', 'algorithm', 'created', 'expires', 'opaque', 21 | 'headers', 'signature' ]; 22 | 23 | ///--- Specific Errors 24 | 25 | function MissingHeaderError(message) { 26 | HttpSignatureError.call(this, message, MissingHeaderError); 27 | } 28 | util.inherits(MissingHeaderError, HttpSignatureError); 29 | 30 | function StrictParsingError(message) { 31 | HttpSignatureError.call(this, message, StrictParsingError); 32 | } 33 | util.inherits(StrictParsingError, HttpSignatureError); 34 | 35 | function FormatAuthz(prefix, params) { 36 | assert.string(prefix, 'prefix'); 37 | assert.object(params, 'params'); 38 | 39 | var authz = ''; 40 | for (var i = 0; i < AUTHZ_PARAMS.length; i++) { 41 | var param = AUTHZ_PARAMS[i]; 42 | var value = params[param]; 43 | if (value === undefined) 44 | continue; 45 | if (typeof (value) === 'number') { 46 | authz += prefix + sprintf('%s=%d', param, value); 47 | } else { 48 | assert.string(value, 'params.' + param); 49 | 50 | authz += prefix + sprintf('%s="%s"', param, value); 51 | } 52 | prefix = ','; 53 | } 54 | 55 | return (authz); 56 | } 57 | 58 | /* See createSigner() */ 59 | function RequestSigner(options) { 60 | assert.object(options, 'options'); 61 | 62 | var alg = []; 63 | if (options.algorithm !== undefined) { 64 | assert.string(options.algorithm, 'options.algorithm'); 65 | alg = validateAlgorithm(options.algorithm); 66 | } 67 | this.rs_alg = alg; 68 | 69 | /* 70 | * RequestSigners come in two varieties: ones with an rs_signFunc, and ones 71 | * with an rs_signer. 72 | * 73 | * rs_signFunc-based RequestSigners have to build up their entire signing 74 | * string within the rs_lines array and give it to rs_signFunc as a single 75 | * concat'd blob. rs_signer-based RequestSigners can add a line at a time to 76 | * their signing state by using rs_signer.update(), thus only needing to 77 | * buffer the hash function state and one line at a time. 78 | */ 79 | if (options.sign !== undefined) { 80 | assert.func(options.sign, 'options.sign'); 81 | this.rs_signFunc = options.sign; 82 | 83 | } else if (alg[0] === 'hmac' && options.key !== undefined) { 84 | assert.string(options.keyId, 'options.keyId'); 85 | this.rs_keyId = options.keyId; 86 | 87 | if (typeof (options.key) !== 'string' && !Buffer.isBuffer(options.key)) 88 | throw (new TypeError('options.key for HMAC must be a string or Buffer')); 89 | 90 | /* 91 | * Make an rs_signer for HMACs, not a rs_signFunc -- HMACs digest their 92 | * data in chunks rather than requiring it all to be given in one go 93 | * at the end, so they are more similar to signers than signFuncs. 94 | */ 95 | this.rs_signer = crypto.createHmac(alg[1].toUpperCase(), options.key); 96 | this.rs_signer.sign = function () { 97 | var digest = this.digest('base64'); 98 | return ({ 99 | hashAlgorithm: alg[1], 100 | toString: function () { return (digest); } 101 | }); 102 | }; 103 | 104 | } else if (options.key !== undefined) { 105 | var key = options.key; 106 | if (typeof (key) === 'string' || Buffer.isBuffer(key)) 107 | assert.optionalString(options.keyPassphrase, 'options.keyPassphrase'); 108 | key = sshpk.parsePrivateKey(key, 'auto', { 109 | passphrase: options.keyPassphrase 110 | }); 111 | 112 | assert.ok(sshpk.PrivateKey.isPrivateKey(key, [1, 2]), 113 | 'options.key must be a sshpk.PrivateKey'); 114 | this.rs_key = key; 115 | 116 | assert.string(options.keyId, 'options.keyId'); 117 | this.rs_keyId = options.keyId; 118 | 119 | if (!PK_ALGOS[key.type]) { 120 | throw (new InvalidAlgorithmError(key.type.toUpperCase() + ' type ' + 121 | 'keys are not supported')); 122 | } 123 | 124 | if (alg[0] !== undefined && key.type !== alg[0]) { 125 | throw (new InvalidAlgorithmError('options.key must be a ' + 126 | alg[0].toUpperCase() + ' key, was given a ' + 127 | key.type.toUpperCase() + ' key instead')); 128 | } 129 | 130 | this.rs_signer = key.createSign(alg[1]); 131 | 132 | } else { 133 | throw (new TypeError('options.sign (func) or options.key is required')); 134 | } 135 | 136 | this.rs_headers = []; 137 | this.rs_lines = []; 138 | } 139 | 140 | /** 141 | * Adds a header to be signed, with its value, into this signer. 142 | * 143 | * @param {String} header 144 | * @param {String} value 145 | * @return {String} value written 146 | */ 147 | RequestSigner.prototype.writeHeader = function (header, value) { 148 | assert.string(header, 'header'); 149 | header = header.toLowerCase(); 150 | assert.string(value, 'value'); 151 | 152 | this.rs_headers.push(header); 153 | 154 | if (this.rs_signFunc) { 155 | this.rs_lines.push(header + ': ' + value); 156 | 157 | } else { 158 | var line = header + ': ' + value; 159 | if (this.rs_headers.length > 0) 160 | line = '\n' + line; 161 | this.rs_signer.update(line); 162 | } 163 | 164 | return (value); 165 | }; 166 | 167 | /** 168 | * Adds a default Date header, returning its value. 169 | * 170 | * @return {String} 171 | */ 172 | RequestSigner.prototype.writeDateHeader = function () { 173 | return (this.writeHeader('date', jsprim.rfc1123(new Date()))); 174 | }; 175 | 176 | /** 177 | * Adds the request target line to be signed. 178 | * 179 | * @param {String} method, HTTP method (e.g. 'get', 'post', 'put') 180 | * @param {String} path 181 | */ 182 | RequestSigner.prototype.writeTarget = function (method, path) { 183 | assert.string(method, 'method'); 184 | assert.string(path, 'path'); 185 | method = method.toLowerCase(); 186 | this.writeHeader('(request-target)', method + ' ' + path); 187 | }; 188 | 189 | /** 190 | * Calculate the value for the Authorization header on this request 191 | * asynchronously. 192 | * 193 | * @param {Func} callback (err, authz) 194 | */ 195 | RequestSigner.prototype.sign = function (cb) { 196 | assert.func(cb, 'callback'); 197 | 198 | if (this.rs_headers.length < 1) 199 | throw (new Error('At least one header must be signed')); 200 | 201 | var alg, authz; 202 | if (this.rs_signFunc) { 203 | var data = this.rs_lines.join('\n'); 204 | var self = this; 205 | this.rs_signFunc(data, function (err, sig) { 206 | if (err) { 207 | cb(err); 208 | return; 209 | } 210 | try { 211 | assert.object(sig, 'signature'); 212 | assert.string(sig.keyId, 'signature.keyId'); 213 | assert.string(sig.algorithm, 'signature.algorithm'); 214 | assert.string(sig.signature, 'signature.signature'); 215 | alg = validateAlgorithm(sig.algorithm); 216 | 217 | authz = FormatAuthz('Signature ', { 218 | keyId: sig.keyId, 219 | algorithm: sig.algorithm, 220 | headers: self.rs_headers.join(' '), 221 | signature: sig.signature 222 | }); 223 | } catch (e) { 224 | cb(e); 225 | return; 226 | } 227 | cb(null, authz); 228 | }); 229 | 230 | } else { 231 | try { 232 | var sigObj = this.rs_signer.sign(); 233 | } catch (e) { 234 | cb(e); 235 | return; 236 | } 237 | alg = sigObj.hideAlgorithm ? 238 | 'hs2019' : 239 | (this.rs_alg[0] || this.rs_key.type) + '-' + sigObj.hashAlgorithm; 240 | var signature = sigObj.toString(); 241 | authz = FormatAuthz('Signature ', { 242 | keyId: this.rs_keyId, 243 | algorithm: alg, 244 | headers: this.rs_headers.join(' '), 245 | signature: signature 246 | }); 247 | cb(null, authz); 248 | } 249 | }; 250 | 251 | ///--- Exported API 252 | 253 | module.exports = { 254 | /** 255 | * Identifies whether a given object is a request signer or not. 256 | * 257 | * @param {Object} object, the object to identify 258 | * @returns {Boolean} 259 | */ 260 | isSigner: function (obj) { 261 | if (typeof (obj) === 'object' && obj instanceof RequestSigner) 262 | return (true); 263 | return (false); 264 | }, 265 | 266 | /** 267 | * Creates a request signer, used to asynchronously build a signature 268 | * for a request (does not have to be an http.ClientRequest). 269 | * 270 | * @param {Object} options, either: 271 | * - {String} keyId 272 | * - {String|Buffer} key 273 | * - {String} algorithm (optional, required for HMAC) 274 | * - {String} keyPassphrase (optional, not for HMAC) 275 | * or: 276 | * - {Func} sign (data, cb) 277 | * @return {RequestSigner} 278 | */ 279 | createSigner: function createSigner(options) { 280 | return (new RequestSigner(options)); 281 | }, 282 | 283 | /** 284 | * Adds an 'Authorization' header to an http.ClientRequest object. 285 | * 286 | * Note that this API will add a Date header if it's not already set. Any 287 | * other headers in the options.headers array MUST be present, or this 288 | * will throw. 289 | * 290 | * You shouldn't need to check the return type; it's just there if you want 291 | * to be pedantic. 292 | * 293 | * The optional flag indicates whether parsing should use strict enforcement 294 | * of the version draft-cavage-http-signatures-04 of the spec or beyond. 295 | * The default is to be loose and support 296 | * older versions for compatibility. 297 | * 298 | * @param {Object} request an instance of http.ClientRequest. 299 | * @param {Object} options signing parameters object: 300 | * - {String} keyId required. 301 | * - {String} key required (either a PEM or HMAC key). 302 | * - {Array} headers optional; defaults to ['date']. 303 | * - {String} algorithm optional (unless key is HMAC); 304 | * default is the same as the sshpk default 305 | * signing algorithm for the type of key given 306 | * - {String} httpVersion optional; defaults to '1.1'. 307 | * - {Boolean} strict optional; defaults to 'false'. 308 | * - {int} expiresIn optional; defaults to 60. The 309 | * seconds after which the signature should 310 | * expire; 311 | * - {String} keyPassphrase optional; The passphrase to 312 | * pass to sshpk to parse the privateKey. 313 | * This doesn't do anything if algorithm is 314 | * HMAC. 315 | * - {Boolean} hideAlgorithm optional; defaults to 'false'. 316 | * if true, hides algorithm by writing "hs2019" 317 | * to signature. 318 | * @return {Boolean} true if Authorization (and optionally Date) were added. 319 | * @throws {TypeError} on bad parameter types (input). 320 | * @throws {InvalidAlgorithmError} if algorithm was bad or incompatible with 321 | * the given key. 322 | * @throws {sshpk.KeyParseError} if key was bad. 323 | * @throws {MissingHeaderError} if a header to be signed was specified but 324 | * was not present. 325 | */ 326 | signRequest: function signRequest(request, options) { 327 | assert.object(request, 'request'); 328 | assert.object(options, 'options'); 329 | assert.optionalString(options.algorithm, 'options.algorithm'); 330 | assert.string(options.keyId, 'options.keyId'); 331 | assert.optionalString(options.opaque, 'options.opaque'); 332 | assert.optionalArrayOfString(options.headers, 'options.headers'); 333 | assert.optionalString(options.httpVersion, 'options.httpVersion'); 334 | assert.optionalNumber(options.expiresIn, 'options.expiresIn'); 335 | assert.optionalString(options.keyPassphrase, 'options.keyPassphrase'); 336 | assert.optionalBool(options.hideAlgorithm, 'options.hideAlgorithm'); 337 | 338 | if (!request.getHeader('Date')) 339 | request.setHeader('Date', jsprim.rfc1123(new Date())); 340 | var headers = ['date']; 341 | if (options.headers) 342 | headers = options.headers; 343 | if (!options.httpVersion) 344 | options.httpVersion = '1.1'; 345 | 346 | var alg = []; 347 | if (options.algorithm) { 348 | options.algorithm = options.algorithm.toLowerCase(); 349 | alg = validateAlgorithm(options.algorithm); 350 | } 351 | 352 | var key = options.key; 353 | if (alg[0] === 'hmac') { 354 | if (typeof (key) !== 'string' && !Buffer.isBuffer(key)) 355 | throw (new TypeError('options.key must be a string or Buffer')); 356 | } else { 357 | if (typeof (key) === 'string' || Buffer.isBuffer(key)) 358 | key = sshpk.parsePrivateKey(options.key, 'auto', { 359 | passphrase: options.keyPassphrase 360 | }); 361 | 362 | assert.ok(sshpk.PrivateKey.isPrivateKey(key, [1, 2]), 363 | 'options.key must be a sshpk.PrivateKey'); 364 | 365 | if (!PK_ALGOS[key.type]) { 366 | throw (new InvalidAlgorithmError(key.type.toUpperCase() + ' type ' + 367 | 'keys are not supported')); 368 | } 369 | 370 | if (alg[0] === undefined) { 371 | alg[0] = key.type; 372 | } else if (key.type !== alg[0]) { 373 | throw (new InvalidAlgorithmError('options.key must be a ' + 374 | alg[0].toUpperCase() + ' key, was given a ' + 375 | key.type.toUpperCase() + ' key instead')); 376 | } 377 | if (alg[1] === undefined) { 378 | alg[1] = key.defaultHashAlgorithm(); 379 | } 380 | 381 | options.algorithm = options.hideAlgorithm ? 382 | 'hs2019' : 383 | alg[0] + '-' + alg[1]; 384 | } 385 | 386 | var params = { 387 | 'keyId': options.keyId, 388 | 'algorithm': options.algorithm 389 | }; 390 | 391 | var i; 392 | var stringToSign = ''; 393 | for (i = 0; i < headers.length; i++) { 394 | if (typeof (headers[i]) !== 'string') 395 | throw new TypeError('options.headers must be an array of Strings'); 396 | 397 | var h = headers[i].toLowerCase(); 398 | 399 | if (h === 'request-line') { 400 | if (!options.strict) { 401 | /** 402 | * We allow headers from the older spec drafts if strict parsing isn't 403 | * specified in options. 404 | */ 405 | stringToSign += 406 | request.method + ' ' + request.path + ' HTTP/' + 407 | options.httpVersion; 408 | } else { 409 | /* Strict parsing doesn't allow older draft headers. */ 410 | throw (new StrictParsingError('request-line is not a valid header ' + 411 | 'with strict parsing enabled.')); 412 | } 413 | } else if (h === '(request-target)') { 414 | stringToSign += 415 | '(request-target): ' + request.method.toLowerCase() + ' ' + 416 | request.path; 417 | } else if (h === '(keyid)') { 418 | stringToSign += '(keyid): ' + options.keyId; 419 | } else if (h === '(algorithm)') { 420 | stringToSign += '(algorithm): ' + options.algorithm; 421 | } else if (h === '(opaque)') { 422 | var opaque = options.opaque; 423 | if (opaque == undefined || opaque === '') { 424 | throw new MissingHeaderError('options.opaque was not in the request'); 425 | } 426 | stringToSign += '(opaque): ' + opaque; 427 | } else if (h === '(created)') { 428 | var created = Math.floor(Date.now() / 1000); 429 | params.created = created; 430 | stringToSign += '(created): ' + created; 431 | } else if (h === '(expires)') { 432 | var expiresIn = options.expiresIn; 433 | if (expiresIn === undefined) { 434 | expiresIn = 60; 435 | } 436 | const expires = Math.floor(Date.now() / 1000) + expiresIn; 437 | params.expires = expires; 438 | stringToSign += '(expires): ' + expires; 439 | } else { 440 | var value = request.getHeader(h); 441 | if (value === undefined || value === '') { 442 | throw new MissingHeaderError(h + ' was not in the request'); 443 | } 444 | stringToSign += h + ': ' + value; 445 | } 446 | 447 | if ((i + 1) < headers.length) 448 | stringToSign += '\n'; 449 | } 450 | 451 | /* This is just for unit tests. */ 452 | if (request.hasOwnProperty('_stringToSign')) { 453 | request._stringToSign = stringToSign; 454 | } 455 | 456 | var signature; 457 | if (alg[0] === 'hmac') { 458 | var hmac = crypto.createHmac(alg[1].toUpperCase(), key); 459 | hmac.update(stringToSign); 460 | signature = hmac.digest('base64'); 461 | } else { 462 | var signer = key.createSign(alg[1]); 463 | signer.update(stringToSign); 464 | var sigObj = signer.sign(); 465 | if (!HASH_ALGOS[sigObj.hashAlgorithm]) { 466 | throw (new InvalidAlgorithmError(sigObj.hashAlgorithm.toUpperCase() + 467 | ' is not a supported hash algorithm')); 468 | } 469 | assert.strictEqual(alg[1], sigObj.hashAlgorithm, 470 | 'hash algorithm mismatch'); 471 | signature = sigObj.toString(); 472 | assert.notStrictEqual(signature, '', 'empty signature produced'); 473 | } 474 | 475 | var authzHeaderName = options.authorizationHeaderName || 'Authorization'; 476 | var prefix = authzHeaderName.toLowerCase() === utils.HEADER.SIG ? 477 | '' : 'Signature '; 478 | 479 | params.signature = signature; 480 | 481 | if (options.opaque) 482 | params.opaque = options.opaque; 483 | if (options.headers) 484 | params.headers = options.headers.join(' '); 485 | 486 | request.setHeader(authzHeaderName, FormatAuthz(prefix, params)); 487 | 488 | return true; 489 | } 490 | 491 | }; 492 | -------------------------------------------------------------------------------- /test/parser.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Joyent, Inc. All rights reserved. 2 | 3 | var http = require('http'); 4 | 5 | var test = require('tap').test; 6 | var uuid = require('uuid').v4; 7 | var jsprim = require('jsprim'); 8 | 9 | var httpSignature = require('../lib/index'); 10 | 11 | 12 | 13 | ///--- Globals 14 | 15 | var options = null; 16 | var server = null; 17 | var socket = null; 18 | 19 | 20 | ///--- Tests 21 | 22 | test('setup', function(t) { 23 | socket = '/tmp/.' + uuid(); 24 | options = { 25 | socketPath: socket, 26 | path: '/', 27 | headers: {} 28 | }; 29 | 30 | server = http.createServer(function(req, res) { 31 | server.tester(req, res); 32 | }); 33 | 34 | server.listen(socket, function() { 35 | t.end(); 36 | }); 37 | }); 38 | 39 | 40 | test('no authorization', function(t) { 41 | server.tester = function(req, res) { 42 | try { 43 | httpSignature.parseRequest(req); 44 | } catch (e) { 45 | t.equal(e.name, 'MissingHeaderError'); 46 | } 47 | res.writeHead(200); 48 | res.end(); 49 | }; 50 | 51 | http.get(options, function(res) { 52 | t.equal(res.statusCode, 200); 53 | t.end(); 54 | }); 55 | }); 56 | 57 | 58 | test('bad scheme', function(t) { 59 | server.tester = function(req, res) { 60 | try { 61 | httpSignature.parseRequest(req); 62 | } catch (e) { 63 | t.equal(e.name, 'InvalidHeaderError'); 64 | t.equal(e.message, 'scheme was not "Signature"'); 65 | } 66 | 67 | res.writeHead(200); 68 | res.end(); 69 | }; 70 | 71 | options.headers.Authorization = 'Basic blahBlahBlah'; 72 | http.get(options, function(res) { 73 | t.equal(res.statusCode, 200); 74 | t.end(); 75 | }); 76 | }); 77 | 78 | 79 | test('no key id', function(t) { 80 | server.tester = function(req, res) { 81 | try { 82 | httpSignature.parseRequest(req); 83 | } catch (e) { 84 | t.equal(e.name, 'InvalidHeaderError'); 85 | t.equal(e.message, 'keyId was not specified'); 86 | } 87 | 88 | res.writeHead(200); 89 | res.end(); 90 | }; 91 | 92 | options.headers.Authorization = 'Signature foo'; 93 | http.get(options, function(res) { 94 | t.equal(res.statusCode, 200); 95 | t.end(); 96 | }); 97 | }); 98 | 99 | 100 | test('key id no value', function(t) { 101 | server.tester = function(req, res) { 102 | try { 103 | httpSignature.parseRequest(req); 104 | } catch (e) { 105 | t.equal(e.name, 'InvalidHeaderError'); 106 | t.equal(e.message, 'keyId was not specified'); 107 | } 108 | 109 | res.writeHead(200); 110 | res.end(); 111 | }; 112 | 113 | options.headers.Authorization = 'Signature keyId='; 114 | http.get(options, function(res) { 115 | t.equal(res.statusCode, 200); 116 | t.end(); 117 | }); 118 | }); 119 | 120 | 121 | test('key id no quotes', function(t) { 122 | server.tester = function(req, res) { 123 | try { 124 | httpSignature.parseRequest(req); 125 | } catch (e) { 126 | t.equal(e.name, 'InvalidHeaderError'); 127 | t.equal(e.message, 'bad param format'); 128 | } 129 | 130 | res.writeHead(200); 131 | res.end(); 132 | }; 133 | 134 | options.headers.Authorization = 135 | 'Signature keyId=foo,algorithm=hmac-sha1,signature=aabbcc'; 136 | http.get(options, function(res) { 137 | t.equal(res.statusCode, 200); 138 | t.end(); 139 | }); 140 | }); 141 | 142 | 143 | test('key id param quotes', function(t) { 144 | server.tester = function(req, res) { 145 | try { 146 | httpSignature.parseRequest(req); 147 | } catch (e) { 148 | t.equal(e.name, 'InvalidHeaderError'); 149 | t.equal(e.message, 'bad param format'); 150 | } 151 | 152 | res.writeHead(200); 153 | res.end(); 154 | }; 155 | 156 | options.headers.Authorization = 'Signature "keyId"="key"'; 157 | http.get(options, function(res) { 158 | t.equal(res.statusCode, 200); 159 | t.end(); 160 | }); 161 | }); 162 | 163 | 164 | test('param name with space', function(t) { 165 | server.tester = function(req, res) { 166 | try { 167 | httpSignature.parseRequest(req); 168 | } catch (e) { 169 | t.equal(e.name, 'InvalidHeaderError'); 170 | t.equal(e.message, 'bad param format'); 171 | } 172 | 173 | res.writeHead(200); 174 | res.end(); 175 | }; 176 | 177 | options.headers.Authorization = 'Signature key Id="key"'; 178 | http.get(options, function(res) { 179 | t.equal(res.statusCode, 200); 180 | t.end(); 181 | }); 182 | }); 183 | 184 | 185 | test('no algorithm', function(t) { 186 | server.tester = function(req, res) { 187 | try { 188 | httpSignature.parseRequest(req); 189 | } catch (e) { 190 | t.equal(e.name, 'InvalidHeaderError'); 191 | t.equal(e.message, 'algorithm was not specified'); 192 | } 193 | 194 | res.writeHead(200); 195 | res.end(); 196 | }; 197 | 198 | options.headers.Authorization = 'Signature keyId="foo"'; 199 | http.get(options, function(res) { 200 | t.equal(res.statusCode, 200); 201 | t.end(); 202 | }); 203 | }); 204 | 205 | 206 | test('algorithm no value', function(t) { 207 | server.tester = function(req, res) { 208 | try { 209 | httpSignature.parseRequest(req); 210 | } catch (e) { 211 | t.equal(e.name, 'InvalidHeaderError'); 212 | t.equal(e.message, 'algorithm was not specified'); 213 | } 214 | 215 | res.writeHead(200); 216 | res.end(); 217 | }; 218 | 219 | options.headers.Authorization = 'Signature keyId="foo",algorithm='; 220 | http.get(options, function(res) { 221 | t.equal(res.statusCode, 200); 222 | t.end(); 223 | }); 224 | }); 225 | 226 | 227 | test('no signature', function(t) { 228 | server.tester = function(req, res) { 229 | try { 230 | httpSignature.parseRequest(req); 231 | } catch (e) { 232 | t.equal(e.name, 'InvalidHeaderError'); 233 | t.equal(e.message, 'signature was not specified'); 234 | } 235 | 236 | res.writeHead(200); 237 | res.end(); 238 | }; 239 | 240 | options.headers.Authorization = 'Signature keyId="foo",algorithm="foo"'; 241 | http.get(options, function(res) { 242 | t.equal(res.statusCode, 200); 243 | t.end(); 244 | }); 245 | }); 246 | 247 | 248 | test('invalid algorithm', function(t) { 249 | server.tester = function(req, res) { 250 | try { 251 | httpSignature.parseRequest(req); 252 | } catch (e) { 253 | t.equal(e.name, 'InvalidParamsError'); 254 | t.equal(e.message, 'foo is not supported'); 255 | } 256 | 257 | res.writeHead(200); 258 | res.end(); 259 | }; 260 | 261 | options.headers.Authorization = 262 | 'Signature keyId="foo",algorithm="foo",signature="aaabbbbcccc"'; 263 | http.get(options, function(res) { 264 | t.equal(res.statusCode, 200); 265 | t.end(); 266 | }); 267 | }); 268 | 269 | 270 | test('no date header', function(t) { 271 | server.tester = function(req, res) { 272 | try { 273 | httpSignature.parseRequest(req); 274 | } catch (e) { 275 | t.equal(e.name, 'MissingHeaderError'); 276 | t.equal(e.message, 'date was not in the request'); 277 | } 278 | 279 | res.writeHead(200); 280 | res.end(); 281 | }; 282 | 283 | options.headers.Authorization = 284 | 'Signature keyId="foo",algorithm="rsa-sha256",signature="aaabbbbcccc"'; 285 | http.get(options, function(res) { 286 | t.equal(res.statusCode, 200); 287 | t.end(); 288 | }); 289 | }); 290 | 291 | test('valid numeric parameter', function(t) { 292 | server.tester = function(req, res) { 293 | var options = { 294 | headers: ['(created)', 'digest'] 295 | }; 296 | 297 | try { 298 | httpSignature.parseRequest(req, options); 299 | } catch (e) { 300 | t.fail(e.stack); 301 | } 302 | 303 | res.writeHead(200); 304 | res.end(); 305 | }; 306 | 307 | options.headers.Authorization = 308 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 309 | 'created=123456,' + 310 | 'headers="(created) dIgEsT",signature="digitalSignature"'; 311 | options.headers['digest'] = uuid(); 312 | http.get(options, function(res) { 313 | t.equal(res.statusCode, 200); 314 | t.end(); 315 | }); 316 | }); 317 | 318 | test('invalid numeric parameter', function(t) { 319 | server.tester = function(req, res) { 320 | var options = { 321 | headers: ['(created)', 'digest'] 322 | }; 323 | 324 | try { 325 | httpSignature.parseRequest(req, options); 326 | } catch (e) { 327 | t.equal(e.name, 'InvalidHeaderError'); 328 | t.equal(e.message, 'bad param format'); 329 | res.writeHead(200); 330 | res.end(); 331 | return; 332 | } 333 | 334 | t.fail("should throw error"); 335 | res.writeHead(200); 336 | res.end(); 337 | }; 338 | 339 | options.headers.Authorization = 340 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 341 | 'created=123@456,' + 342 | 'headers="(created) dIgEsT",signature="digitalSignature"'; 343 | options.headers['digest'] = uuid(); 344 | http.get(options, function(res) { 345 | t.equal(res.statusCode, 200); 346 | t.end(); 347 | }); 348 | }); 349 | 350 | test('invalid numeric parameter - decimal', function(t) { 351 | server.tester = function(req, res) { 352 | var options = { 353 | headers: ['(created)', 'digest'] 354 | }; 355 | 356 | try { 357 | httpSignature.parseRequest(req, options); 358 | } catch (e) { 359 | t.equal(e.name, 'InvalidHeaderError'); 360 | t.equal(e.message, 'bad param format'); 361 | res.writeHead(200); 362 | res.end(); 363 | return; 364 | } 365 | 366 | t.fail("should throw error"); 367 | res.writeHead(200); 368 | res.end(); 369 | }; 370 | 371 | options.headers.Authorization = 372 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 373 | 'created=123.456,' + 374 | 'headers="(created) dIgEsT",signature="digitalSignature"'; 375 | options.headers['digest'] = uuid(); 376 | http.get(options, function(res) { 377 | t.equal(res.statusCode, 200); 378 | t.end(); 379 | }); 380 | }); 381 | 382 | test('invalid numeric parameter - signed integer', function(t) { 383 | server.tester = function(req, res) { 384 | var options = { 385 | headers: ['(created)', 'digest'] 386 | }; 387 | 388 | try { 389 | httpSignature.parseRequest(req, options); 390 | } catch (e) { 391 | t.equal(e.name, 'InvalidHeaderError'); 392 | t.equal(e.message, 'bad param format'); 393 | res.writeHead(200); 394 | res.end(); 395 | return; 396 | } 397 | 398 | t.fail("should throw error"); 399 | res.writeHead(200); 400 | res.end(); 401 | }; 402 | 403 | options.headers.Authorization = 404 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 405 | 'created=-123456,' + 406 | 'headers="(created) dIgEsT",signature="digitalSignature"'; 407 | options.headers['digest'] = uuid(); 408 | http.get(options, function(res) { 409 | t.equal(res.statusCode, 200); 410 | t.end(); 411 | }); 412 | }); 413 | 414 | test('created in future', function(t) { 415 | var skew = 1000; 416 | server.tester = function(req, res) { 417 | var options = { 418 | headers: ['(created)', 'digest'], 419 | clockSkew: skew 420 | }; 421 | 422 | try { 423 | httpSignature.parseRequest(req, options); 424 | } catch (e) { 425 | t.equal(e.name, 'ExpiredRequestError'); 426 | t.match(e.message, new RegExp('Created lies in the future.*')); 427 | res.writeHead(200); 428 | res.end(); 429 | return; 430 | } 431 | 432 | t.fail("should throw error"); 433 | res.writeHead(200); 434 | res.end(); 435 | }; 436 | 437 | var created = Math.floor(Date.now() / 1000) + skew + 10; 438 | options.headers.Authorization = 439 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 440 | 'created=' + created + ',' + 441 | 'headers="(created) dIgEsT",signature="digitalSignature"'; 442 | options.headers['digest'] = uuid(); 443 | http.get(options, function(res) { 444 | t.equal(res.statusCode, 200); 445 | t.end(); 446 | }); 447 | }); 448 | 449 | test('expires expired', function(t) { 450 | var skew = 1000; 451 | server.tester = function(req, res) { 452 | var options = { 453 | headers: ['(expires)', 'digest'], 454 | clockSkew: skew 455 | }; 456 | 457 | try { 458 | httpSignature.parseRequest(req, options); 459 | } catch (e) { 460 | t.equal(e.name, 'ExpiredRequestError'); 461 | t.match(e.message, new RegExp('Request expired.*')); 462 | res.writeHead(200); 463 | res.end(); 464 | return; 465 | } 466 | 467 | t.fail("should throw error"); 468 | res.writeHead(200); 469 | res.end(); 470 | }; 471 | 472 | var expires = Math.floor(Date.now() / 1000) - skew - 1; 473 | options.headers.Authorization = 474 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 475 | 'expires=' + expires + ',' + 476 | 'headers="(expires) dIgEsT",signature="digitalSignature"'; 477 | options.headers['digest'] = uuid(); 478 | http.get(options, function(res) { 479 | t.equal(res.statusCode, 200); 480 | t.end(); 481 | }); 482 | }); 483 | 484 | test('valid created and expires with skew', function(t) { 485 | var skew = 1000; 486 | server.tester = function(req, res) { 487 | var options = { 488 | headers: ['(created)', '(expires)', 'digest'], 489 | clockSkew: skew 490 | }; 491 | 492 | try { 493 | httpSignature.parseRequest(req, options); 494 | } catch (e) { 495 | t.fail(e.stack); 496 | } 497 | 498 | res.writeHead(200); 499 | res.end(); 500 | }; 501 | 502 | //created is in the future but within allowed skew 503 | var created = Math.floor(Date.now() / 1000) + skew - 1; 504 | //expires is in the past but within allowed skew 505 | var expires = Math.floor(Date.now() / 1000) - skew + 10; 506 | options.headers.Authorization = 507 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 508 | 'created=' + created + ',' + 'expires=' + expires + ',' + 509 | 'headers="(created) (expires) dIgEsT",signature="digitalSignature"'; 510 | options.headers['digest'] = uuid(); 511 | http.get(options, function(res) { 512 | t.equal(res.statusCode, 200); 513 | t.end(); 514 | }); 515 | }); 516 | 517 | 518 | 519 | test('valid default headers', function(t) { 520 | server.tester = function(req, res) { 521 | try { 522 | httpSignature.parseRequest(req); 523 | } catch (e) { 524 | t.fail(e.stack); 525 | } 526 | 527 | res.writeHead(200); 528 | res.end(); 529 | }; 530 | 531 | options.headers.Authorization = 532 | 'Signature keyId="foo",algorithm="rsa-sha256",signature="aaabbbbcccc"'; 533 | options.headers.Date = jsprim.rfc1123(new Date()); 534 | http.get(options, function(res) { 535 | t.equal(res.statusCode, 200); 536 | t.end(); 537 | }); 538 | }); 539 | 540 | 541 | test('valid custom authorizationHeaderName', function(t) { 542 | server.tester = function(req, res) { 543 | try { 544 | httpSignature.parseRequest(req, { authorizationHeaderName: 'x-auth' }); 545 | } catch (e) { 546 | t.fail(e.stack); 547 | } 548 | 549 | res.writeHead(200); 550 | res.end(); 551 | }; 552 | 553 | options.headers['x-auth'] = 554 | 'Signature keyId="foo",algorithm="rsa-sha256",signature="aaabbbbcccc"'; 555 | options.headers.Date = jsprim.rfc1123(new Date()); 556 | http.get(options, function(res) { 557 | t.equal(res.statusCode, 200); 558 | t.end(); 559 | }); 560 | }); 561 | 562 | 563 | test('explicit headers missing', function(t) { 564 | server.tester = function(req, res) { 565 | try { 566 | httpSignature.parseRequest(req); 567 | } catch (e) { 568 | t.equal(e.name, 'MissingHeaderError'); 569 | t.equal(e.message, 'digest was not in the request'); 570 | } 571 | 572 | res.writeHead(200); 573 | res.end(); 574 | }; 575 | 576 | options.headers.Authorization = 577 | 'Signature keyId="foo",algorithm="rsa-sha256",' + 578 | 'headers="date digest",signature="aaabbbbcccc"'; 579 | options.headers.Date = jsprim.rfc1123(new Date()); 580 | http.get(options, function(res) { 581 | t.equal(res.statusCode, 200); 582 | t.end(); 583 | }); 584 | }); 585 | 586 | 587 | test('valid explicit headers request-line', function(t) { 588 | server.tester = function(req, res) { 589 | var parsed = httpSignature.parseRequest(req); 590 | res.writeHead(200); 591 | res.write(JSON.stringify(parsed, null, 2)); 592 | res.end(); 593 | }; 594 | 595 | 596 | options.headers.Authorization = 597 | 'Signature keyId="fo,o",algorithm="RSA-sha256",' + 598 | 'headers="dAtE dIgEsT request-line",' + 599 | 'extensions="blah blah",signature="digitalSignature"'; 600 | options.headers.Date = jsprim.rfc1123(new Date()); 601 | options.headers['digest'] = uuid(); 602 | 603 | http.get(options, function(res) { 604 | t.equal(res.statusCode, 200); 605 | 606 | var body = ''; 607 | res.setEncoding('utf8'); 608 | res.on('data', function(chunk) { 609 | body += chunk; 610 | }); 611 | 612 | res.on('end', function() { 613 | console.log(body); 614 | var parsed = JSON.parse(body); 615 | t.ok(parsed); 616 | t.equal(parsed.scheme, 'Signature'); 617 | t.ok(parsed.params); 618 | t.equal(parsed.params.keyId, 'fo,o'); 619 | t.equal(parsed.params.algorithm, 'rsa-sha256'); 620 | t.equal(parsed.params.extensions, 'blah blah'); 621 | t.ok(parsed.params.headers); 622 | t.equal(parsed.params.headers.length, 3); 623 | t.equal(parsed.params.headers[0], 'date'); 624 | t.equal(parsed.params.headers[1], 'digest'); 625 | t.equal(parsed.params.headers[2], 'request-line'); 626 | t.equal(parsed.params.signature, 'digitalSignature'); 627 | t.ok(parsed.signingString); 628 | t.equal(parsed.signingString, 629 | ('date: ' + options.headers.Date + '\n' + 630 | 'digest: ' + options.headers['digest'] + '\n' + 631 | 'GET / HTTP/1.1')); 632 | t.equal(parsed.params.keyId, parsed.keyId); 633 | t.equal(parsed.params.algorithm.toUpperCase(), 634 | parsed.algorithm); 635 | t.end(); 636 | }); 637 | }); 638 | }); 639 | 640 | test('valid explicit headers request-line strict true', function(t) { 641 | server.tester = function(req, res) { 642 | 643 | try { 644 | httpSignature.parseRequest(req, {strict: true}); 645 | } catch (e) { 646 | t.equal(e.name, 'StrictParsingError'); 647 | t.equal(e.message, 'request-line is not a valid header with strict parsing enabled.'); 648 | } 649 | 650 | res.writeHead(200); 651 | res.end(); 652 | }; 653 | 654 | 655 | options.headers.Authorization = 656 | 'Signature keyId="fo,o",algorithm="RSA-sha256",' + 657 | 'headers="dAtE dIgEsT request-line",' + 658 | 'extensions="blah blah",signature="digitalSignature"'; 659 | options.headers.Date = jsprim.rfc1123(new Date()); 660 | options.headers['digest'] = uuid(); 661 | 662 | http.get(options, function(res) { 663 | t.equal(res.statusCode, 200); 664 | t.end(); 665 | }); 666 | }); 667 | 668 | test('valid explicit headers request-target', function(t) { 669 | server.tester = function(req, res) { 670 | var parsed = httpSignature.parseRequest(req); 671 | res.writeHead(200); 672 | res.write(JSON.stringify(parsed, null, 2)); 673 | res.end(); 674 | }; 675 | 676 | 677 | options.headers.Authorization = 678 | 'Signature keyId="fo,o",algorithm="RSA-sha256",' + 679 | 'headers="dAtE dIgEsT (request-target)",' + 680 | 'extensions="blah blah",signature="digitalSignature"'; 681 | options.headers.Date = jsprim.rfc1123(new Date()); 682 | options.headers['digest'] = uuid(); 683 | 684 | http.get(options, function(res) { 685 | t.equal(res.statusCode, 200); 686 | 687 | var body = ''; 688 | res.setEncoding('utf8'); 689 | res.on('data', function(chunk) { 690 | body += chunk; 691 | }); 692 | 693 | res.on('end', function() { 694 | console.log(body); 695 | var parsed = JSON.parse(body); 696 | t.ok(parsed); 697 | t.equal(parsed.scheme, 'Signature'); 698 | t.ok(parsed.params); 699 | t.equal(parsed.params.keyId, 'fo,o'); 700 | t.equal(parsed.params.algorithm, 'rsa-sha256'); 701 | t.equal(parsed.params.extensions, 'blah blah'); 702 | t.ok(parsed.params.headers); 703 | t.equal(parsed.params.headers.length, 3); 704 | t.equal(parsed.params.headers[0], 'date'); 705 | t.equal(parsed.params.headers[1], 'digest'); 706 | t.equal(parsed.params.headers[2], '(request-target)'); 707 | t.equal(parsed.params.signature, 'digitalSignature'); 708 | t.ok(parsed.signingString); 709 | t.equal(parsed.signingString, 710 | ('date: ' + options.headers.Date + '\n' + 711 | 'digest: ' + options.headers['digest'] + '\n' + 712 | '(request-target): get /')); 713 | t.equal(parsed.params.keyId, parsed.keyId); 714 | t.equal(parsed.params.algorithm.toUpperCase(), 715 | parsed.algorithm); 716 | t.end(); 717 | }); 718 | }); 719 | }); 720 | 721 | 722 | test('expired', function(t) { 723 | server.tester = function(req, res) { 724 | var options = { 725 | clockSkew: 1, 726 | headers: ['date'] 727 | }; 728 | 729 | setTimeout(function() { 730 | try { 731 | httpSignature.parseRequest(req); 732 | } catch (e) { 733 | t.equal(e.name, 'ExpiredRequestError'); 734 | t.ok(/clock skew of \d\.\d+s was greater than 1s/.test(e.message)); 735 | } 736 | 737 | res.writeHead(200); 738 | res.end(); 739 | }, 1200); 740 | }; 741 | 742 | options.headers.Authorization = 743 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 744 | 'headers="dAtE dIgEsT",signature="digitalSignature"'; 745 | options.headers.Date = jsprim.rfc1123(new Date()); 746 | options.headers['digest'] = uuid(); 747 | http.get(options, function(res) { 748 | t.equal(res.statusCode, 200); 749 | t.end(); 750 | }); 751 | }); 752 | 753 | 754 | test('missing required header', function(t) { 755 | server.tester = function(req, res) { 756 | var options = { 757 | clockSkew: 1, 758 | headers: ['date', 'x-unit-test'] 759 | }; 760 | 761 | try { 762 | httpSignature.parseRequest(req, options); 763 | } catch (e) { 764 | t.equal(e.name, 'MissingHeaderError'); 765 | t.equal(e.message, 'x-unit-test was not a signed header'); 766 | } 767 | 768 | res.writeHead(200); 769 | res.end(); 770 | }; 771 | 772 | options.headers.Authorization = 773 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 774 | 'headers="dAtE cOntEnt-MD5",signature="digitalSignature"'; 775 | options.headers.Date = jsprim.rfc1123(new Date()); 776 | options.headers['content-md5'] = uuid(); 777 | http.get(options, function(res) { 778 | t.equal(res.statusCode, 200); 779 | t.end(); 780 | }); 781 | }); 782 | 783 | 784 | test('valid mixed case headers', function(t) { 785 | server.tester = function(req, res) { 786 | var options = { 787 | clockSkew: 1, 788 | headers: ['Date', 'Content-MD5'] 789 | }; 790 | 791 | try { 792 | httpSignature.parseRequest(req, options); 793 | } catch (e) { 794 | t.fail(e.stack); 795 | } 796 | 797 | res.writeHead(200); 798 | res.end(); 799 | }; 800 | 801 | options.headers.Authorization = 802 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 803 | 'headers="dAtE cOntEnt-MD5",signature="digitalSignature"'; 804 | options.headers.Date = jsprim.rfc1123(new Date()); 805 | options.headers['content-md5'] = uuid(); 806 | http.get(options, function(res) { 807 | t.equal(res.statusCode, 200); 808 | t.end(); 809 | }); 810 | }); 811 | 812 | 813 | test('not whitelisted algorithm', function(t) { 814 | server.tester = function(req, res) { 815 | var options = { 816 | clockSkew: 1, 817 | algorithms: ['rsa-sha1'] 818 | }; 819 | 820 | try { 821 | httpSignature.parseRequest(req, options); 822 | } catch (e) { 823 | t.equal('InvalidParamsError', e.name); 824 | t.equal('rsa-sha256 is not a supported algorithm', e.message); 825 | } 826 | 827 | res.writeHead(200); 828 | res.end(); 829 | }; 830 | 831 | options.headers.Authorization = 832 | 'Signature keyId="f,oo",algorithm="RSA-sha256",' + 833 | 'headers="dAtE dIgEsT",signature="digitalSignature"'; 834 | options.headers.Date = jsprim.rfc1123(new Date()); 835 | options.headers['digest'] = uuid(); 836 | http.get(options, function(res) { 837 | t.equal(res.statusCode, 200); 838 | t.end(); 839 | }); 840 | }); 841 | 842 | 843 | 844 | 845 | test('tearDown', function(t) { 846 | server.on('close', function() { 847 | t.end(); 848 | }); 849 | server.close(); 850 | }); 851 | -------------------------------------------------------------------------------- /test/verify.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Joyent, Inc. All rights reserved. 2 | 3 | var crypto = require('crypto'); 4 | var fs = require('fs'); 5 | var http = require('http'); 6 | var jsprim = require('jsprim'); 7 | var sshpk = require('sshpk'); 8 | 9 | var test = require('tap').test; 10 | var uuid = require('uuid').v4; 11 | 12 | var httpSignature = require('../lib/index'); 13 | 14 | 15 | 16 | ///--- Globals 17 | 18 | var hmacKey = null; 19 | var rawhmacKey = null; 20 | var options = null; 21 | var rsaPrivate = null; 22 | var rsaPublic = null; 23 | var dsaPrivate = null; 24 | var dsaPublic = null; 25 | var ecdsaPrivate = null; 26 | var ecdsaPublic = null; 27 | var ed25519Private = null; 28 | var ed25519Public = null; 29 | var server = null; 30 | var socket = null; 31 | 32 | 33 | ///--- Tests 34 | 35 | test('setup', function(t) { 36 | rsaPrivate = fs.readFileSync(__dirname + '/rsa_private.pem', 'ascii'); 37 | dsaPrivate = fs.readFileSync(__dirname + '/dsa_private.pem', 'ascii'); 38 | ecdsaPrivate = fs.readFileSync(__dirname + '/ecdsa_private.pem', 'ascii'); 39 | 40 | { 41 | const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519', { 42 | publicKeyEncoding: { 43 | type: 'spki', 44 | format: 'pem' 45 | }, 46 | privateKeyEncoding: { 47 | type: 'pkcs8', 48 | format: 'pem' 49 | } 50 | }); 51 | 52 | ed25519Private = privateKey; 53 | ed25519Public = publicKey; 54 | } 55 | 56 | t.ok(rsaPrivate); 57 | t.ok(dsaPrivate); 58 | t.ok(ecdsaPrivate); 59 | t.ok(ed25519Private); 60 | 61 | rsaPublic = fs.readFileSync(__dirname + '/rsa_public.pem', 'ascii'); 62 | dsaPublic = fs.readFileSync(__dirname + '/dsa_public.pem', 'ascii'); 63 | ecdsaPublic = fs.readFileSync(__dirname + '/ecdsa_public.pem', 'ascii'); 64 | t.ok(rsaPublic); 65 | t.ok(dsaPublic); 66 | t.ok(ecdsaPublic); 67 | t.ok(ed25519Public); 68 | 69 | hmacKey = uuid(); 70 | rawhmacKey = crypto.randomBytes(64); 71 | 72 | socket = '/tmp/.' + uuid(); 73 | options = { 74 | socketPath: socket, 75 | path: '/', 76 | headers: {} 77 | }; 78 | 79 | server = http.createServer(function(req, res) { 80 | server.tester(req, res); 81 | }); 82 | 83 | server.listen(socket, function() { 84 | t.end(); 85 | }); 86 | }); 87 | 88 | 89 | test('invalid hmac', function(t) { 90 | server.tester = function(req, res) { 91 | var parsed = httpSignature.parseRequest(req); 92 | t.ok(!httpSignature.verifyHMAC(parsed, hmacKey)); 93 | 94 | res.writeHead(200); 95 | res.write(JSON.stringify(parsed, null, 2)); 96 | res.end(); 97 | }; 98 | 99 | options.headers.Date = jsprim.rfc1123(new Date()); 100 | options.headers.Authorization = 101 | 'Signature keyId="foo",algorithm="hmac-sha1",signature="' + 102 | uuid() + '"'; 103 | 104 | http.get(options, function(res) { 105 | t.equal(res.statusCode, 200); 106 | t.end(); 107 | }); 108 | }); 109 | 110 | 111 | test('valid hmac', function(t) { 112 | server.tester = function(req, res) { 113 | var parsed = httpSignature.parseRequest(req); 114 | t.ok(httpSignature.verifyHMAC(parsed, hmacKey)); 115 | 116 | res.writeHead(200); 117 | res.write(JSON.stringify(parsed, null, 2)); 118 | res.end(); 119 | }; 120 | 121 | options.headers.Date = jsprim.rfc1123(new Date()); 122 | var hmac = crypto.createHmac('sha1', hmacKey); 123 | hmac.update('date: ' + options.headers.Date); 124 | options.headers.Authorization = 125 | 'Signature keyId="foo",algorithm="hmac-sha1",signature="' + 126 | hmac.digest('base64') + '"'; 127 | 128 | http.get(options, function(res) { 129 | t.equal(res.statusCode, 200); 130 | t.end(); 131 | }); 132 | }); 133 | 134 | test('invalid raw hmac', function(t) { 135 | server.tester = function(req, res) { 136 | var parsed = httpSignature.parseRequest(req); 137 | t.ok(!httpSignature.verifyHMAC(parsed, rawhmacKey)); 138 | 139 | res.writeHead(200); 140 | res.write(JSON.stringify(parsed, null, 2)); 141 | res.end(); 142 | }; 143 | 144 | options.headers.Date = jsprim.rfc1123(new Date()); 145 | options.headers.Authorization = 146 | 'Signature keyId="foo",algorithm="hmac-sha1",signature="' + 147 | uuid() + '"'; 148 | 149 | http.get(options, function(res) { 150 | t.equal(res.statusCode, 200); 151 | t.end(); 152 | }); 153 | }); 154 | 155 | test('valid raw hmac', function(t) { 156 | server.tester = function(req, res) { 157 | var parsed = httpSignature.parseRequest(req); 158 | t.ok(httpSignature.verifyHMAC(parsed, rawhmacKey)); 159 | 160 | res.writeHead(200); 161 | res.write(JSON.stringify(parsed, null, 2)); 162 | res.end(); 163 | }; 164 | 165 | options.headers.Date = jsprim.rfc1123(new Date()); 166 | var hmac = crypto.createHmac('sha1', rawhmacKey); 167 | hmac.update('date: ' + options.headers.Date); 168 | options.headers.Authorization = 169 | 'Signature keyId="foo",algorithm="hmac-sha1",signature="' + 170 | hmac.digest('base64') + '"'; 171 | 172 | http.get(options, function(res) { 173 | t.equal(res.statusCode, 200); 174 | t.end(); 175 | }); 176 | }); 177 | 178 | test('invalid rsa', function(t) { 179 | server.tester = function(req, res) { 180 | var parsed = httpSignature.parseRequest(req); 181 | t.ok(!httpSignature.verify(parsed, rsaPublic)); 182 | 183 | res.writeHead(200); 184 | res.write(JSON.stringify(parsed, null, 2)); 185 | res.end(); 186 | }; 187 | 188 | options.headers.Date = jsprim.rfc1123(new Date()); 189 | options.headers.Authorization = 190 | 'Signature keyId="foo",algorithm="rsa-sha1",signature="' + 191 | uuid() + '"'; 192 | 193 | http.get(options, function(res) { 194 | t.equal(res.statusCode, 200); 195 | t.end(); 196 | }); 197 | }); 198 | 199 | 200 | test('valid rsa', function(t) { 201 | server.tester = function(req, res) { 202 | var parsed = httpSignature.parseRequest(req); 203 | t.ok(httpSignature.verify(parsed, rsaPublic)); 204 | 205 | res.writeHead(200); 206 | res.write(JSON.stringify(parsed, null, 2)); 207 | res.end(); 208 | }; 209 | 210 | options.headers.Date = jsprim.rfc1123(new Date()); 211 | var signer = crypto.createSign('RSA-SHA256'); 212 | signer.update('date: ' + options.headers.Date); 213 | options.headers.Authorization = 214 | 'Signature keyId="foo",algorithm="rsa-sha256",signature="' + 215 | signer.sign(rsaPrivate, 'base64') + '"'; 216 | 217 | http.get(options, function(res) { 218 | t.equal(res.statusCode, 200); 219 | t.end(); 220 | }); 221 | }); 222 | 223 | test('invalid dsa', function(t) { 224 | server.tester = function(req, res) { 225 | var parsed = httpSignature.parseRequest(req); 226 | t.ok(!httpSignature.verify(parsed, dsaPublic)); 227 | 228 | res.writeHead(200); 229 | res.write(JSON.stringify(parsed, null, 2)); 230 | res.end(); 231 | }; 232 | 233 | options.headers.Date = jsprim.rfc1123(new Date()); 234 | options.headers.Authorization = 235 | 'Signature keyId="foo",algorithm="dsa-sha1",signature="' + 236 | uuid() + '"'; 237 | 238 | http.get(options, function(res) { 239 | t.equal(res.statusCode, 200); 240 | t.end(); 241 | }); 242 | }); 243 | 244 | 245 | test('valid dsa', function(t) { 246 | server.tester = function(req, res) { 247 | var parsed = httpSignature.parseRequest(req); 248 | t.ok(httpSignature.verify(parsed, dsaPublic)); 249 | 250 | res.writeHead(200); 251 | res.write(JSON.stringify(parsed, null, 2)); 252 | res.end(); 253 | }; 254 | 255 | options.headers.Date = jsprim.rfc1123(new Date()); 256 | var key = sshpk.parsePrivateKey(dsaPrivate); 257 | var signer = key.createSign('sha256'); 258 | signer.update('date: ' + options.headers.Date); 259 | options.headers.Authorization = 260 | 'Signature keyId="foo",algorithm="dsa-sha256",signature="' + 261 | signer.sign().toString() + '"'; 262 | 263 | http.get(options, function(res) { 264 | t.equal(res.statusCode, 200); 265 | t.end(); 266 | }); 267 | }); 268 | 269 | test('invalid ecdsa', function(t) { 270 | server.tester = function(req, res) { 271 | var parsed = httpSignature.parseRequest(req); 272 | t.ok(!httpSignature.verify(parsed, ecdsaPublic)); 273 | 274 | res.writeHead(200); 275 | res.write(JSON.stringify(parsed, null, 2)); 276 | res.end(); 277 | }; 278 | 279 | options.headers.Date = jsprim.rfc1123(new Date()); 280 | options.headers.Authorization = 281 | 'Signature keyId="foo",algorithm="ecdsa-sha256",signature="' + 282 | uuid() + '"'; 283 | 284 | http.get(options, function(res) { 285 | t.equal(res.statusCode, 200); 286 | t.end(); 287 | }); 288 | }); 289 | 290 | 291 | test('valid ecdsa', function(t) { 292 | server.tester = function(req, res) { 293 | var parsed = httpSignature.parseRequest(req); 294 | t.ok(httpSignature.verify(parsed, ecdsaPublic)); 295 | 296 | res.writeHead(200); 297 | res.write(JSON.stringify(parsed, null, 2)); 298 | res.end(); 299 | }; 300 | 301 | options.headers.Date = jsprim.rfc1123(new Date()); 302 | var key = sshpk.parsePrivateKey(ecdsaPrivate); 303 | var signer = key.createSign('sha512'); 304 | signer.update('date: ' + options.headers.Date); 305 | options.headers.Authorization = 306 | 'Signature keyId="foo",algorithm="ecdsa-sha512",signature="' + 307 | signer.sign().toString() + '"'; 308 | 309 | http.get(options, function(res) { 310 | t.equal(res.statusCode, 200); 311 | t.end(); 312 | }); 313 | }); 314 | 315 | test('invalid ed25519', function(t) { 316 | server.tester = function(req, res) { 317 | var parsed = httpSignature.parseRequest(req); 318 | t.ok(!httpSignature.verify(parsed, ed25519Public)); 319 | 320 | res.writeHead(200); 321 | res.write(JSON.stringify(parsed, null, 2)); 322 | res.end(); 323 | }; 324 | 325 | options.headers.Date = jsprim.rfc1123(new Date()); 326 | options.headers.Authorization = 327 | 'Signature keyId="foo",algorithm="ed25519-sha512",signature="' + 328 | 'a'.repeat(86) + '"'; 329 | 330 | http.get(options, function(res) { 331 | t.equal(res.statusCode, 200); 332 | t.end(); 333 | }); 334 | }); 335 | 336 | 337 | test('valid ed25519', function(t) { 338 | server.tester = function(req, res) { 339 | var parsed = httpSignature.parseRequest(req); 340 | t.ok(httpSignature.verify(parsed, ed25519Public)); 341 | 342 | res.writeHead(200); 343 | res.write(JSON.stringify(parsed, null, 2)); 344 | res.end(); 345 | }; 346 | 347 | options.headers.Date = jsprim.rfc1123(new Date()); 348 | var key = sshpk.parsePrivateKey(ed25519Private); 349 | var signer = key.createSign('sha512'); 350 | signer.update('date: ' + options.headers.Date); 351 | options.headers.Authorization = 352 | 'Signature keyId="foo",algorithm="ed25519-sha512",signature="' + 353 | signer.sign().toString() + '"'; 354 | 355 | http.get(options, function(res) { 356 | t.equal(res.statusCode, 200); 357 | t.end(); 358 | }); 359 | }); 360 | 361 | 362 | test('invalid hs2019', function(t) { 363 | server.tester = function(req, res) { 364 | var parsed = httpSignature.parseRequest(req); 365 | t.ok(!httpSignature.verify(parsed, ecdsaPublic)); 366 | 367 | res.writeHead(200); 368 | res.write(JSON.stringify(parsed, null, 2)); 369 | res.end(); 370 | }; 371 | 372 | options.headers.Date = jsprim.rfc1123(new Date()); 373 | options.headers.Authorization = 374 | 'Signature keyId="foo",algorithm="hs2019",signature="' + 375 | uuid() + '"'; 376 | 377 | http.get(options, function(res) { 378 | t.equal(res.statusCode, 200); 379 | t.end(); 380 | }); 381 | }); 382 | 383 | test('for now valid hs2019 (valid ecdsa-sha256)', function(t) { 384 | server.tester = function(req, res) { 385 | var parsed = httpSignature.parseRequest(req); 386 | t.ok(httpSignature.verify(parsed, ecdsaPublic), 'hs2019 ecdsa-sha256'); 387 | 388 | res.writeHead(200); 389 | res.write(JSON.stringify(parsed, null, 2)); 390 | res.end(); 391 | }; 392 | 393 | options.headers.Date = jsprim.rfc1123(new Date()); 394 | var key = sshpk.parsePrivateKey(ecdsaPrivate); 395 | var signer = key.createSign('sha256'); 396 | signer.update('date: ' + options.headers.Date); 397 | options.headers.Authorization = 398 | 'Signature keyId="foo",algorithm="hs2019",signature="' + 399 | signer.sign().toString() + '"'; 400 | 401 | http.get(options, function(res) { 402 | t.equal(res.statusCode, 200); 403 | t.end(); 404 | }); 405 | }); 406 | 407 | test('for now invalid hs2019 (valid ecdsa-sha512)', function(t) { 408 | server.tester = function(req, res) { 409 | var parsed = httpSignature.parseRequest(req); 410 | t.ok(!httpSignature.verify(parsed, ecdsaPublic), 'hs2019 ecdsa-sha512'); 411 | 412 | res.writeHead(200); 413 | res.write(JSON.stringify(parsed, null, 2)); 414 | res.end(); 415 | }; 416 | 417 | options.headers.Date = jsprim.rfc1123(new Date()); 418 | var key = sshpk.parsePrivateKey(ecdsaPrivate); 419 | var signer = key.createSign('sha512'); 420 | signer.update('date: ' + options.headers.Date); 421 | options.headers.Authorization = 422 | 'Signature keyId="foo",algorithm="hs2019",signature="' + 423 | signer.sign().toString() + '"'; 424 | 425 | http.get(options, function(res) { 426 | t.equal(res.statusCode, 200); 427 | t.end(); 428 | }); 429 | }); 430 | 431 | test('for now invalid hs2019 (valid ed25519-sha512)', function(t) { 432 | server.tester = function(req, res) { 433 | var parsed = httpSignature.parseRequest(req); 434 | t.ok(httpSignature.verify(parsed, ed25519Public), 'hs2019 ed25519-sha512'); 435 | 436 | res.writeHead(200); 437 | res.write(JSON.stringify(parsed, null, 2)); 438 | res.end(); 439 | }; 440 | 441 | options.headers.Date = jsprim.rfc1123(new Date()); 442 | var key = sshpk.parsePrivateKey(ed25519Private); 443 | var signer = key.createSign('sha512'); 444 | signer.update('date: ' + options.headers.Date); 445 | options.headers.Authorization = 446 | 'Signature keyId="foo",algorithm="hs2019",signature="' + 447 | signer.sign().toString() + '"'; 448 | 449 | http.get(options, function(res) { 450 | t.equal(res.statusCode, 200); 451 | t.end(); 452 | }); 453 | }); 454 | 455 | test('for now invalid hs2019 (valid dsa-sha512)', function(t) { 456 | server.tester = function(req, res) { 457 | var parsed = httpSignature.parseRequest(req); 458 | t.ok(!httpSignature.verify(parsed, dsaPublic), 'hs2019 dsa-sha512'); 459 | 460 | res.writeHead(200); 461 | res.write(JSON.stringify(parsed, null, 2)); 462 | res.end(); 463 | }; 464 | 465 | options.headers.Date = jsprim.rfc1123(new Date()); 466 | var key = sshpk.parsePrivateKey(dsaPrivate); 467 | var signer = key.createSign('sha512'); 468 | signer.update('date: ' + options.headers.Date); 469 | options.headers.Authorization = 470 | 'Signature keyId="foo",algorithm="hs2019",signature="' + 471 | signer.sign().toString() + '"'; 472 | 473 | http.get(options, function(res) { 474 | t.equal(res.statusCode, 200); 475 | t.end(); 476 | }); 477 | }); 478 | 479 | test('for now valid hs2019 (valid rsa-sha256)', function(t) { 480 | server.tester = function(req, res) { 481 | var parsed = httpSignature.parseRequest(req); 482 | t.ok(httpSignature.verify(parsed, rsaPublic), 'hs2019 rsa-sha256'); 483 | 484 | res.writeHead(200); 485 | res.write(JSON.stringify(parsed, null, 2)); 486 | res.end(); 487 | }; 488 | 489 | options.headers.Date = jsprim.rfc1123(new Date()); 490 | var signer = crypto.createSign('RSA-SHA256'); 491 | signer.update('date: ' + options.headers.Date); 492 | options.headers.Authorization = 493 | 'Signature keyId="foo",algorithm="hs2019",signature="' + 494 | signer.sign(rsaPrivate, 'base64') + '"'; 495 | 496 | http.get(options, function(res) { 497 | t.equal(res.statusCode, 200); 498 | t.end(); 499 | }); 500 | }); 501 | 502 | test('for now invalid hs2019 (valid rsa-sha512)', function(t) { 503 | server.tester = function(req, res) { 504 | var parsed = httpSignature.parseRequest(req); 505 | t.ok(!httpSignature.verify(parsed, rsaPublic), 'hs2019 rsa-sha512'); 506 | 507 | res.writeHead(200); 508 | res.write(JSON.stringify(parsed, null, 2)); 509 | res.end(); 510 | }; 511 | 512 | options.headers.Date = jsprim.rfc1123(new Date()); 513 | var signer = crypto.createSign('RSA-SHA512'); 514 | signer.update('date: ' + options.headers.Date); 515 | options.headers.Authorization = 516 | 'Signature keyId="foo",algorithm="hs2019",signature="' + 517 | signer.sign(rsaPrivate, 'base64') + '"'; 518 | 519 | http.get(options, function(res) { 520 | t.equal(res.statusCode, 200); 521 | t.end(); 522 | }); 523 | }); 524 | 525 | 526 | test('invalid date', function(t) { 527 | server.tester = function(req, res) { 528 | t.throws(function() { 529 | httpSignature.parseRequest(req); 530 | }); 531 | 532 | res.writeHead(400); 533 | res.end(); 534 | }; 535 | 536 | options.method = 'POST'; 537 | options.path = '/'; 538 | options.headers.host = 'example.com'; 539 | // very old, out of valid date range 540 | options.headers.Date = 'Sat, 01 Jan 2000 00:00:00 GMT'; 541 | var signer = crypto.createSign('RSA-SHA256'); 542 | signer.update('date: ' + options.headers.Date); 543 | options.headers.Authorization = 544 | 'Signature keyId="Test",algorithm="rsa-sha256",signature="' + 545 | signer.sign(rsaPrivate, 'base64') + '"'; 546 | 547 | var req = http.request(options, function(res) { 548 | t.equal(res.statusCode, 400); 549 | t.end(); 550 | }); 551 | req.end(); 552 | }); 553 | 554 | 555 | // test values from spec for simple test 556 | test('valid rsa from spec default', function(t) { 557 | server.tester = function(req, res) { 558 | var parsed = httpSignature.parseRequest(req, { 559 | // this test uses a fixed old date so ignore clock skew 560 | clockSkew: Number.MAX_VALUE 561 | }); 562 | t.ok(httpSignature.verify(parsed, rsaPublic)); 563 | // check known signature 564 | t.ok(req.headers.authorization === 'Signature keyId="Test",algorithm="rsa-sha256",signature="ATp0r26dbMIxOopqw0OfABDT7CKMIoENumuruOtarj8n/97Q3htHFYpH8yOSQk3Z5zh8UxUym6FYTb5+A0Nz3NRsXJibnYi7brE/4tx5But9kkFGzG+xpUmimN4c3TMN7OFH//+r8hBf7BT9/GmHDUVZT2JzWGLZES2xDOUuMtA="'); 565 | 566 | res.writeHead(200); 567 | res.write(JSON.stringify(parsed, null, 2)); 568 | res.end(); 569 | }; 570 | 571 | options.method = 'POST'; 572 | options.path = '/'; 573 | options.headers.host = 'example.com'; 574 | // date from spec examples 575 | options.headers.Date = 'Thu, 05 Jan 2012 21:31:40 GMT'; 576 | var signer = crypto.createSign('RSA-SHA256'); 577 | signer.update('date: ' + options.headers.Date); 578 | options.headers.Authorization = 579 | 'Signature keyId="Test",algorithm="rsa-sha256",signature="' + 580 | signer.sign(rsaPrivate, 'base64') + '"'; 581 | 582 | var req = http.request(options, function(res) { 583 | t.equal(res.statusCode, 200); 584 | t.end(); 585 | }); 586 | req.end(); 587 | }); 588 | 589 | 590 | // test values from spec for defaults 591 | test('valid rsa from spec default', function(t) { 592 | var jsonMessage = '{"hello": "world"}'; 593 | var sha256sum = crypto.createHash('sha256'); 594 | sha256sum.update(jsonMessage) 595 | 596 | server.tester = function(req, res) { 597 | var parsed = httpSignature.parseRequest(req, { 598 | // this test uses a fixed old date so ignore clock skew 599 | clockSkew: Number.MAX_VALUE 600 | }); 601 | t.ok(httpSignature.verify(parsed, rsaPublic)); 602 | // check known signature 603 | t.ok(req.headers.authorization === 'Signature keyId="Test",algorithm="rsa-sha256",signature="jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w="'); 604 | 605 | res.writeHead(200); 606 | res.write(JSON.stringify(parsed, null, 2)); 607 | res.end(); 608 | }; 609 | 610 | options.method = 'POST'; 611 | options.path = '/foo?param=value&pet=dog'; 612 | options.headers.host = 'example.com'; 613 | options.headers.Date = 'Thu, 05 Jan 2014 21:31:40 GMT'; 614 | options.headers['content-type'] = 'application/json'; 615 | options.headers['digest'] = 'SHA-256=' + sha256sum.digest('base64'); 616 | options.headers['content-length'] = '' + (jsonMessage.length - 1); 617 | var signer = crypto.createSign('RSA-SHA256'); 618 | signer.update('date: ' + options.headers.Date); 619 | options.headers.Authorization = 620 | 'Signature keyId="Test",algorithm="rsa-sha256",signature="' + 621 | signer.sign(rsaPrivate, 'base64') + '"'; 622 | 623 | var req = http.request(options, function(res) { 624 | t.equal(res.statusCode, 200); 625 | t.end(); 626 | }); 627 | req.write(jsonMessage); 628 | req.end(); 629 | }); 630 | 631 | // test values from spec for all headers 632 | test('valid rsa from spec all headers', function(t) { 633 | var jsonMessage = '{"hello": "world"}'; 634 | var sha256sum = crypto.createHash('sha256'); 635 | sha256sum.update(jsonMessage) 636 | 637 | server.tester = function(req, res) { 638 | var parsed = httpSignature.parseRequest(req, { 639 | // this test uses a fixed old date so ignore clock skew 640 | clockSkew: Number.MAX_VALUE 641 | }); 642 | t.ok(httpSignature.verify(parsed, rsaPublic)); 643 | // check known signature 644 | t.ok(req.headers.authorization === 'Signature keyId="Test",algorithm="rsa-sha256",headers="request-line host date content-type digest content-length",signature="jgSqYK0yKclIHfF9zdApVEbDp5eqj8C4i4X76pE+XHoxugXv7qnVrGR+30bmBgtpR39I4utq17s9ghz/2QFVxlnToYAvbSVZJ9ulLd1HQBugO0jOyn9sXOtcN7uNHBjqNCqUsnt0sw/cJA6B6nJZpyNqNyAXKdxZZItOuhIs78w="'); 645 | 646 | res.writeHead(200); 647 | res.write(JSON.stringify(parsed, null, 2)); 648 | res.end(); 649 | }; 650 | 651 | options.method = 'POST'; 652 | options.path = '/foo?param=value&pet=dog'; 653 | options.headers.host = 'example.com'; 654 | options.headers.Date = 'Thu, 05 Jan 2014 21:31:40 GMT'; 655 | options.headers['content-type'] = 'application/json'; 656 | options.headers['digest'] = 'SHA-256=' + sha256sum.digest('base64'); 657 | options.headers['content-length'] = '' + (jsonMessage.length - 1); 658 | var signer = crypto.createSign('RSA-SHA256'); 659 | signer.update(options.method + ' ' + options.path + ' HTTP/1.1\n'); 660 | signer.update('host: ' + options.headers.host + '\n'); 661 | signer.update('date: ' + options.headers.Date + '\n'); 662 | signer.update('content-type: ' + options.headers['content-type'] + '\n'); 663 | signer.update('digest: ' + options.headers['digest'] + '\n'); 664 | signer.update('content-length: ' + options.headers['content-length']); 665 | options.headers.Authorization = 666 | 'Signature keyId="Test",algorithm="rsa-sha256",headers=' + 667 | '"request-line host date content-type digest content-length"' + 668 | ',signature="' + signer.sign(rsaPrivate, 'base64') + '"'; 669 | 670 | var req = http.request(options, function(res) { 671 | t.equal(res.statusCode, 200); 672 | t.end(); 673 | }); 674 | req.write(jsonMessage); 675 | req.end(); 676 | }); 677 | 678 | test('valid rsa from spec all headers (request-target)', function(t) { 679 | var jsonMessage = '{"hello": "world"}'; 680 | var sha256sum = crypto.createHash('sha256'); 681 | sha256sum.update(jsonMessage); 682 | 683 | server.tester = function(req, res) { 684 | var parsed = httpSignature.parseRequest(req, { 685 | // this test uses a fixed old date so ignore clock skew 686 | clockSkew: Number.MAX_VALUE 687 | }); 688 | t.ok(httpSignature.verify(parsed, rsaPublic)); 689 | // check known signature 690 | t.ok(req.headers.authorization === 'Signature keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="Tqfe2TGMEOwrHLItN2pDnKZiV3cKDWx1dTreYvWRH/kYVT0avw975g25I0/Sig2l60CDkRKTk9ciJMkn8Eanpa7aICnRWbOu38+ozMfQrM7cc06NRSY6+UQ67dn6K4jEW0WNWxhLLwWBSXxhxuXOL3rFKYZliNCundM9FiYk5aE="'); 691 | 692 | res.writeHead(200); 693 | res.write(JSON.stringify(parsed, null, 2)); 694 | res.end(); 695 | }; 696 | 697 | 698 | 699 | options.method = 'POST'; 700 | options.path = '/foo?param=value&pet=dog'; 701 | options.headers.host = 'example.com'; 702 | options.headers.Date = 'Thu, 05 Jan 2014 21:31:40 GMT'; 703 | options.headers['content-type'] = 'application/json'; 704 | options.headers['digest'] = 'SHA-256=' + sha256sum.digest('base64'); 705 | options.headers['content-length'] = '' + (jsonMessage.length - 1); 706 | var signer = crypto.createSign('RSA-SHA256'); 707 | 708 | signer.update('(request-target): ' + options.method.toLowerCase() + ' ' + options.path + '\n'); 709 | signer.update('host: ' + options.headers.host + '\n'); 710 | signer.update('date: ' + options.headers.Date + '\n'); 711 | signer.update('content-type: ' + options.headers['content-type'] + '\n'); 712 | signer.update('digest: ' + options.headers['digest'] + '\n'); 713 | signer.update('content-length: ' + options.headers['content-length']); 714 | 715 | options.headers.Authorization = 716 | 'Signature keyId="Test",algorithm="rsa-sha256",headers=' + 717 | '"(request-target) host date content-type digest content-length"' + 718 | ',signature="' + signer.sign(rsaPrivate, 'base64') + '"'; 719 | 720 | var req = http.request(options, function(res) { 721 | t.equal(res.statusCode, 200); 722 | t.end(); 723 | }); 724 | req.write(jsonMessage); 725 | req.end(); 726 | }); 727 | 728 | 729 | test('tear down', function(t) { 730 | server.on('close', function() { 731 | t.end(); 732 | }); 733 | server.close(); 734 | }); 735 | --------------------------------------------------------------------------------