Rust implementation of Crate Transparency using Google Trillian
- 8 minutes read - 1690 wordsI’ve been hacking on a Rust-based transparent application for Google Trillian. As appears to be my fixation, this personality is for another package manager. This time, Rust’s Crates often found in crates.io
which is Rust’s Package Registry. I discussed this project earlier this month Rust Crate Transparency && Rust SDK for Google Trillian and and earlier approach for Python’s packages with pypi-transparency.
This time, of course, I’m using Rust. And, by way of a first for me, for the gRPC server implementation (aka “personality”). I’ve been lazy thanks to the excellent gRPCurl and have been using it way of a client. Because I’m more familiar with Golang and because I’ve written (most) other Trillian personalities in Golang, I resorted to quickly implementing Crate Transparency in Golang too in order to uncover bugs with the Rust implementation. I’ll write a follow-up post on the complexity I seem to struggle with when using protobufs and gRPC [in Golang].
The Solution
The Rust gRPC server:
Starting: gRPC Listener [50051]
[CrateTransparencyImpl::add_package] Entered
[CrateTransparencyImpl::add_package] package {name: "grpc-0.6.1.crate" version: "0.6.1" filename: "/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/grpc-0.6.1.crate" sha256: "\216S\016\367\211J\020J\034\205%\316hx{4\221\357\242\t\214\345\345EN\203$\352x\2115H"}
[CrateTransparencyImpl::add_package] Create QueueLeafRequest
[CrateTransparencyImpl::add_package] queue_leaf
[CrateTransparencyImpl::add_package] wait
Ok((Metadata { entries: [MetadataEntry { key: MetadataKey { name: "content-type" }, value: b"application/grpc" }] }, queued_leaf {leaf {merkle_leaf_hash: "\034\364&.\t\200\030r\177\221z\307\206\216cj\214j\316\255hU\016\302\362\356X\004\332v,_" leaf_value: "\n\020grpc-0.6.1.crate\022\0050.6.1\032R/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/grpc-0.6.1.crate* \216S\016\367\211J\020J\034\205%\316hx{4\221\357\242\t\214\345\345EN\203$\352x\2115H" leaf_identity_hash: "\216S\016\367\211J\020J\034\205%\316hx{4\221\357\242\t\214\345\345EN\203$\352x\2115H" queue_timestamp {seconds: 1588189298 nanos: 559714612}}}, Metadata { entries: [] }))
[CrateTransparencyImpl::add_package] done
[CrateTransparencyImpl::add_package] Completed
[CrateTransparencyImpl::add_package] Entered
[CrateTransparencyImpl::add_package] package {name: "grpc-0.6.2.crate" version: "0.6.2" filename: "/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/grpc-0.6.2.crate" sha256: "*\257\035t\037\346\363A?\037\237q\271\237^N&wmV4u\250\245<\345:s\250SL\035"}
[CrateTransparencyImpl::add_package] Create QueueLeafRequest
[CrateTransparencyImpl::add_package] queue_leaf
[CrateTransparencyImpl::add_package] wait
Ok((Metadata { entries: [MetadataEntry { key: MetadataKey { name: "content-type" }, value: b"application/grpc" }] }, queued_leaf {leaf {merkle_leaf_hash: "\264#]\316\216\036W~=\t\214\034\271&\276c\246?K\3427\262\224%u\246\306\273\017\371v\266" leaf_value: "\n\020grpc-0.6.2.crate\022\0050.6.2\032R/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/grpc-0.6.2.crate* *\257\035t\037\346\363A?\037\237q\271\237^N&wmV4u\250\245<\345:s\250SL\035" leaf_identity_hash: "*\257\035t\037\346\363A?\037\237q\271\237^N&wmV4u\250\245<\345:s\250SL\035" queue_timestamp {seconds: 1588189298 nanos: 623131819}}}, Metadata { entries: [] }))
[CrateTransparencyImpl::add_package] done
[CrateTransparencyImpl::add_package] Completed
[CrateTransparencyImpl::add_package] Entered
[CrateTransparencyImpl::add_package] package {name: "protobuf-1.7.5.crate" version: "1.7.5" filename: "/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/protobuf-1.7.5.crate" sha256: "\341L\315ky\354t\204\022\324\362\337\336\032\200\3726:g\336\364\006)i\370\256\323\327\220\243\017("}
[CrateTransparencyImpl::add_package] Create QueueLeafRequest
[CrateTransparencyImpl::add_package] queue_leaf
[CrateTransparencyImpl::add_package] wait
Ok((Metadata { entries: [MetadataEntry { key: MetadataKey { name: "content-type" }, value: b"application/grpc" }] }, queued_leaf {leaf {merkle_leaf_hash: "\343\3758\251,\331\006\237D\\[3k\276\025\006\307\333\372\301K\343\247\236O\246\251e\264\205\371\257" leaf_value: "\n\024protobuf-1.7.5.crate\022\0051.7.5\032V/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/protobuf-1.7.5.crate* \341L\315ky\354t\204\022\324\362\337\336\032\200\3726:g\336\364\006)i\370\256\323\327\220\243\017(" leaf_identity_hash: "\341L\315ky\354t\204\022\324\362\337\336\032\200\3726:g\336\364\006)i\370\256\323\327\220\243\017(" queue_timestamp {seconds: 1588189298 nanos: 640144786}}}, Metadata { entries: [] }))
[CrateTransparencyImpl::add_package] done
[CrateTransparencyImpl::add_package] Completed
[CrateTransparencyImpl::add_package] Entered
[CrateTransparencyImpl::add_package] package {name: "protobuf-2.8.0.crate" version: "2.8.0" filename: "/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/protobuf-2.8.0.crate" sha256: "\212\357\316\311\361B\265$\331\217\310\035\007\202wC\276\211\335e\206\241\272j\262\037\246jP\013?\245"}
[CrateTransparencyImpl::add_package] Create QueueLeafRequest
[CrateTransparencyImpl::add_package] queue_leaf
[CrateTransparencyImpl::add_package] wait
Ok((Metadata { entries: [MetadataEntry { key: MetadataKey { name: "content-type" }, value: b"application/grpc" }] }, queued_leaf {leaf {merkle_leaf_hash: "$\277}\002$\324\n\032~\021vf\325$\260\375\354\205p_\331PMZ\304\305P\250%<Sw" leaf_value: "\n\024protobuf-2.8.0.crate\022\0052.8.0\032V/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/protobuf-2.8.0.crate* \212\357\316\311\361B\265$\331\217\310\035\007\202wC\276\211\335e\206\241\272j\262\037\246jP\013?\245" leaf_identity_hash: "\212\357\316\311\361B\265$\331\217\310\035\007\202wC\276\211\335e\206\241\272j\262\037\246jP\013?\245" queue_timestamp {seconds: 1588189298 nanos: 655852669}}}, Metadata { entries: [] }))
[CrateTransparencyImpl::add_package] done
[CrateTransparencyImpl::add_package] Completed
The gRPCurl client:
./packages.sh \
| while read in
do
echo ${in}
echo ${in} \
| grpcurl \
-plaintext \
--import-path=protos \
--proto=protos/crate_transparency.proto \
--proto=protos/package.proto \
-d '@' \
localhost:50051 crate_transparency.v1.CrateTransparency.AddPackage
done
{"package": { "name": "grpc-0.6.1.crate", "version": "0.6.1", "filename":"/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/grpc-0.6.1.crate", "url":"", "sha256":"jlMO94lKEEochSXOaHh7NJHvogmM5eVFToMk6niJNUg=" } }
{
"ok": true
}
{"package": { "name": "grpc-0.6.2.crate", "version": "0.6.2", "filename":"/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/grpc-0.6.2.crate", "url":"", "sha256":"Kq8ddB/m80E/H59xuZ9eTiZ3bVY0dailPOU6c6hTTB0=" } }
{
"ok": true
}
{"package": { "name": "protobuf-1.7.5.crate", "version": "1.7.5", "filename":"/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/protobuf-1.7.5.crate", "url":"", "sha256":"4UzNa3nsdIQS1PLf3hqA+jY6Z970Bilp+K7T15CjDyg=" } }
{
"ok": true
}
{"package": { "name": "protobuf-2.8.0.crate", "version": "2.8.0", "filename":"/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/protobuf-2.8.0.crate", "url":"", "sha256":"iu/OyfFCtSTZj8gdB4J3Q76J3WWGobpqsh+malALP6U=" } }
{
"ok": true
}
{"package": { "name": "protobuf-2.8.1.crate", "version": "2.8.1", "filename":"/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/protobuf-2.8.1.crate", "url":"", "sha256":"QDYYNt791Ycf9+hAlsb2REr3/BV/jvF4n1TxR2h8qiA=" } }
{
"ok": true
}
{"package": { "name": "protobuf-2.8.2.crate", "version": "2.8.2", "filename":"/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/protobuf-2.8.2.crate", "url":"", "sha256":"cHMYUu7HLFbREibIpflq1QWKPatzZHyl9+41HkZPJXE=" } }
{
"ok": true
}
{"package": { "name": "protobuf-2.12.0.crate", "version": "2.12.0", "filename":"/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/protobuf-2.12.0.crate", "url":"", "sha256":"cZZPNP1RzwSILXrjMl+geU1MrWagPQAD842K5PY7oSY=" } }
{
"ok": true
}
{"package": { "name": "protoc-2.8.1.crate", "version": "2.8.1", "filename":"/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/protoc-2.8.1.crate", "url":"", "sha256":"OZjEvAr4zL08xoJF7p9yZjxa4vt4vEj/dxmu8RVi7eo=" } }
{
"ok": true
}
{"package": { "name": "protoc-2.8.2.crate", "version": "2.8.2", "filename":"/${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/protoc-2.8.2.crate", "url":"", "sha256":"UNlQDqFIimGqltoTkDm3ipLu9koPPILTgXNynwrXPPg=" } }
{
"ok": true
}
For brevity, I’ll omit the output from the equivalent client scripting running GetPackage
.
For testing, I used the Golang server’s AddPackage
with the Rust server’s GetPackage
and vice versa to prove (to myself) that it works correctly.
I’ve taken to using Adminer in my deployments (Kubernetes and Docker Compose) as this provides a convenient way to interrogate the MySQL database underlying Trillian. Here’s the content of Trillian’s SequencedLeafData
table after the adds:
Or:
TreeId | LeafIdentityHash | MerkleLeafHash |
---|---|---|
2878187571004555676 | 8E530EF7894A104A1C8525CE68787B3491EFA2098CE5E5454E8324EA78893548 | 1CF4262E098018727F917AC7868E636A8C6ACEAD68550EC2F2EE5804DA762C5F |
2878187571004555676 | 2AAF1D741FE6F3413F1F9F71B99F5E4E26776D563475A8A53CE53A73A8534C1D | B4235DCE8E1E577E3D098C1CB926BE63A63F4BE237B2942575A6C6BB0FF976B6 |
2878187571004555676 | E14CCD6B79EC748412D4F2DFDE1A80FA363A67DEF4062969F8AED3D790A30F28 | E3FD38A92CD9069F445C5B336BBE1506C7DBFAC14BE3A79E4FA6A965B485F9AF |
2878187571004555676 | 8AEFCEC9F142B524D98FC81D07827743BE89DD6586A1BA6AB21FA66A500B3FA5 | 24BF7D0224D40A1A7E117666D524B0FDEC85705FD9504D5AC4C550A8253C5377 |
2878187571004555676 | 40361836DEFDD5871FF7E84096C6F6444AF7FC157F8EF1789F54F147687CAA20 | 0C5B6C4AAB7530A9BD62A05D386D2D2EB0D1B26EFA09A151F46F7BD9A98E754A |
2878187571004555676 | 70731852EEC72C56D11226C8A5F96AD5058A3DAB73647CA5F7EE351E464F2571 | BDC16E3B66F21EB4716C56511277926EB0A8752B3CB0ABBFA353DA13CA4F0EB3 |
2878187571004555676 | 71964F34FD51CF04882D7AE3325FA0794D4CAD66A03D0003F38D8AE4F63BA126 | 05EB079DD4F44869991813F57FA98A2E6CA6A9BD430F06F644964E18597FC3E9 |
2878187571004555676 | 3998C4BC0AF8CCBD3CC68245EE9F72663C5AE2FB78BC48FF7719AEF11562EDEA | 9219736E509B0FE1C6D656407E6828B90D50FA2E5F1B755CFA0567D1872A9B16 |
2878187571004555676 | 50D9500EA1488A61AA96DA139039B78A92EEF64A0F3C82D38173729F0AD73CF8 | 240170616A9D18D13DDEA042316C3499C66E837DAC024B65487BF7E3DAAEC6CE |
To show working, let’s pick one of these crates: protobuf=2.12.0
This crate is cached in ${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/protobuf-2.12.0.crate
This crate’s SHA256 hash is:
DIGEST=$(cat ${HOME}/.cargo/registry/cache/github.com-1ecc6299db9ec823/protobuf-2.12.0.crate | sha256sum | head --bytes=64) && echo ${DIGEST}
71964f34fd51cf04882d7ae3325fa0794d4cad66a03d0003f38d8ae4f63ba126
NB if you eyeball the Trillian
SequencedLeafData
table above, you should be able to findLeafIdentityHash
value matching the above
NB I’m explicitly setting the
LeafIdentityHash
to be the Crate’s SHA-256 hash as this value represents a unique identifier of the Crate.
The definition for package.proto
is:
message Package {
string name = 1;
string version = 2;
string filename = 3;
string url = 4;
bytes sha256 = 5;
}
NB
sha256
is bytes (not string)
We can convert the SHA256 digest into bytes and then base-64 encode it:
BASE64=$(printf ${DIGEST} | xxd -r -p | base64 --wrap=0)) && echo ${BASE64}
cZZPNP1RzwSILXrjMl+geU1MrWagPQAD842K5PY7oSY=
It is this ${BASE64}
value that we use with gRPCurl to pass the sha256
value (as bytes) using gRPCurl:
echo "
{
\"package\": {
\"name\":\"protobuf\",
\"version\":\"2.12.0\",
\"filename\":\"\",
\"url\":\"\",
\"sha256\": \"${BASE64}\"
}
}" |\
grpcurl \
-plaintext \
--import-path=protos \
--proto=protos/crate_transparency.proto \
--proto=protos/package.proto \
-d '@' \
localhost:50051 crate_transparency.v1.CrateTransparency.AddPackage
NB Apologies for the ugly escaping, this is to permit the
${BASE64}
substitution
Protobufs
This is a work in progress but, my first take is 3 methods AddPackage
, GetPackage
and GetInclusionProof
. The latter (the most complex) is not yet implemented by the Rust server.
syntax = "proto3";
package crate_transparency.v1;
service CrateTransparency {
rpc AddPackage(AddPackageRequest) returns (AddPackageResponse) {};
rpc GetPackage(GetPackageRequest) returns (GetPackageResponse) {};
rpc GetInclusionProof(GetInclusionProofRequest) returns (GetInclusionProofResponse) {};
}
message AddPackageRequest {
Package package = 1;
}
message AddPackageResponse {
bool ok = 1;
}
message GetPackageRequest {
Package package = 1;
}
message GetPackageResponse {
bool ok = 1;
}
message GetInclusionProofRequest {
Package package = 1;
}
message GetInclusionProofResponse {
bool ok = 1;
}
message Package {
string name = 1;
string version = 2;
string filename = 3;
string url = 4;
bytes sha256 = 5;
}
NB For Golang, the
option
is becoming mandatory and I learned that it’s best to use it particularly if you’re using modules to manage the output location of the compiler:option go_package = "github.com/DazWilkin/crate-transparency/protos";
Server
I can’t tell you that code flew from my mind into Visual Studio Code but, having used Stepan Koltsov’s excellent rust-protobuf
and grpc-rust
crates previously, the process was not as complicated as I’d anticipated.
NB One thing that Golang does better than Rust is that Golang modules are uniquely identified by repo, so e.g.
github.com/google/trillian
. The aboverust-protobuf
repo becomes a Crate calledprotobuf
. It’s only by checking the ‘Repository’ link on the crate.io page that this mapping is evident. It’s not just Rust, Python has the same issue with PyPi. It’s the little things!
In further disclosure, I’m a Rust noob and working my way through two Rust programming books so, I’d appreciate coding feedback and apologize if I mangling the language.
One thing I really like with rust-protobuf
is that it’s straightforward and convenient to use it to compile Rust sources from protos:
fn main() {
protoc_rust_grpc::run(protoc_rust_grpc::Args {
out_dir: "./src/protos",
includes: &["protos"],
input: &[
"protos/crate_transparency.proto",
"protos/package.proto",
],
rust_protobuf: true,
..Default::default()
})
.expect("protoc-rust-grpc");
}
NB One thing I often forget, is that this is, of course, dependent on
protoc
and the compiler must be in the path before this will work.
NB I remain unclear on the best practice for locating protoc compiled sources but I’ve been using
./protos
in both Golang and Rust (mostly) without issue.
As we expect, we must implement the methods defined by the proto.
The only nuance here is that the struct (CrateTransparencyImpl
) that implements CrateTransparency
use ::protos::{
crate_transparency::{
AddPackageRequest, AddPackageResponse,
GetPackageRequest, GetPackageResponse,
GetInclusionProofRequest, GetInclusionProofResponse,
},
crate_transparency_grpc::CrateTransparency,
};
pub struct CrateTransparencyImpl {
client: TrillianLogClient,
log_id: i64,
}
impl CrateTransparencyImpl {
pub fn new(
client: TrillianLogClient,
log_id: i64,
) -> CrateTransparencyImpl {
CrateTransparencyImpl { client, log_id }
}
}
impl CrateTransparency for CrateTransparencyImpl {
fn add_package(
&self,
_: RequestOptions,
rqst: AddPackageRequest,
) -> SingleResponse<AddPackageResponse> {}
fn get_package(
&self,
_: RequestOptions,
rqst: GetPackageRequest,
) -> SingleResponse<GetPackageResponse> {}
fn get_inclusion_proof(
&self,
_: RequestOptions,
rqst: GetInclusionProofRequest,
) -> SingleResponse<GetInclusionProofResponse> {}
}
This is where most of my hacking came to the fore and, in truth, the code herein requires SinglePointerField
and RepeatedField
and SingleResponse
which I continue to find confusing.
AddPackage
and GetPackage
are similar and straightforward. In both cases we need to marshal the Package
(!) data, we can do this using write_to_bytes()
:
let package = rqst.get_package();
let leaf_value = package
.write_to_bytes()
.expect("Unable to marshall `Package` to bytes");
For AddPackage
, we then construct a LogLeaf
using the leaf_value
and we explicity set the leaf_identity_hash
to be value of the Crate file’s SHA-256 hash (this was provided by the client in sha256
). We create QueueLeafRequest
using the LogLeaf
and the Trillian Log ID (log_id
) and we ship it to Trillian using queue_leaf
.
let leaf_identity_hash = package.get_sha256().to_vec();
let leaf = LogLeaf {
leaf_value: leaf_value,
leaf_identity_hash: leaf_identity_hash,
..Default::default()
};
let r = QueueLeafRequest {
log_id: self.log_id,
leaf: ::protobuf::SingularPtrField::some(leaf),
..Default::default()
};
let rsp = self.client.queue_leaf(
::grpc::RequestOptions {
..Default::default()
},
r,
);
rsp.wait();
For GetPackage
, we need to create a leaf_hash
to construct a GetLeavesByHashRequest
used by get_leaves_by_hash
.
However, there are two wrinkles here:
First, Trillian uses RFC6962: Certificate Transparency. This includes a tweaked mechanism to hash values. The mechanism is based on SHA-256 but, for our purposes, we must prefix the Vec<u8>
corresponding to the leaf_value
with 0x00
.
Second, leaf_hash
is of type repeated bytes
and so we must take the Vec<u8>
representing the hashed leaf_value
and put this in another vector (Vec<Vec<u8>>
).
let mut v: Vec<u8> = vec![0x00];
v.append(&mut leaf_value);
let mut hasher = Sha256::new();
hasher.input(v);
let d = hasher.result();
let v = d.to_vec();
let vv = vec![v];
let leaf_hash = protobuf::RepeatedField::from_vec(vv);
NB As with the Golang (SDK), this method should be refactored into something permitting:
hasher = RFC6962::new()
.
Then we’re ready to create the GetLeavesByHashRequest
, make the request (get_leaves_by_hash
) to Trillian and deal with the results.
In this case, we expect zero (not found) or one (found) leaves to be returned.
Now that we can AddPackage
and GetPackage
, the basic functionality is available and the overview tests, shown at the top of this post, may be run.