Skip to content
This repository has been archived by the owner on Sep 1, 2020. It is now read-only.

Commit

Permalink
Implement signed badge baking.
Browse files Browse the repository at this point in the history
  • Loading branch information
brianloveswords committed Nov 6, 2013
1 parent 8a02e75 commit f959dc1
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 20 deletions.
11 changes: 7 additions & 4 deletions README.md
Expand Up @@ -36,20 +36,23 @@ Bakes some data into an image.

Options are
- `image`: either a buffer or a stream representing the PNG or SVG to bake
- `assertion`: the assertion to save into the image
- `assertion`: assertion to save into the image (optional)
- `signature`: JSON Web Signature representing a signed OpenBadges assertion (optional)

You must pass either `assertion` or `signature`

`callback` has the signature `function(err, imageData)`

## bakery.extract(image, callback)

Gets the assertion data from the baked badge.
Gets the raw data from the badge. This could be a URL, assertion in JSON format or a signature.

`callback` has the signature `function (err, data)`

## bakery.debake(image, callback);
## bakery.getRemoteAssertion(image, callback);
## bakery.getAssertion(image, callback);

Gets the verification URL from a baked badge and attempts to retreive the assertion at the other end.
Gets the assertion from the badge. If the assertion is remote, this will require an HTTP request. If the assertion is baked into the badge, either directly or as part of a signature, this will pull the local copy.

`image` should be a stream or a buffer

Expand Down
28 changes: 22 additions & 6 deletions index.js
Expand Up @@ -10,6 +10,7 @@ const util = require('util');
const request = require('request');
const urlutil = require('url');
const typeCheck = require('./stream-type-check')
const jws = require('jws')

const png = require('./png')
const svg = require('./svg')
Expand All @@ -33,7 +34,6 @@ var extractors = {
'unknown': unknownImageType
}


function unknownImageType(opts, callback) {
return callback(new Error('Unknown/unhandled image type'))
}
Expand All @@ -54,19 +54,35 @@ function extract(imgdata, callback) {
}

