├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── collaborators.md ├── example.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.12' 4 | - '0.10' 5 | - 'iojs' 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ghsign 2 | 3 | Sign/verify data using your local ssh private key and your public key from Github 4 | 5 | ``` 6 | npm install ghsign 7 | ``` 8 | 9 | [![Build status](https://travis-ci.org/mafintosh/ghsign.svg?branch=master)](https://travis-ci.org/mafintosh/ghsign) 10 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://github.com/feross/standard) 11 | 12 | ## Usage 13 | 14 | ``` js 15 | var ghsign = require('ghsign') 16 | 17 | // create a signer (has be your own Github username) 18 | var sign = ghsign.signer('mafintosh') 19 | 20 | // create a verifier (can be any Github username) 21 | var verify = ghsign.verifier('mafintosh') 22 | 23 | // sign some data 24 | sign('test', function(err, sig) { 25 | console.log('test signature is', sig) 26 | 27 | // verify the signature 28 | verify('test', sig, function(err, valid) { 29 | console.log('wat test signed by mafintosh?', valid) 30 | }) 31 | }) 32 | ``` 33 | 34 | Creating a signer will fetch your public keys from github and use your 35 | corresponding local ssh private key to sign the data. The verifier will verify the signature by also fetching the public keys from Github. 36 | 37 | ## License 38 | 39 | MIT 40 | -------------------------------------------------------------------------------- /collaborators.md: -------------------------------------------------------------------------------- 1 | ## Collaborators 2 | 3 | ghsign is only possible due to the excellent work of the following collaborators: 4 | 5 | 6 | 7 | 8 | 9 | 10 |
watsonGitHub/watson
maxogdenGitHub/maxogden
beaugundersonGitHub/beaugunderson
mafintoshGitHub/mafintosh
calvinmetcalfGitHub/calvinmetcalf
11 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var username = 'maxogden' // change this to your github username 2 | var ghsign = require('./') 3 | 4 | var sign = ghsign.signer(username) 5 | var verify = ghsign.verifier(username) 6 | 7 | sign('hello', function (err, sig) { 8 | if (err) throw err 9 | verify('hello', sig, function (err, valid) { 10 | if (err) throw err 11 | console.log('hello was signed by ' + username + '? ' + valid) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | var thunky = require('thunky') 3 | var crypto = require('crypto') 4 | var fs = require('fs') 5 | var path = require('path') 6 | var SSHAgentClient = require('ssh-agent') 7 | var sshKeyToPEM = require('ssh-key-to-pem') 8 | var debug = require('debug')('ghsign') 9 | var bufferFrom = require('buffer-from') 10 | 11 | var readSync = function (file) { 12 | try { 13 | return fs.readFileSync(file) 14 | } catch (err) { 15 | return null 16 | } 17 | } 18 | 19 | var HOME = process.env.HOME || process.env.USERPROFILE 20 | var CACHE = path.join(HOME, '.cache') 21 | var DEFAULT_PRIVATE_KEY = readSync(path.join(HOME, '.ssh/id_rsa')) || readSync(path.join(HOME, '.ssh/id_dsa')) 22 | var SSH_AUTH_SOCK = !!process.env.SSH_AUTH_SOCK 23 | 24 | debug('SSH_AUTH_SOCK', process.env.SSH_AUTH_SOCK) 25 | 26 | var create = function (fetchKey) { 27 | var toPEM = function (key) { 28 | if (Buffer.isBuffer(key)) key = key.toString() 29 | try { 30 | return key.indexOf('-----BEGIN ') > -1 ? key : sshKeyToPEM(key) 31 | } catch (e) { 32 | return '' 33 | } 34 | } 35 | 36 | var isPublicKey = function (key) { 37 | return key.indexOf('-----BEGIN PUBLIC KEY-----') === 0 38 | } 39 | 40 | var githubPublicKeys = function (username, cb) { 41 | fetchKey(username, function (err, keys) { 42 | if (err) return cb(err) 43 | cb(null, keys.toString().trim().split('\n')) 44 | }) 45 | } 46 | 47 | var signer = function (username, keys) { 48 | if (username && username.indexOf('\n') > -1) return signer(null, username) 49 | keys = [].concat(keys || []).map(toPEM) 50 | 51 | var privateKey = keys.length && !isPublicKey(keys[0]) && keys[0] 52 | var publicKeys = keys.length && keys.every(isPublicKey) && keys 53 | var encrypted = false 54 | 55 | if (!SSH_AUTH_SOCK && !privateKey) { 56 | debug('using default private key (either ~/.ssh/id_rsa or ~/.ssh/id_dsa)') 57 | privateKey = DEFAULT_PRIVATE_KEY 58 | } 59 | 60 | if (privateKey) { 61 | if (privateKey.toString().indexOf('ENCRYPTED') > -1) encrypted = true 62 | return function sign (data, enc, cb) { 63 | if (typeof enc === 'function') return sign(data, null, enc) 64 | process.nextTick(function () { 65 | if (encrypted) return cb(new Error('Encrypted keys not supported. Setup an SSH agent or decrypt it first')) 66 | try { 67 | var sig = crypto.createSign('RSA-SHA1').update(data).sign(privateKey, enc) 68 | } catch (err) { 69 | return cb(err) 70 | } 71 | cb(null, sig) 72 | }) 73 | } 74 | } 75 | 76 | var client = new SSHAgentClient({timeout: 30000}) 77 | var pks = publicKeys ? 78 | function (cb) { 79 | cb(null, publicKeys) 80 | } : 81 | function (cb) { 82 | githubPublicKeys(username, cb) 83 | } 84 | 85 | var detectPublicKey = thunky(function (cb) { 86 | var oncache = function (cache, cb) { 87 | client.requestIdentities(function (err, keys) { 88 | if (err) return cb(err) 89 | debug('ssh-agent public keys', keys.map(function (k) { return k.ssh_key })) 90 | 91 | var key = keys.reduce(function (result, key) { 92 | var match = key.type === cache.type && key.ssh_key === cache.ssh_key && key 93 | if (match && match.type === 'ssh-rsa') return match 94 | return result || match 95 | }, null) 96 | 97 | if (!key) return onnocache(cb) 98 | cb(null, key) 99 | }) 100 | } 101 | 102 | var onnocache = function (cb) { 103 | pks(function (err, pubs) { 104 | if (err) return cb(err) 105 | 106 | client.requestIdentities(function (err, keys) { 107 | if (err) return cb(err) 108 | debug('ssh-agent public keys', keys.map(function (k) { return k.ssh_key })) 109 | 110 | var pubPems = pubs.map(toPEM) 111 | var key = keys.reduce(function (result, key) { 112 | var match = (pubPems.indexOf(toPEM(key.type + ' ' + key.ssh_key)) > -1 && key) && key 113 | if (match && match.type === 'ssh-rsa') return match 114 | return result || match 115 | }, null) 116 | 117 | if (!key && SSH_AUTH_SOCK && DEFAULT_PRIVATE_KEY) { 118 | debug('no suitable ssh-agent key') 119 | SSH_AUTH_SOCK = false 120 | return cb(null, null) 121 | } 122 | 123 | if (!key) return cb(new Error('No corresponding local SSH private key found for ' + username)) 124 | 125 | fs.mkdir(CACHE, function () { 126 | fs.writeFile(path.join(CACHE, 'ghsign.json'), JSON.stringify({username: username, type: key.type, ssh_key: key.ssh_key}), function () { 127 | cb(null, key) 128 | }) 129 | }) 130 | }) 131 | }) 132 | } 133 | 134 | fs.readFile(path.join(CACHE, 'ghsign.json'), 'utf-8', function (err, data) { 135 | if (err && err.code !== 'ENOENT') return cb(err) 136 | if (!data) return onnocache(cb) 137 | try { 138 | data = JSON.parse(data) 139 | } catch (err) { 140 | return oncache(cb) 141 | } 142 | 143 | if (data.username !== username) return onnocache(cb) 144 | oncache(data, cb) 145 | }) 146 | }) 147 | 148 | var cachedSign 149 | 150 | return function sign (data, enc, cb) { 151 | if (typeof enc === 'function') return sign(data, null, enc) 152 | if (typeof data === 'string') data = bufferFrom(data) 153 | if (cachedSign) return cachedSign(data, enc, cb) 154 | 155 | detectPublicKey(function (err, key) { 156 | if (err) return cb(err) 157 | if (key === null) { 158 | cachedSign = signer(username) 159 | return sign(data, enc, cb) 160 | } 161 | debug('selected public key', key.ssh_key) 162 | 163 | client.sign(key, data, function (err, sig) { 164 | if (err) return cb(err) 165 | 166 | if (enc === 'base64') return cb(null, sig.signature) 167 | var buf = bufferFrom(sig.signature, 'base64') 168 | if (enc) buf = buf.toString(enc) 169 | cb(null, buf) 170 | }) 171 | }) 172 | } 173 | } 174 | 175 | var verifier = function (username) { 176 | var pks = thunky(function (cb) { 177 | githubPublicKeys(username, cb) 178 | }) 179 | 180 | return function verify (data, sig, enc, cb) { 181 | if (typeof enc === 'function') return verify(data, sig, null, enc) 182 | if (!sig) return cb(null, false) 183 | 184 | pks(function (err, pubs) { 185 | if (err) return cb(err) 186 | 187 | var verified = pubs.some(function (key) { 188 | try { 189 | var valid = crypto.createVerify('RSA-SHA1').update(data).verify(toPEM(key), sig, enc) 190 | } catch (err) { 191 | return false 192 | } 193 | if (!valid) debug('verify failed', key) 194 | else debug('verify OK', key) 195 | return valid 196 | }) 197 | 198 | cb(null, verified) 199 | }) 200 | } 201 | } 202 | 203 | var exports = {} 204 | 205 | exports.publicKeys = githubPublicKeys 206 | exports.verifier = verifier 207 | exports.signer = signer 208 | 209 | return exports 210 | } 211 | 212 | var defaults = create(function (username, cb) { 213 | request('https://github.com/' + username + '.keys', {timeout: 30000}, function (err, response) { 214 | if (err) return cb(err) 215 | if (response.statusCode !== 200 || !response.body) return cb(new Error('Public keys for ' + username + ' not found')) 216 | cb(null, response.body) 217 | }) 218 | }) 219 | 220 | module.exports = create 221 | create.signer = defaults.signer 222 | create.verifier = defaults.verifier 223 | create.publicKeys = defaults.publicKeys 224 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ghsign", 3 | "version": "3.0.7", 4 | "description": "Sign/verify data using your local ssh private key and your public key from Github", 5 | "main": "index.js", 6 | "dependencies": { 7 | "buffer-from": "^1.0.0", 8 | "debug": "^2.1.3", 9 | "request": "^2.39.0", 10 | "ssh-agent": "^0.2.2", 11 | "ssh-key-to-pem": "^0.11.0", 12 | "thunky": "^0.1.0" 13 | }, 14 | "devDependencies": { 15 | "standard": "^4.5.2" 16 | }, 17 | "scripts": { 18 | "test": "standard" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/mafintosh/ghsign" 23 | }, 24 | "keywords": [ 25 | "sign", 26 | "github" 27 | ], 28 | "author": "Mathias Buus", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/mafintosh/ghsign/issues" 32 | }, 33 | "homepage": "https://github.com/mafintosh/ghsign" 34 | } 35 | --------------------------------------------------------------------------------