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

Define separate SignedStatement & UnsignedStatement classes #179

Merged
merged 2 commits into from
Feb 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions integration-test/push_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const { getTestNodeId } = require('../test/util')
const { MediachainNode: AlephNode } = require('../src/peer/node')
const { concatNodeClient, concatNodePeerInfo } = require('./util')
const { PublisherId } = require('../src/peer/identity')
const { Statement } = require('../src/model/statement')
const { SignedStatement } = require('../src/model/statement')

const TEST_NAMESPACE = 'scratch.push-test'
const UNAUTHORIZED_NAMESPACE = 'scratch.unauthorized-push-test'
Expand Down Expand Up @@ -38,14 +38,14 @@ function preparePartiallyValidStatements (alephNode: AlephNode, numValid: number
.then(([object]) => {
const promises = []
for (let i = 0; i < numValid; i++) {
promises.push(Statement.createSimple(alephNode.publisherId, TEST_NAMESPACE, {
promises.push(SignedStatement.createSimple(alephNode.publisherId, TEST_NAMESPACE, {
object,
refs: [`test:${i.toString()}`]
},
alephNode.statementCounter))
}
// add a statement with an invalid object reference
promises.push(Statement.createSimple(alephNode.publisherId, TEST_NAMESPACE, {
promises.push(SignedStatement.createSimple(alephNode.publisherId, TEST_NAMESPACE, {
object: 'QmNLftPEMzsadpbTsGaVP3haETYJb4GfnCgQiaFj5Red9G', refs: [], deps: [], tags: []
}))
return Promise.all(promises)
Expand Down
181 changes: 113 additions & 68 deletions src/model/statement.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,72 +14,29 @@ class Statement {
namespace: string
timestamp: number
body: StatementBody
signature: Buffer

constructor (stmt: {id: string, publisher: string, namespace: string, timestamp: number, body: StatementBody, signature: Buffer | string}) {
this.id = stmt.id
this.publisher = stmt.publisher
this.namespace = stmt.namespace
this.timestamp = stmt.timestamp
this.body = stmt.body
if (Buffer.isBuffer(stmt.signature)) {
this.signature = (stmt.signature: any)
} else {
this.signature = Buffer.from(stmt.signature, 'base64')
}
}

static create (
publisherId: PublisherId,
namespace: string,
statementBody: StatementBody | StatementBodyMsg,
counter: number = 0,
timestampGenerator: () => number = Date.now)
: Promise<Statement> {
const body = (statementBody instanceof StatementBody) ? statementBody : StatementBody.fromProtobuf(statementBody)
const timestamp = timestampGenerator()
const statementId = [publisherId.id58, timestamp.toString(), counter.toString()].join(':')
const stmt = new Statement({
id: statementId,
publisher: publisherId.id58,
namespace,
timestamp,
body,
signature: Buffer.from('')
})
return stmt.sign(publisherId)
}

static createSimple (
publisherId: PublisherId,
namespace: string,
statementBody: {object: string | Object, refs?: Array<string>, deps?: Array<string>, tags?: Array<string>},
counter: number = 0,
timestampGenerator: () => number = Date.now
): Promise<Statement> {
let body
if (typeof statementBody.object === 'object') {
body = new ExpandedSimpleStatementBody((statementBody: any))
} else {
body = new SimpleStatementBody((statementBody: any))
}
return Statement.create(publisherId, namespace, body, counter, timestampGenerator)
}

static fromProtobuf (msg: StatementMsg): Statement {
const body = StatementBody.fromProtobuf(msg.body)

const {id, publisher, namespace, timestamp, signature} = msg
return new Statement({id, publisher, namespace, timestamp, body, signature})
if (signature == null) {
return new UnsignedStatement({id, publisher, namespace, timestamp, body})
}

return new SignedStatement({id, publisher, namespace, timestamp, body, signature})
}

static fromBytes (msgBuffer: Buffer): Statement {
return Statement.fromProtobuf(pb.stmt.Statement.decode(msgBuffer))
static fromBytes (bytes: Buffer): Statement {
return Statement.fromProtobuf(pb.stmt.Statement.decode(bytes))
}

toProtobuf (): StatementMsg {
const {id, publisher, namespace, timestamp, signature, body} = this
return {id, publisher, namespace, timestamp: timestamp, signature, body: body.toProtobuf()}
const {id, publisher, namespace, timestamp, body} = this
const msg: StatementMsg = {id, publisher, namespace, timestamp: timestamp, body: body.toProtobuf()}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spent a while staring at these before concurring that this is in fact the best way to do it w/o libs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha, yeah. It does look pretty odd :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, I don't know why I did timestamp: timestamp instead of just timestamp - probably some earlier refactor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yeah, that one can get short-handed

if (this instanceof SignedStatement) {
msg.signature = this.signature
}
return msg
}

toBytes (): Buffer {
Expand All @@ -92,9 +49,12 @@ class Statement {

inspect (_depth?: number, opts?: Object) {
opts = Object.assign({}, opts, {depth: null})
const {id, publisher, namespace, timestamp, signature, body} = this
const output = stringifyNestedBuffers({id, publisher, namespace, timestamp, signature, body})
return inspect(output, opts)
const {id, publisher, namespace, timestamp, body} = this
const output: Object = {id, publisher, namespace, timestamp, body}
if (this instanceof SignedStatement) {
output.signature = this.signature
}
return inspect(stringifyNestedBuffers(output), opts)
}

get objectIds (): Array<string> {
Expand All @@ -119,11 +79,30 @@ class Statement {

expandObjects (source: Map<string, Object>): Statement {
const body = this.body.expandObjects(source)
const {id, publisher, namespace, timestamp, signature} = this
return new Statement({id, publisher, namespace, timestamp, body, signature})
const {id, publisher, namespace, timestamp} = this
if (this instanceof SignedStatement) {
return new SignedStatement({ id, publisher, namespace, timestamp, body, signature: this.signature })
}
return new UnsignedStatement({ id, publisher, namespace, timestamp, body })
}

asUnsignedStatement (): UnsignedStatement {
const {id, namespace, publisher, timestamp, body} = this
return new UnsignedStatement({id, namespace, publisher, timestamp, body})
}
}

sign (publisherId: PublisherId): Promise<Statement> {
class UnsignedStatement extends Statement {
constructor (stmt: {id: string, publisher: string, namespace: string, timestamp: number, body: StatementBody}) {
super()
this.id = stmt.id
this.publisher = stmt.publisher
this.namespace = stmt.namespace
this.timestamp = stmt.timestamp
this.body = stmt.body
}

sign (publisherId: PublisherId): Promise<SignedStatement> {
return Promise.resolve().then(() => {
if (publisherId.id58 !== this.publisher) {
throw new Error(`Cannot sign statement, publisher id of signer does not match statement publisher`)
Expand All @@ -132,16 +111,80 @@ class Statement {
return this.calculateSignature(publisherId)
.then(signature => {
const {id, namespace, publisher, timestamp, body} = this
return new Statement({id, namespace, publisher, timestamp, body, signature})
return new SignedStatement({id, namespace, publisher, timestamp, body, signature})
})
})
}

calculateSignature (signer: {sign: (msg: Buffer) => Promise<Buffer>}): Promise<Buffer> {
const msg: Object = this.toProtobuf()
msg.signature = undefined
const bytes = pb.stmt.Statement.encode(msg)
return signer.sign(bytes)
return signer.sign(this.toBytes())
}
}

class SignedStatement extends Statement {
signature: Buffer

constructor (stmt: {id: string, publisher: string, namespace: string, timestamp: number, body: StatementBody, signature: Buffer | string}) {
super()
this.id = stmt.id
this.publisher = stmt.publisher
this.namespace = stmt.namespace
this.timestamp = stmt.timestamp
this.body = stmt.body
if (Buffer.isBuffer(stmt.signature)) {
this.signature = (stmt.signature: any)
} else {
this.signature = Buffer.from(stmt.signature, 'base64')
}
}

static create (
publisherId: PublisherId,
namespace: string,
statementBody: StatementBody | StatementBodyMsg,
counter: number = 0,
timestampGenerator: () => number = Date.now)
: Promise<SignedStatement> {
const body = (statementBody instanceof StatementBody) ? statementBody : StatementBody.fromProtobuf(statementBody)
const timestamp = timestampGenerator()
const statementId = [publisherId.id58, timestamp.toString(), counter.toString()].join(':')
const stmt = new UnsignedStatement({
id: statementId,
publisher: publisherId.id58,
namespace,
timestamp,
body
})
return stmt.sign(publisherId)
}

static createSimple (
publisherId: PublisherId,
namespace: string,
statementBody: {object: string | Object, refs?: Array<string>, deps?: Array<string>, tags?: Array<string>},
counter: number = 0,
timestampGenerator: () => number = Date.now
): Promise<SignedStatement> {
let body
if (typeof statementBody.object === 'object') {
body = new ExpandedSimpleStatementBody((statementBody: any))
} else {
body = new SimpleStatementBody((statementBody: any))
}
return SignedStatement.create(publisherId, namespace, body, counter, timestampGenerator)
}

static fromProtobuf (msg: StatementMsg): SignedStatement {
const body = StatementBody.fromProtobuf(msg.body)

const {id, publisher, namespace, timestamp, signature} = msg
if (signature == null) {
throw new Error(
'SignedStatement.fromProtobuf() requires a non-null signature. ' +
'Use Statement.fromProtobuf() or UnsignedStatement.fromProtobuf() if your message is unsigned.'
)
}
return new SignedStatement({id, publisher, namespace, timestamp, body, signature})
}

verifySignature (publicKey?: ?PublicSigningKey, keyCache?: Map<string, PublicSigningKey>): Promise<boolean> {
Expand Down Expand Up @@ -294,7 +337,7 @@ class EnvelopeStatementBody extends StatementBody {
}

static fromProtobuf (msg: EnvelopeStatementMsg): EnvelopeStatementBody {
return new EnvelopeStatementBody(msg.body.map(stmt => Statement.fromProtobuf(stmt)))
return new EnvelopeStatementBody(msg.body.map(stmt => SignedStatement.fromProtobuf(stmt)))
}

toProtobuf (): StatementBodyMsg {
Expand Down Expand Up @@ -348,6 +391,8 @@ class ExpandedSimpleStatementBody extends SimpleStatementBody {

module.exports = {
Statement,
UnsignedStatement,
SignedStatement,
StatementBody,
SimpleStatementBody,
CompoundStatementBody,
Expand Down
9 changes: 7 additions & 2 deletions src/peer/merge.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const pb = require('../protobuf')
const { protoStreamEncode, protoStreamDecode } = require('./util')
const { flatMap, promiseHash, b58MultihashForBuffer } = require('../common/util')

const { Statement } = require('../model/statement')
const { Statement, SignedStatement } = require('../model/statement')
const { CompoundQueryResultValue } = require('../model/query_result')

import type { MediachainNode } from './node'
Expand Down Expand Up @@ -76,7 +76,12 @@ function mergeFromStreams (
// verify all statements. verification failure causes the whole statement ingestion to fail
// by passing an Error into endQueryStream (as opposed to a string, which will cause a partially
// successful result
Promise.all(statements.map(stmt => stmt.verifySignature(null, publisherKeyCache)))
Promise.all(statements.map(stmt => {
if (!(stmt instanceof SignedStatement)) {
return Promise.reject('Statements must be signed.')
}
return stmt.verifySignature(null, publisherKeyCache)
}))
.catch(err => endQueryStream(err))
.then(results => {
results.forEach((valid, idx) => {
Expand Down
4 changes: 2 additions & 2 deletions src/peer/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const {
const { promiseHash, isB58Multihash } = require('../common/util')
const { pushStatementsToConn } = require('./push')
const { mergeFromStreams } = require('./merge')
const { Statement } = require('../model/statement')
const { Statement, SignedStatement } = require('../model/statement')
const { unpackQueryResultProtobuf } = require('../model/query_result')

import type { QueryResult, QueryResultValue } from '../model/query_result'
Expand Down Expand Up @@ -439,7 +439,7 @@ class MediachainNode {
return this.putData(object)
.then(([objectHash]) => {
const body = {object: objectHash, refs, deps, tags}
return Statement.createSimple(publisherId, namespace, body, this.statementCounter)
return SignedStatement.createSimple(publisherId, namespace, body, this.statementCounter)
})
.then(stmt => this.db.put(stmt)
.then(() => stmt.id))
Expand Down
2 changes: 1 addition & 1 deletion src/protobuf/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export type StatementMsg = {
namespace: string,
body: StatementBodyMsg,
timestamp: number,
signature: Buffer,
signature?: Buffer,
};

// manifest.proto
Expand Down
15 changes: 7 additions & 8 deletions test/metadata/signature_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const assert = require('assert')
const { before, describe, it } = require('mocha')
const path = require('path')

const { Statement } = require('../../src/model/statement')
const { Statement, SignedStatement } = require('../../src/model/statement')
const { PublisherId, PrivateSigningKey } = require('../../src/peer/identity')

const CONCAT_PUBLISHER_ID_PUB58 = '4XTTM4JKrrBeAK6qXmo8FoKmT5RkfjeXfZrnWjJNw9fKvPnEs'
Expand Down Expand Up @@ -40,31 +40,30 @@ describe('Signature verification', () => {
})

it('validates a statement made with makeSimpleStatement helper', () => {
return Statement.createSimple(publisherId, 'scratch.sig-test', {object: 'QmF00123', refs: []})
return SignedStatement.createSimple(publisherId, 'scratch.sig-test', {object: 'QmF00123', refs: []})
.then(stmt => stmt.verifySignature)
.then(valid => {
assert(valid, 'statement did not validate')
})
})

it('signs and validates a manually-constructed statement', () => {
const stmtNoSig = Statement.fromProtobuf({
const unsigned = Statement.fromProtobuf({
id: 'foo',
publisher: publisherId.id58,
namespace: 'scratch.sig-test',
timestamp: Date.now(),
body: {simple: {object: 'QmF00123', refs: [], deps: [], tags: []}},
signature: Buffer.from('')
})
return stmtNoSig.sign(publisherId)
body: {simple: {object: 'QmF00123', refs: [], deps: [], tags: []}}
}).asUnsignedStatement()
return unsigned.sign(publisherId)
.then(signed => signed.verifySignature())
.then(valid => {
assert(valid, 'statement did not validate')
})
})

it('does not validate an altered statement', () => {
Statement.createSimple(publisherId, 'scratch.sig-test', {object: 'QmF00123', refs: []})
SignedStatement.createSimple(publisherId, 'scratch.sig-test', {object: 'QmF00123', refs: []})
.then(stmt => {
stmt.namespace = 'scratch.new-namespace'
return stmt
Expand Down
Loading