function debake(image, callback) {
function defer(fn) {
return (global.setImmediate || process.nextTick)(fn)
}

extract(image, function (error, data) {
if (error)
return callback(error)
return defer(function(){ callback(error) })

var url = data
var assertion
var decoded

// signature?
if ((decoded = jws.decode(data))) {
try {
assertion = JSON.parse(decoded.payload)
return defer(function(){ callback(null, assertion) })
} catch (e) {
return defer(function(){ callback(errors.jsonParse(e)) })
}
}

// is the extracted data a URL or an assertion?
// assertion?
try {
assertion = JSON.parse(data)
url = assertion.verify.url
} catch (e) {}
return defer(function(){ callback(null, assertion) })
} catch (_) {}

// fall back to url
request(url, function (error, response, body) {
if (error) {
error = errors.request(error, url)
Expand Down Expand Up @@ -124,7 +140,7 @@ const errors = {
},

jsonParse: function (original, url) {
var error = new Error('Could not parse JSON at endpoint');
var error = new Error('Could not parse JSON');
error.code = 'JSON_PARSE_ERROR';
error.url = url;
error.original = original;
Expand Down
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -14,7 +14,8 @@
"cheerio": "~0.12.3",
"concat-stream": "~1.1.0",
"through": "~2.3.4",
"buffer-equal": "0.0.0"
"buffer-equal": "0.0.0",
"jws": "~0.2.2"
},
"devDependencies": {
"oneshot": "~0.1.0",
Expand Down
2 changes: 1 addition & 1 deletion png.js
Expand Up @@ -12,7 +12,7 @@ module.exports = {

function bake(options, callback) {
const buffer = options.image;
var data = options.url || options.data || options.assertion;
var data = options.url || options.data || options.assertion || options.signature;

if (!data)
return callback(new Error('must pass a `data` or `url` option'));
Expand Down
8 changes: 4 additions & 4 deletions svg.js
Expand Up @@ -10,8 +10,8 @@ module.exports = {

function bake(opts, callback) {
const assertion = opts.data || opts.assertion
const url = opts.url
? opts.url
const verifyString = (opts.url || opts.signature)
? (opts.url || opts.signature)
: (assertion && assertion.verify && assertion.verify.url)

var err;
Expand All @@ -23,7 +23,7 @@ function bake(opts, callback) {
}))
}

if (!url) {
if (!verifyString) {
err = new TypeError('Must provide a valid assertion or URL')
err.code = 'INVALID_ASSERTION'
return callback(err)
Expand All @@ -50,7 +50,7 @@ function bake(opts, callback) {
// add assertion information
const fmt = '<openbadges:assertion verify="%s">%s</openbadges:assertion>'
const contents = assertion ? cdata(assertion) : ''
const element = util.format(fmt, url, contents)
const element = util.format(fmt, verifyString, contents)

$('svg').prepend(element)

Expand Down
8 changes: 4 additions & 4 deletions test/bakery.test.js
Expand Up @@ -47,7 +47,7 @@ test('bakery.debake: should work with full assertion', function (t) {

t.plan(3)

broil(opts, function (baked) {
broil(opts, function (baked, server) {
bakery.extract(baked, function (err, string) {
const contents = JSON.parse(string)
t.same(contents.band, assertion.band)
Expand All @@ -57,6 +57,8 @@ test('bakery.debake: should work with full assertion', function (t) {
t.notOk(err, 'should not have an error')
t.same(contents.band, assertion.band)
})

server.close()
})
})

Expand Down Expand Up @@ -135,8 +137,6 @@ test('bakery.debake: error when debaking unbaked image', function (t) {
})
})



const urlutil = require('url')
const path = require('path')

Expand Down Expand Up @@ -167,7 +167,7 @@ function broil(opts, callback) {

bakery.bake(bakeOpts, function (err, baked) {
if (err) throw err;
callback(baked);
callback(baked, server);
})
})
}
27 changes: 27 additions & 0 deletions test/rsa-private.pem
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAzd55FbthjoKxo0SmKjS28M0IgK9uw+xY2ts/X2EEwM/g3svk
GND7rBikE26pJtJdRk6iJhn7eUHTKGRP48T5sXJaBvGexpmnAk6F1JS5J9YOl9hz
C4WIF3aiUGSs07Y9zUJGiuTWFcZAZh4bLxV/mijxGgC6DKZ3CluGLTguEwNHTLaC
j2+5rPIvRA5UjHq3du993MppKZChpdUoXWscfgVIX/sXQUtRXr+FRvZiahzqzq/a
kQBnZ7Z481KegEHfCZu70w98moI42sBFQ2HBj1TT4G4vHkB7OegyD8UTyQPmiuKL
oJtWxmjff3ZmJV/nPznO1qihTaQZ20+YAkkE4wIDAQABAoIBAQCUL52tHPI+KKlB
QrrxCnzcnFmQd7cdZFUZRFQ7jZRkTb4p4U3Q5y0hVaNVMYSkoyvs3dEt5+nyAIXE
P2CaLUEmj49qDCYkApoKuQbwXBVjLVvXxUAafQXXDdgMUkttIXMe+qPYepv3lHrq
hqU+oRT2nJ3f/8HsNS7ez76nchcIKzskoargJ3gbkqs21vgJ+zbbkq7aHKYIv7pU
IuVO4riAzLpzv2CpW5P8CJwpgdgpPt3h8hQcN7dD7LgeRK3zObYl94mHsiidskr9
zf2PeHMlLcDOKKHoLT0IKHAibiUtRBlCujwE2+x6m+taBIdW4gnSiX6K8M+OokgM
ztKNXgVhAoGBAPAeal9cfpPvV0c6o0ekW/zMLpSr24kcVR2HDbkQ+EJxABI22QmV
o18qshfafyPoxwJaNCBsBEMqhcLZrkx5+WqtuHOl9xEdY03ExO4MkVzEqjWW2ywI
/cjlqsXO7hzr9bKfArHCNO0z78n9NSIQdsM29YzRfCD92fYfHC+gAnOpAoGBANt8
J9MXOzW426HLx89aCrBtjbO70P6xt7TEHRfU/BX/OtTjvLDhaWxLta/sDEN97AmY
1OjVl83pqY2iGQl91eDRro8FTCrYsnePtWeSu1LoP4F+Y+AN+4YoqpA9EnF6W7cX
bpdleKvlBaWnVLgCq2GAxqq7V2bzQQvO6xK0sYurAoGALGNDALvlfk1pUfaKBo1g
n9vS5JLcc300UtALfmGfsxCWlcdj2by6xbXr1DME+8tlNo8cQb4WD6CFt55r9SE7
NThF98L/aD+Jgu/UA7l5CbAlUuC/RWWoHIIwTn7RT7Fi6xEv+1JOXGcUdb+EGfb3
LWE0hUKYcrTanP/lQvyOV3kCgYAJoAY3EZu9HWA0VjVq+G3jkspWCXR+1thRixAj
jWmIpWq1D0+lCh2PbDqJfnaDNzB0s0kS9Nr1YrvWxDcI3YXVmBxplTfj4SDicWDp
K9wfJKQ0T5CdsL9JANuJ+8OlSlJfUx28cey6zQ5UV2Q1tZhCbF1J+3E3yI8fr3FL
nrTyhQKBgEFpRGDssD/k+3VT8gYcpJZkYYUinHYq9C2A6KQguk964+GmHmkHMgh7
AP+6ikZB+XZST2ujL9NOrrrIhH/4iIabz7h8fibPYyzOufV/xUmmW2phqzoQX+IU
2wDdT8uGvCOkQ3FlWm9YuZkSok+t0yw6XA5tmfu9JzC2Q6sBB59y
-----END RSA PRIVATE KEY-----
9 changes: 9 additions & 0 deletions test/rsa-public.pem
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzd55FbthjoKxo0SmKjS2
8M0IgK9uw+xY2ts/X2EEwM/g3svkGND7rBikE26pJtJdRk6iJhn7eUHTKGRP48T5
sXJaBvGexpmnAk6F1JS5J9YOl9hzC4WIF3aiUGSs07Y9zUJGiuTWFcZAZh4bLxV/
mijxGgC6DKZ3CluGLTguEwNHTLaCj2+5rPIvRA5UjHq3du993MppKZChpdUoXWsc
fgVIX/sXQUtRXr+FRvZiahzqzq/akQBnZ7Z481KegEHfCZu70w98moI42sBFQ2HB
j1TT4G4vHkB7OegyD8UTyQPmiuKLoJtWxmjff3ZmJV/nPznO1qihTaQZ20+YAkkE
4wIDAQAB
-----END PUBLIC KEY-----
44 changes: 44 additions & 0 deletions test/signature.test.js
@@ -0,0 +1,44 @@
const test = require('tap').test
const bakery = require('..')
const jws = require('jws')


const ASSERTION = {verify: { url: 'oh sup'}}
const sig = jws.sign({
header: { alg: 'rs256' },
privateKey: file('rsa-private.pem'),
payload: ASSERTION
})

test('baking svg badges with signatures', function (t) {
bakery.bake({
image: file('unbaked.svg'),
signature: sig
}, function (err, baked) {
bakery.debake(baked, function (err, assertion) {
t.same(ASSERTION, assertion, 'assertions should match')
t.end()
})
})
})

test('baking png badges with signatures', function (t) {
bakery.bake({
image: file('unbaked.png'),
signature: sig
}, function (err, baked) {
bakery.debake(baked, function (err, assertion) {
t.same(ASSERTION, assertion, 'assertions should match')
t.end()
})
})
})



function file(name) {
return (
require('fs').readFileSync(
require('path').join(__dirname, name))
)
}

0 comments on commit f959dc1

Please sign in to comment.