WIP. 7 tests failing. Refactor Pigeon::Draft / ::Message

Pigeon::Message was holding on to too many drafting responsibilities.
It also was possible for the local identity to inadvertantly author
malformed messages. This is because local messages were not passed
through the ::Lexer / ::Parser and thereby did not receive the same
scrutiny of remote messages.

To avoid security problems later, and for additional security,
I will only allow messages to be saved *after* passing through the
lexer/parser. This means moving much of Pigeon::Message's logic
into Pigeon::Draft.
This commit is contained in:
Netscape Navigator 2020-04-07 08:58:53 -05:00
parent d76f72ed88
commit a6a8634cab
6 changed files with 45 additions and 40 deletions

View File

@ -50,6 +50,7 @@ Eg: `pigeon identity show` becomes `./pigeon-cli show`.
- [X] Fix diagram in spec doc - [X] Fix diagram in spec doc
- [X] refactor `Bundle.create` to use `message find-all`. - [X] refactor `Bundle.create` to use `message find-all`.
- [X] Rename `message find` to `message read`, since other finders return a multihash. - [X] Rename `message find` to `message read`, since other finders return a multihash.
- [ ] Message.ingest should be the only code path to message authoring.
- [ ] Don't allow any type of whitespace in `kind` or `string` keys. Write a test for this. - [ ] Don't allow any type of whitespace in `kind` or `string` keys. Write a test for this.
- [ ] Add Lipmaa links like the Bamboo folks do. - [ ] Add Lipmaa links like the Bamboo folks do.
- [ ] Create regexes in ::Lexer using strings and Regexp.new() for cleaner regexes. - [ ] Create regexes in ::Lexer using strings and Regexp.new() for cleaner regexes.

2
dist/pigeon.rb vendored
View File

@ -111,7 +111,7 @@ module Pigeon
def self.create_message(kind, params) def self.create_message(kind, params)
draft = Pigeon::Draft.create(kind: kind) draft = Pigeon::Draft.create(kind: kind)
params.map { |(k, v)| draft[k] = v } params.map { |(k, v)| draft[k] = v }
Pigeon::Message.publish(draft) draft.publish
end end
def self.verify_string(identity, string_signature, string) def self.verify_string(identity, string_signature, string)

31
dist/pigeon/draft.rb vendored
View File

@ -2,7 +2,8 @@ require "digest"
module Pigeon module Pigeon
class Draft class Draft
attr_reader :kind, :body, :internal_id attr_reader :signature, :prev, :kind, :internal_id,
:depth, :body, :author
def self.create(kind:, body: {}) def self.create(kind:, body: {})
self.new(kind: kind, body: body).save self.new(kind: kind, body: body).save
@ -23,8 +24,12 @@ module Pigeon
end end
def initialize(kind:, body: {}) def initialize(kind:, body: {})
@signature = Pigeon::EMPTY_MESSAGE
@prev = Pigeon::EMPTY_MESSAGE
@kind = kind @kind = kind
@depth = -1
@body = body @body = body
@author = Pigeon::EMPTY_MESSAGE
@internal_id = SecureRandom.uuid @internal_id = SecureRandom.uuid
end end
@ -49,12 +54,36 @@ module Pigeon
end end
def save def save
puts "Rename to `save_as_draft` to avoid confusion"
Pigeon::Storage.current.set_config(CURRENT_DRAFT, self) Pigeon::Storage.current.set_config(CURRENT_DRAFT, self)
self self
end end
# Author a new message.
def publish
count = store.get_message_count_for(author) - 1
template = MessageSerializer.new(self)
@author = LocalIdentity.current
@prev = store.get_message_by_depth(author.multihash, count)
@depth = store.get_message_count_for(author.multihash)
@signature = author.sign(template.render_without_signature)
candidate = template.render
tokens = Lexer.tokenize(candidate)
message = Parser.parse(tokens)[0]
self.discard
message
end
def render def render
puts "Rename to `render_as_draft` to avoid confusion."
puts "Do we even need DraftSerializer any more?"
DraftSerializer.new(self).render DraftSerializer.new(self).render
end end
def store
Pigeon::Storage.current
end
end end
end end

View File

