capnproto / capnproto-rust

Cap'n Proto for Rust

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Canonicalize message on receive

sanderpick opened this issue · comments

We have a client written in Go that is sending a struct and a signature to a server that uses capnproto-rust:

interface Publication {
	push @0 (tx :Tx, signature :Data);
}

In order to create the signature, we're doing something like this in Go:

func (s *Streamer) sign(tx streamercapnp.Tx) ([]byte, error) {
	bytes, err := capnp.Canonicalize(tx.ToPtr().Struct())
	if err != nil {
		return []byte{}, fmt.Errorf("canonicalize: %s", err)
	}

	hash := sha256.Sum256(bytes)
	signature, err := crypto.Sign(hash[:], s.privateKey)
	if err != nil {
		return []byte{}, fmt.Errorf("sign: %s", err)
	}

	return signature, nil
}

Now, on the Rust side we need to verify the signature. My thought was that we'd need to canonicalize Tx to get the signature payload. I see canonicalize test (https://github.com/capnproto/capnproto-rust/blob/master/capnp/tests/canonicalize.rs), but having a hard time understanding how I can do that in the server's push implementation. eg,

fn push(
    &mut self,
    params: publication::PushParams,
    mut results: publication::PushResults,
) -> Promise<(), Error> {
    let args = pry!(params.get());
    let txn = args.get_tx().unwrap();
    let sig = args.get_signature().unwrap();

    // how to get canonical Tx raw bytes? 
    
    Promise::ok(())
}

Thank you!

The usual thing is to use the message::Reader::canonicalize() method:

pub fn canonicalize(&self) -> Result<Vec<crate::Word>> {
let root = self.get_root_internal()?;
let size = root.target_size()?.word_count + 1;
let mut message = Builder::new(HeapAllocator::new().first_segment_words(size as u32));
message.set_root_canonical(root)?;
let output_segments = message.get_segments_for_output();
assert_eq!(1, output_segments.len());
let output = output_segments[0];
assert!((output.len() / BYTES_PER_WORD) as u64 <= size);
let mut result = crate::Word::allocate_zeroed_vec(output.len() / BYTES_PER_WORD);
crate::Word::words_to_bytes_mut(&mut result[..]).copy_from_slice(output);
Ok(result)

But that only works if your data-to-canonicalize is the root struct of a message::Reader, which is not the case for you.

You need to do something like this:

        let size = txn.total_size()?.word_count;
        let mut message = capnp::message::Builder::new(capnp::message::HeapAllocator::new().first_segment_words(size as u32));
        message.set_root_canonical(root)?;
        let output_segments = message.get_segments_for_output();
        assert_eq!(1, output_segments.len());
        let output = output_segments[0];
        assert!((output.len() / BYTES_PER_WORD) as u64 <= size);
        let mut result = crate::Word::allocate_zeroed_vec(output.len() / BYTES_PER_WORD);
        crate::Word::words_to_bytes_mut(&mut result[..]).copy_from_slice(output);

Probably we should add a function to the capnp crate that does this.

Thanks for the quick reply!

In my case, message.set_root_canonical(root)?; would be message.set_root_canonical(txn)?;? I get a panic with that at this assert: https://github.com/capnproto/capnproto-rust/blob/master/capnp/src/message.rs#L507. There are 2 segments, not 1. Perhaps an issue with the client. We are using SingleSegment to build Tx on the Go side.

In my case, message.set_root_canonical(root)?; would be message.set_root_canonical(txn)?;?
yes.

I get a panic with that at this assert:

What if you do

let size = txn.total_size()?.word_count + 1;

instead?

I thought that maybe the + 1 from canonicalize wasn't needed, but maybe it is.

That works! Thank you 🍻 Happy to try a PR for a built-in method if you want.

Happy to try a PR for a built-in method if you want.

Up to you. I don't intend to implement it myself in the near future.