-
Notifications
You must be signed in to change notification settings - Fork 381
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
TLS LogRootV1 Format #1037
TLS LogRootV1 Format #1037
Conversation
8283200
to
3dd45fd
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW, any Log-specific comments usually apply to the Map- variants too
crypto/data_formats.go
Outdated
func ParseLogRoot(b []byte) (*LogRootV1, error) { | ||
// Verify version | ||
version := binary.BigEndian.Uint16(b) | ||
if got, want := version, uint16(trillian.LogRootFormat_LOG_ROOT_FORMAT_V1); got != want { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I'm not a big fan of using the got, want:=...
thing outside of unit tests, unless there's a calculation to avoid repeating -- I think it's easier to see what's going on with:
if version != uint16(trillian.LogRootFormat_LOG_ROOT_FORMAT_V1) {
(But I know other opinions vary...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
crypto/data_formats.go
Outdated
Metadata []byte `tls:"minlen:0,maxlen:65535"` | ||
} | ||
|
||
// ParseLogRoot returns a *SignedLogRootV1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment is incorrect; also, it doesn't really add any extra information over and above the method signature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the comment to
ParseLogRoot verifies that b has the LOG_ROOT_FORMAT_V1 tag and returns a *LogRootV1
crypto/data_formats.go
Outdated
TimestampNanos uint64 | ||
Revision uint64 | ||
Metadata []byte `tls:"minlen:0,maxlen:65535"` | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need LogID
in the structure too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We discussed including the LogID
in the struct, but decided against it in order to prevent a malicious log from falsely representing it's LogID
. Clients should derive the LogID from the valid signature associated with SignedLogRoot
crypto/data_formats.go
Outdated
|
||
// LogRootV1 contains the fields verified by SignedLogRoot | ||
type LogRootV1 struct { | ||
Version uint16 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC this field will always have value 1 (i.e. LogRootV1.Version==LOG_ROOT_FORMAT_V1
is an invariant)? If so, a comment to that effect might be helpful.
Also, if this is effectively an enum, it might be worth using the tls.Enum
type (with a size:2
tag).
More generally, I wonder whether using variants might be more future proof -- e.g. something in TLS-speak like:
enum { v1(1), (65535)} Version;
struct {
uint64 tree_size;
opaque root_hash<0..128>;
uint64 timestamp_nanos;
uint64 revision;
opaque metadata<0..65535>;
} LogRootV1;
struct {
Version version;
select(version) {
case v1: LogRootV1;
}
} LogRoot;
is then easily extendable to:
enum { v1(1), v2(2), (65535)} Version;
...
struct {
uint64 tree_size;
opaque root_hash<0..128>;
uint64 timestamp_nanos;
uint64 revision;
opaque metadata<0..65535>;
opaque something_else<0..255>;
} LogRootV2;
struct {
Version version;
select(version) {
case v1: LogRootV1;
case v2: LogRootV2;
}
} LogRoot;
(might well be overkill though)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea :-)
crypto/data_formats.go
Outdated
@@ -53,3 +55,66 @@ func HashLogRoot(root trillian.SignedLogRoot) ([]byte, error) { | |||
} | |||
return hash[:], nil | |||
} | |||
|
|||
// LogRootV1 contains the fields verified by SignedLogRoot |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having these type definitions in crypto/data_formats.go
feels a bit out of the way, given that this is a data type that forms part of the external API to Trillian. It's not a proto structure so we can't directly have it in the top-level .proto files that define the API, but we could do something like:
- Add a top-level
types.go
file with these API types as Go types. - Add (in a later PR) comments to the .proto files that say something like:
// The log_root field holds the TLS-serialization of the following // structure (described in RFC5246 notation): // struct { // uint16 version; // uint64 tree_size; // opaque root_hash<0..128>; // uint64 timestamp_nanos; // uint64 revision; // opaque metadata<0..65535>; // } LogRootV1;
That way the .proto files would contain an unambiguous description of the API details, and a non-Go user of Trillian would have enough information to be able to use the API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree on both points. Unfortunately, I can't add non-proto code to a proto package.
I'm tempted to move the proto definitions to api/v1/trillian_proto
so we can use the top level trillian
package name for this type of stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the following pull requests, I'll be defining methods to verify SignedLogRoot
that will return LogRootV1
upon verification. That was the rational for keeping these methods in the crypto
package. Should I create a crypto/signature
package?
crypto/data_formats_test.go
Outdated
if err != nil { | ||
t.Errorf("SerializeLogRoot(%v): %v", logRoot, err) | ||
} | ||
r2, err := ParseLogRoot(b) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just call this got
and skip the aliasing below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, but are we sure that skipping the aliasing best matches go style?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't think the aliasing was the point, more that things are named something-like "got" and "want". E.g. the example in the docs doesn't introduce a if want := tt.want; ...
alias
crypto/data_formats_test.go
Outdated
"testing" | ||
|
||
"github.com/google/trillian" | ||
"github.com/google/trillian/testonly" | ||
"github.com/kr/pretty" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not the pretty
package we use everywhere else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
crypto/data_formats_test.go
Outdated
func TestLogRoot(t *testing.T) { | ||
for _, logRoot := range []*LogRootV1{ | ||
{ | ||
Version: 1, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need test cases exercising version overwriting during serialization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored to remove this sharp edge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea.
crypto/data_formats_test.go
Outdated
{ | ||
logRoot: func() []byte { | ||
b, _ := SerializeLogRoot(&LogRootV1{}) | ||
b[0] = 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A comment would be helpful here.
Also/alternatively, it might be nice if the tests distinguished errors -- I think this one is checking that a wrong version (0x101) gets bounced (and so the error will be something containing "Invalid MapRoot.Version"), whereas the test below is a plain parse failure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the error types are internal to the tls
package. It's difficult to reliably discern what the error was from the outside?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I often do something like if !strings.Contains(err.Error(), test.wantErr) {
and put a relevant substring in the test definition ("failed to parse"
, "trailing data"
, etc.) -- that seems to get a decent balance between specificity and brittleness.
3dd45fd
to
ab4818d
Compare
Codecov Report
@@ Coverage Diff @@
## master #1037 +/- ##
==========================================
+ Coverage 62.56% 62.69% +0.12%
==========================================
Files 101 103 +2
Lines 8379 8408 +29
==========================================
+ Hits 5242 5271 +29
Misses 2610 2610
Partials 527 527
Continue to review full report at Codecov.
|
Updated and ready for a second look |
d91d546
to
29bb73f
Compare
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have a package comment in exactly one of the files in types
? (Some stricter linters check for this.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
types/logroot.go
Outdated
if b == nil { | ||
return nil, fmt.Errorf("nil log root") | ||
} | ||
// Verify version |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(nit) As it stands, this comment doesn't really add anything that the code doesn't say clearly. But it could -- eg. by explaining why you do the version check first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll remove the comment. As it stands, this check is not strictly needed because of the way we're using TLS variants, but it's a nice belt and braces check for others that might look to use this as a hint for other languages and parsers.
types/logroot.go
Outdated
Metadata []byte `tls:"minlen:0,maxlen:65535"` | ||
} | ||
|
||
// LogRoot contains the fields serialized into SignedLogRoot.LogRoot |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is going to be API documentation, it would be good to say how serialized
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a description of the TLS serialization
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
types/logroot.go
Outdated
V1 *LogRootV1 `tls:"selector:Version,val:1"` | ||
} | ||
|
||
// ParseLogRoot verifies that b has the LOG_ROOT_FORMAT_V1 tag and returns a *LogRootV1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Be nice to describe the expected form of b
-- it's not just bytes, it's supposed to be bytes that hold a TLS-serialized LogRoot
structure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
types/logroot.go
Outdated
return logRoot.V1, nil | ||
} | ||
|
||
// SerializeLogRoot returns a canonical TLS serialization of the log root. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe include a reference or two to RFC 5246 section 4 scattered around -- I don't think TLS encoding is as well-known as other encodings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
crypto/data_formats_test.go
Outdated
{ | ||
logRoot: func() []byte { | ||
b, _ := SerializeLogRoot(&LogRootV1{}) | ||
b[0] = 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I often do something like if !strings.Contains(err.Error(), test.wantErr) {
and put a relevant substring in the test definition ("failed to parse"
, "trailing data"
, etc.) -- that seems to get a decent balance between specificity and brittleness.
types/logroot_test.go
Outdated
{ | ||
logRoot: []byte("foo"), | ||
wantErr: true, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we have a more adversarial test case or two here? One that starts with a valid version field but has nonsense thereafter? Or which has a 129 byte root_hash
(which will decode fine but should hit a runtime limit)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
crypto/data_formats_test.go
Outdated
func TestLogRoot(t *testing.T) { | ||
for _, logRoot := range []*LogRootV1{ | ||
{ | ||
Version: 1, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(As before log* comments => map*... )
Looking good, just doc changes + a test suggestion
29bb73f
to
42bf9b0
Compare
Added tests and documentation for both logs and maps.
is a good idea but it doesn't work well when err is nil. Do you know of any reliable function to stringify |
types/logroot.go
Outdated
"github.com/google/trillian" | ||
) | ||
|
||
// LogRootV1 holds the TLS-deserializtion of the following structure |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"deserialization"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
types/logroot.go
Outdated
Metadata []byte `tls:"minlen:0,maxlen:65535"` | ||
} | ||
|
||
// LogRoot holds the TLS-deserializtion of the following structure |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"deserialization"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
Metadata: []byte{}, | ||
}, | ||
} { | ||
b, err := logRoot.MarshalBinary() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could alter the test so it handles objects of encoding.BinaryMarshaller
and encoding.BinaryUnmarshaller
interfaces ? That would implicitly check that the interface
s are indeed satisfied.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea.
This PR is part of a multi step process updating the
SignedLogRoot
struct to look like the following:This PR defines the serialization and deserialization functions for
log_root
.#958