@ -7,23 +7,6 @@ module Pigeon
class VerificationError < StandardError; end class VerificationError < StandardError; end
VERFIY_ERROR = "Expected field `%s` to equal %s, got: %s" VERFIY_ERROR = "Expected field `%s` to equal %s, got: %s"
# Author a new message.
def self.publish(draft)
author = LocalIdentity.current
depth = Pigeon::Storage
.current
.get_message_count_for(author.multihash)
count = store.get_message_count_for(author.multihash)
prev = store.get_message_by_depth(author.multihash, count - 1)
msg = self.new(author: author,
kind: draft.kind,
body: draft.body,
depth: depth,
prev: prev)
msg.save!
draft.discard
msg
end
# Store a message that someone (not the LocalIdentity) # Store a message that someone (not the LocalIdentity)
# has authored. # has authored.
@ -41,13 +24,15 @@ module Pigeon
end end
def multihash def multihash
sha256 = Helpers.b32_encode(Digest::SHA256.digest(self.render)) tpl = self.render
digest = Digest::SHA256.digest(tpl)
sha256 = Helpers.b32_encode(digest)
"#{MESSAGE_SIGIL}#{sha256}#{BLOB_FOOTER}" "#{MESSAGE_SIGIL}#{sha256}#{BLOB_FOOTER}"
end end
def save! def save!
puts "TODO: Make this method private."
return store.read_message(multihash) if store.message?(multihash) return store.read_message(multihash) if store.message?(multihash)
calculate_signature
verify_depth_prev_and_depth verify_depth_prev_and_depth
verify_signature verify_signature
self.freeze self.freeze
@ -86,22 +71,12 @@ module Pigeon
@signature = signature @signature = signature
end end
def calculate_signature
return if @signature
#TODO: Verify that the author is Pigeon::LocalIdentity.current?
@signature = author.sign(template.render_without_signature)
end
def template def template
MessageSerializer.new(self) MessageSerializer.new(self)
end end
def self.store
Pigeon::Storage.current
end
def store def store
self.class.store Pigeon::Storage.current
end end
end end
end end

View File

@ -108,7 +108,7 @@ RSpec.describe Pigeon::Lexer do
let(:message) do let(:message) do
draft = Pigeon::Draft.create(kind: "unit_test") draft = Pigeon::Draft.create(kind: "unit_test")
draft["foo"] = "bar" draft["foo"] = "bar"
Pigeon::Message.publish(draft) draft.publish
end end
before(:each) do before(:each) do

View File

@ -14,7 +14,7 @@ RSpec.describe Pigeon::Message do
def create_message(params) def create_message(params)
draft = create_draft(params) draft = create_draft(params)
Pigeon::Message.publish(draft) draft.publish
end end
let(:draft) do let(:draft) do
@ -29,13 +29,13 @@ RSpec.describe Pigeon::Message do
it "discards a draft after signing" do it "discards a draft after signing" do
expect(draft.internal_id).to eq(Pigeon::Draft.current.internal_id) expect(draft.internal_id).to eq(Pigeon::Draft.current.internal_id)
Pigeon::Message.publish(draft) draft.publish
expect { Pigeon::Draft.current }.to raise_error("NO DRAFT FOUND") expect { Pigeon::Draft.current }.to raise_error("NO DRAFT FOUND")
end end
it "creates a single message" do it "creates a single message" do
message = Pigeon::Message.publish(draft) message = draft.publish
expect(message.author).to eq(Pigeon::LocalIdentity.current) expect(message.author.multihash).to eq(Pigeon::LocalIdentity.current.multihash)
expect(message.body).to eq(draft.body) expect(message.body).to eq(draft.body)
expect(message.depth).to eq(0) expect(message.depth).to eq(0)
expect(message.kind).to eq("unit_test") expect(message.kind).to eq("unit_test")
@ -64,7 +64,7 @@ RSpec.describe Pigeon::Message do
0.upto(4) do |expected_depth| 0.upto(4) do |expected_depth|
draft1 = Pigeon::Draft.create(kind: "unit_test") draft1 = Pigeon::Draft.create(kind: "unit_test")
draft1["description"] = "Message number #{expected_depth}" draft1["description"] = "Message number #{expected_depth}"
message = Pigeon::Message.publish(draft1) message = draft1.publish
all.push(message) all.push(message)
expect(message.depth).to eq(expected_depth) expect(message.depth).to eq(expected_depth)
if expected_depth == 0 if expected_depth == 0
@ -143,7 +143,7 @@ RSpec.describe Pigeon::Message do
kind[rand(0...8)] = n kind[rand(0...8)] = n
draft = Pigeon::Draft.create(kind: kind) draft = Pigeon::Draft.create(kind: kind)
draft["body"] = "empty" draft["body"] = "empty"
tpl = Pigeon::Message.publish(draft).render tpl = draft.publish.render
boom = ->() { Pigeon::Lexer.tokenize(tpl) } boom = ->() { Pigeon::Lexer.tokenize(tpl) }
expect(boom).to raise_error(Pigeon::Lexer::LexError) expect(boom).to raise_error(Pigeon::Lexer::LexError)
end end
@ -155,7 +155,7 @@ RSpec.describe Pigeon::Message do
key = SecureRandom.alphanumeric(8) key = SecureRandom.alphanumeric(8)
key[rand(0...8)] = n key[rand(0...8)] = n
draft[key] = "should crash" draft[key] = "should crash"
tpl = Pigeon::Message.publish(draft).render tpl = draft.publish.render
boom = ->() { Pigeon::Lexer.tokenize(tpl) } boom = ->() { Pigeon::Lexer.tokenize(tpl) }
expect(boom).to raise_error(Pigeon::Lexer::LexError) expect(boom).to raise_error(Pigeon::Lexer::LexError)
end end