├── .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 | [](https://travis-ci.org/mafintosh/ghsign)
10 | [](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 |
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 |
--------------------------------------------------------------------------------