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

TLS LogRootV1 Format #1037

Merged
merged 9 commits into from
Mar 9, 2018
Merged

TLS LogRootV1 Format #1037

merged 9 commits into from
Mar 9, 2018

Conversation

gdbelvin
Copy link
Contributor

@gdbelvin gdbelvin commented Mar 2, 2018

This PR is part of a multi step process updating the SignedLogRoot struct to look like the following:

message SignedLogRoot {
   bytes key_hint
   bytes log_root
   bytes signature
}

This PR defines the serialization and deserialization functions for log_root.

#958

@gdbelvin gdbelvin requested a review from daviddrysdale March 2, 2018 12:41
@gdbelvin gdbelvin requested a review from AlCutter March 2, 2018 12:41
@gdbelvin gdbelvin force-pushed the f/sig/logroot_format branch 3 times, most recently from 8283200 to 3dd45fd Compare March 2, 2018 13:23
Copy link
Contributor

@daviddrysdale daviddrysdale left a 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

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 {
Copy link
Contributor

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...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Metadata []byte `tls:"minlen:0,maxlen:65535"`
}

// ParseLogRoot returns a *SignedLogRootV1
Copy link
Contributor

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.

Copy link
Contributor Author

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

TimestampNanos uint64
Revision uint64
Metadata []byte `tls:"minlen:0,maxlen:65535"`
}
Copy link
Contributor

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?

Copy link
Contributor Author

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


// LogRootV1 contains the fields verified by SignedLogRoot
type LogRootV1 struct {
Version uint16
Copy link
Contributor

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great idea :-)

@@ -53,3 +55,66 @@ func HashLogRoot(root trillian.SignedLogRoot) ([]byte, error) {
}
return hash[:], nil
}

// LogRootV1 contains the fields verified by SignedLogRoot
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

if err != nil {
t.Errorf("SerializeLogRoot(%v): %v", logRoot, err)
}
r2, err := ParseLogRoot(b)
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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

"testing"

"github.com/google/trillian"
"github.com/google/trillian/testonly"
"github.com/kr/pretty"
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

func TestLogRoot(t *testing.T) {
for _, logRoot := range []*LogRootV1{
{
Version: 1,
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good idea.

{
logRoot: func() []byte {
b, _ := SerializeLogRoot(&LogRootV1{})
b[0] = 1
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

@google google deleted a comment from codecov-io Mar 6, 2018
@gdbelvin gdbelvin force-pushed the f/sig/logroot_format branch from 3dd45fd to ab4818d Compare March 6, 2018 12:14
@codecov-io
Copy link

codecov-io commented Mar 6, 2018

Codecov Report

Merging #1037 into master will increase coverage by 0.12%.
The diff coverage is 100%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
crypto/data_formats.go 77.77% <ø> (ø) ⬆️
types/maproot.go 100% <100%> (ø)
types/logroot.go 100% <100%> (ø)
trees/trees.go 83.67% <0%> (-1.52%) ⬇️
trees/types.go 100% <0%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c5bde67...e073651. Read the comment docs.

@gdbelvin
Copy link
Contributor Author

gdbelvin commented Mar 6, 2018

Updated and ready for a second look

@gdbelvin gdbelvin force-pushed the f/sig/logroot_format branch 2 times, most recently from d91d546 to 29bb73f Compare March 8, 2018 10:59
// 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.

Copy link
Contributor

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.)

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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

Copy link
Contributor Author

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

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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.
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

{
logRoot: func() []byte {
b, _ := SerializeLogRoot(&LogRootV1{})
b[0] = 1
Copy link
Contributor

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.

{
logRoot: []byte("foo"),
wantErr: true,
},
Copy link
Contributor

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added.

func TestLogRoot(t *testing.T) {
for _, logRoot := range []*LogRootV1{
{
Version: 1,
Copy link
Contributor

Choose a reason for hiding this comment

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

Good idea.

Copy link
Contributor

@daviddrysdale daviddrysdale left a 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

@gdbelvin gdbelvin force-pushed the f/sig/logroot_format branch from 29bb73f to 42bf9b0 Compare March 8, 2018 15:27
@gdbelvin
Copy link
Contributor Author

gdbelvin commented Mar 8, 2018

Added tests and documentation for both logs and maps.

I often do something like if !strings.Contains(err.Error(), test.wantErr)

is a good idea but it doesn't work well when err is nil. Do you know of any reliable function to stringify err and not panic on nil?

types/logroot.go Outdated
"github.com/google/trillian"
)

// LogRootV1 holds the TLS-deserializtion of the following structure
Copy link
Contributor

Choose a reason for hiding this comment

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

"deserialization"

Copy link
Contributor Author

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
Copy link
Contributor

Choose a reason for hiding this comment

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

"deserialization"

Copy link
Contributor Author

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()
Copy link
Contributor

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 interfaces are indeed satisfied.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea.

@gdbelvin gdbelvin merged commit 16362da into google:master Mar 9, 2018
@gdbelvin gdbelvin deleted the f/sig/logroot_format branch March 9, 2018 16:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants