Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

server: require signed requests #4

Merged
merged 6 commits into from Dec 7, 2016
Merged

server: require signed requests #4

merged 6 commits into from Dec 7, 2016

Conversation

@ayumi
Copy link
Contributor

ayumi commented Dec 6, 2016

  • Adds request-verifier which verifies signed timestamp in request bodies.
  • Uses request-verifier to verify requests to POST /{userId}/credentials.

auditors: @diracdeltas

@ayumi ayumi force-pushed the feature/verified-requests branch from a28e89c to dff58e2 Dec 6, 2016
@diracdeltas
Copy link
Member

diracdeltas commented Dec 6, 2016

travis is error'ing at /home/travis/build/brave/sync/test/server/users.js:32

return response.status(400).end('Unable to verify the signed request. Please check the signing private key matches the pubkey.')
}

const result = serializer.serializer.byteArrayToString(verifiedBytes)

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 6, 2016

Member

this needs to handle the case where serializer has not finished initializing; it should probably wait 1s and try again

This comment has been minimized.

Copy link
@ayumi

ayumi Dec 6, 2016

Author Contributor

is there a legit way to do this synchronously

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 6, 2016

Member

not easily; i guess you could return a 503 and tell the client to retry the request after some time

if (!body || body.length === 0) {
return response.status(400).end('Signed request body is required.')
}
const publicKey = Buffer.from(userId, 'base64')

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 6, 2016

Member

does this need to be urlsafe base64?

This comment has been minimized.

Copy link
@ayumi

ayumi Dec 6, 2016

Author Contributor

i think it's done automagically – if the corresponding test passes consistently it's probably okay?

This comment has been minimized.

return response.status(400).end('Signed request body is required.')
}
const publicKey = Buffer.from(userId, 'base64')
const verifiedBytes = crypto.verify(Buffer.from(body), publicKey)

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 6, 2016

Member

body is a Buffer, so Buffer.from seems unnecessary here

function signedTimestamp (secretKey, timestamp) {
if (!timestamp) { timestamp = Math.floor(Date.now() / 1000) }
const message = timestamp.toString()
return crypto.sign(serializer.serializer.stringToByteArray(message), secretKey)

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 6, 2016

Member

this needs to ensure that serializer is initialized; i handled this by putting the tests in the serializer.init callback in test/serializer.js

function signedTimestamp (secretKey, timestamp) {
if (!timestamp) { timestamp = Math.floor(Date.now() / 1000) }
const message = timestamp.toString()
return crypto.sign(serializer.serializer.stringToByteArray(message), secretKey)

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 6, 2016

Member

also needs to ensure serializer is initialized

@ayumi ayumi force-pushed the feature/verified-requests branch 2 times, most recently from 2e529ab to 76084e2 Dec 6, 2016
Copy link
Contributor Author

ayumi left a comment

thanks, have updated serializer and tests to wait until it's been initialized.


module.exports.serializer = null

module.exports.initSerializer = function (apiFile/* : string */) {

This comment has been minimized.

Copy link
@ayumi

ayumi Dec 6, 2016

Author Contributor

Updated this to return a Promise

maybe init() above should just do this?

i made initSerializer() so a whole app might share a single serializer instance, since it's kind of #effort to reinit the serializer each time we need it.

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 6, 2016

Member

yeah seems like this should just be a modification to init instead of its own method

return response.status(400).end('Unable to verify the signed request. Please check the signing private key matches the pubkey.')
}

const result = serializer.serializer.byteArrayToString(verifiedBytes)

This comment has been minimized.

Copy link
@ayumi

ayumi Dec 6, 2016

Author Contributor

is there a legit way to do this synchronously

} else {
t.fail(`${t.name} (${response.statusCode}) (${body})`)
}
// TODO: Use protobuf?

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 6, 2016

Member

++ protobuf here

const serializer = require('../../../lib/serializer.js')
const Express = require('express')

test('users router', (t) => {

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 6, 2016

Member

for async tests, i think it is best practice to use t.plan so that the test doesn't abort early if the callback with t.end gets run before some of the other callbacks

@@ -3,90 +3,122 @@ const awsSdk = require('aws-sdk')
const config = require('config')
const crypto = require('../../lib/crypto')
const request = require('request')
const supertest = require('supertest')
const serializer = require('../../lib/serializer.js')
const usersRouter = require('../../server/users.js')
const util = require('../../server/lib/util.js')
const Express = require('express')

test('users router', (t) => {

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 6, 2016

Member

same as above re: t.plan

@diracdeltas
Copy link
Member

diracdeltas commented Dec 6, 2016

sweet tests 🍭

@ayumi ayumi force-pushed the feature/verified-requests branch from c437afe to 1cf97e4 Dec 6, 2016
@ayumi ayumi force-pushed the feature/verified-requests branch from 1cf97e4 to 2f07193 Dec 6, 2016
@ayumi
Copy link
Contributor Author

ayumi commented Dec 7, 2016

have updated with:

  • aws credentials protobuf
  • tape test.plan(n)

@diracdeltas can u pls take another look 🎁

/**
* Deserializes client sync credentials for accessing AWS and S3.
* @param {Uint8Array} bytes
* @returns {{aws: <Uint8Array>, s3Post: <Uint8Array>}}

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 7, 2016

Member

surprised that this deserializes to Uint8Array in the fields instead of the credentials object

This comment has been minimized.

Copy link
@ayumi

ayumi Dec 7, 2016

Author Contributor

oops. typo

Serializer.prototype.credentialsToByteArray = function (credentials) {
return this.api.Credentials.encode({
aws: this.api.Credentials.lookup('Aws').create(credentials.aws),
s3Post: this.api.Credentials.lookup('S3Post').create(credentials.s3Post)

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 7, 2016

Member

does this.api.Credentials.encode(credentials) work here? i think that would avoid the fields being Uint8Arrays instead of the original object types

This comment has been minimized.

Copy link
@ayumi

ayumi Dec 7, 2016

Author Contributor

see: typo comment above regarding return of decode not being UInt8Array

I think encode() for a type with nested types only works when the nested types are legit instances
{
aws: {Credentials.Aws},
s3Post: {Credentials.S3Post}
}

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 7, 2016

Member

so the fields are the actual types instead of Uint8Arrays with this the way it is now? would be good to have a test to prove that

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 7, 2016

Member

if so, lgtm for merging 🎆

@@ -5,16 +5,22 @@ const pb = require('protobufjs')

module.exports.init = function (apiFile/* : string */) {
return pb.load(apiFile || './lib/api.proto').then((r) => {
return new Serializer({
if (this.serializer) { return this.serializer }

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 7, 2016

Member

i'm not sure this line helps actually. https://nodejs.org/api/modules.html#modules_caching suggests that every module import gets the same object, so calling 'init' won't help future imports.

This comment has been minimized.

Copy link
@ayumi

ayumi Dec 7, 2016

Author Contributor

yes, but we've been calling init() which always instantiates new Serializers.

with this return, it caches a single Serializer instance onto the shared cached serializer module and returns it next time we try to init()

This comment has been minimized.

Copy link
@diracdeltas

diracdeltas Dec 7, 2016

Member

i see; i didn't intend multiple calls of init in the same module to be used outside of tests. the intended pattern is module calls init, saves the serializer in its own scope, calls the saved serializer in the future.

not a big deal either way

@ayumi ayumi force-pushed the feature/verified-requests branch from 7f317f4 to afd4f62 Dec 7, 2016
@ayumi ayumi force-pushed the feature/verified-requests branch from afd4f62 to ab2e5c1 Dec 7, 2016
@ayumi ayumi merged commit e2dd84b into master Dec 7, 2016
2 checks passed
2 checks passed
continuous-integration/travis-ci/pr The Travis CI build passed
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details
@ayumi ayumi deleted the feature/verified-requests branch Dec 7, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

2 participants
You can’t perform that action at this time.