From 0bcf659fed4b15e5f27706ca6b51482e12fa0338 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 2 Jul 2024 09:19:44 +0800 Subject: [PATCH] feat: enhanced output of TSTInfo Validate (#23) Signed-off-by: Patrick Zheng --- conformance_test.go | 18 +++++----- http_test.go | 38 ++++++++++---------- internal/oid/oid.go | 13 +++++-- response.go | 4 +-- response_test.go | 18 +++++----- timestamp.go | 45 ++++++++++++++++++++++++ timestamp_test.go | 85 +++++++++++++++++++++++++++++++++++++++++++++ token.go | 29 ++++++++++++---- token_test.go | 38 +++++++++++++++----- 9 files changed, 232 insertions(+), 56 deletions(-) create mode 100644 timestamp.go create mode 100644 timestamp_test.go diff --git a/conformance_test.go b/conformance_test.go index 8697dd1..c14aa59 100644 --- a/conformance_test.go +++ b/conformance_test.go @@ -113,17 +113,17 @@ func TestTSATimestampGranted(t *testing.T) { if err != nil { t.Fatal("SignedToken.Info() error =", err) } - ts, accuracy, err := info.Validate(message) + timestamp, err := info.Validate(message) if err != nil { t.Errorf("TSTInfo.Timestamp() error = %v", err) } - wantTimestamp := now - if ts != wantTimestamp { - t.Errorf("TSTInfo.Timestamp() Timestamp = %v, want %v", ts, wantTimestamp) + wantTimestampValue := time.Date(2021, 9, 18, 11, 54, 34, 0, time.UTC) + wantTimestampAccuracy := time.Second + if timestamp.Value != wantTimestampValue { + t.Fatalf("TSTInfo.Timestamp() Timestamp = %v, want %v", wantTimestampValue, timestamp.Value) } - wantAccuracy := time.Second - if accuracy != wantAccuracy { - t.Errorf("TSTInfo.Timestamp() Accuracy = %v, want %v", accuracy, wantAccuracy) + if timestamp.Accuracy != wantTimestampAccuracy { + t.Fatalf("TSTInfo.Timestamp() Timestamp Accuracy = %v, want %v", wantTimestampAccuracy, timestamp.Accuracy) } } @@ -328,7 +328,7 @@ func newTestTSA(malformedExtKeyUsage, criticalTimestampingExtKeyUsage bool) (*te BasicConstraintsValid: true, } if criticalTimestampingExtKeyUsage { - extValue, err := asn1.Marshal([]asn1.ObjectIdentifier{oid.TimeStamping}) + extValue, err := asn1.Marshal([]asn1.ObjectIdentifier{oid.Timestamping}) if err != nil { return nil, err } @@ -414,7 +414,7 @@ func (tsa *testTSA) Timestamp(_ context.Context, req *Request) (*Response, error Status: pki.StatusInfo{ Status: pki.StatusGranted, }, - TimeStampToken: token, + TimestampToken: token, }, nil } diff --git a/http_test.go b/http_test.go index 9714298..25554bc 100644 --- a/http_test.go +++ b/http_test.go @@ -40,10 +40,10 @@ func TestHTTPTimestampGranted(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { const wantContentType = MediaTypeTimestampQuery if got := r.Header.Get("Content-Type"); got != wantContentType { - t.Fatalf("TimeStampRequest.ContentType = %v, want %v", err, wantContentType) + t.Fatalf("TimestampRequest.ContentType = %v, want %v", err, wantContentType) } if _, err := io.ReadAll(r.Body); err != nil { - t.Fatalf("TimeStampRequest.Body read error = %v", err) + t.Fatalf("TimestampRequest.Body read error = %v", err) } // write reply @@ -122,17 +122,17 @@ func TestHTTPTimestampGranted(t *testing.T) { if err != nil { t.Fatal("SignedToken.Info() error =", err) } - timestamp, accuracy, err := info.Validate(message) + timestamp, err := info.Validate(message) if err != nil { t.Errorf("TSTInfo.Timestamp() error = %v", err) } - wantTimestamp := time.Date(2021, 9, 18, 11, 54, 34, 0, time.UTC) - if timestamp != wantTimestamp { - t.Errorf("TSTInfo.Timestamp() Timestamp = %v, want %v", timestamp, wantTimestamp) + wantTimestampValue := time.Date(2021, 9, 18, 11, 54, 34, 0, time.UTC) + wantTimestampAccuracy := time.Second + if timestamp.Value != wantTimestampValue { + t.Fatalf("TSTInfo.Timestamp() Timestamp = %v, want %v", wantTimestampValue, timestamp.Value) } - wantAccuracy := time.Second - if accuracy != wantAccuracy { - t.Errorf("TSTInfo.Timestamp() Accuracy = %v, want %v", accuracy, wantAccuracy) + if timestamp.Accuracy != wantTimestampAccuracy { + t.Fatalf("TSTInfo.Timestamp() Timestamp Accuracy = %v, want %v", wantTimestampAccuracy, timestamp.Accuracy) } } @@ -145,10 +145,10 @@ func TestHTTPTimestampRejection(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { const wantContentType = MediaTypeTimestampQuery if got := r.Header.Get("Content-Type"); got != wantContentType { - t.Fatalf("TimeStampRequest.ContentType = %v, want %v", err, wantContentType) + t.Fatalf("TimestampRequest.ContentType = %v, want %v", err, wantContentType) } if _, err := io.ReadAll(r.Body); err != nil { - t.Fatalf("TimeStampRequest.Body read error = %v", err) + t.Fatalf("TimestampRequest.Body read error = %v", err) } // write reply @@ -267,10 +267,10 @@ func TestHttpTimestamperTimestamp(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { const wantContentType = MediaTypeTimestampQuery if got := r.Header.Get("Content-Type"); got != wantContentType { - t.Fatalf("TimeStampRequest.ContentType = %v, want %v", err, wantContentType) + t.Fatalf("TimestampRequest.ContentType = %v, want %v", err, wantContentType) } if _, err := io.ReadAll(r.Body); err != nil { - t.Fatalf("TimeStampRequest.Body read error = %v", err) + t.Fatalf("TimestampRequest.Body read error = %v", err) } // write reply @@ -308,10 +308,10 @@ func TestHttpTimestamperTimestamp(t *testing.T) { ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { const wantContentType = MediaTypeTimestampQuery if got := r.Header.Get("Content-Type"); got != wantContentType { - t.Fatalf("TimeStampRequest.ContentType = %v, want %v", err, wantContentType) + t.Fatalf("TimestampRequest.ContentType = %v, want %v", err, wantContentType) } if _, err := io.ReadAll(r.Body); err != nil { - t.Fatalf("TimeStampRequest.Body read error = %v", err) + t.Fatalf("TimestampRequest.Body read error = %v", err) } // write reply @@ -334,10 +334,10 @@ func TestHttpTimestamperTimestamp(t *testing.T) { ts3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { const wantContentType = MediaTypeTimestampQuery if got := r.Header.Get("Content-Type"); got != wantContentType { - t.Fatalf("TimeStampRequest.ContentType = %v, want %v", err, wantContentType) + t.Fatalf("TimestampRequest.ContentType = %v, want %v", err, wantContentType) } if _, err := io.ReadAll(r.Body); err != nil { - t.Fatalf("TimeStampRequest.Body read error = %v", err) + t.Fatalf("TimestampRequest.Body read error = %v", err) } // write reply @@ -363,10 +363,10 @@ func TestHttpTimestamperTimestamp(t *testing.T) { ts4 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { const wantContentType = MediaTypeTimestampQuery if got := r.Header.Get("Content-Type"); got != wantContentType { - t.Fatalf("TimeStampRequest.ContentType = %v, want %v", err, wantContentType) + t.Fatalf("TimestampRequest.ContentType = %v, want %v", err, wantContentType) } if _, err := io.ReadAll(r.Body); err != nil { - t.Fatalf("TimeStampRequest.Body read error = %v", err) + t.Fatalf("TimestampRequest.Body read error = %v", err) } // write reply diff --git a/internal/oid/oid.go b/internal/oid/oid.go index d93ba6b..7ea542f 100644 --- a/internal/oid/oid.go +++ b/internal/oid/oid.go @@ -93,8 +93,17 @@ var ( // Reference: https://www.rfc-editor.org/rfc/rfc5280.html#section-4.2.1.12 ExtKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37} - // TimeStamping (id-kp-timeStamping) is defined in RFC 3161 2.3 + // Timestamping (id-kp-timeStamping) is defined in RFC 3161 2.3 // // Reference: https://datatracker.ietf.org/doc/html/rfc3161#section-2.3 - TimeStamping = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8} + Timestamping = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8} +) + +// OIDs for RFC 3628 Policy Requirements for Time-Stamping Authorities (TSAs) +var ( + // BaselineTimestampPolicy (baseline time-stamp policy) is defined in + // RFC 3628 + // + // Referene: https://datatracker.ietf.org/doc/html/rfc3628#section-5.2 + BaselineTimestampPolicy = asn1.ObjectIdentifier{0, 4, 0, 2023, 1, 1} ) diff --git a/response.go b/response.go index ba428d6..5d6492b 100644 --- a/response.go +++ b/response.go @@ -107,7 +107,7 @@ type generalNames struct { // timeStampToken TimeStampToken OPTIONAL } type Response struct { Status pki.StatusInfo - TimeStampToken asn1.RawValue `asn1:"optional"` + TimestampToken asn1.RawValue `asn1:"optional"` } // MarshalBinary encodes the response to binary form. @@ -138,7 +138,7 @@ func (r *Response) SignedToken() (*SignedToken, error) { if err := r.validateStatus(); err != nil { return nil, err } - return ParseSignedToken(r.TimeStampToken.FullBytes) + return ParseSignedToken(r.TimestampToken.FullBytes) } // Validate checks if resp is a successful timestamp response against diff --git a/response_test.go b/response_test.go index 1f06cbc..64f1fe7 100644 --- a/response_test.go +++ b/response_test.go @@ -168,7 +168,7 @@ func TestValidateResponse(t *testing.T) { Status: pki.StatusInfo{ Status: pki.StatusGranted, }, - TimeStampToken: asn1.RawValue{ + TimestampToken: asn1.RawValue{ FullBytes: token, }, } @@ -187,7 +187,7 @@ func TestValidateResponse(t *testing.T) { Status: pki.StatusInfo{ Status: pki.StatusGranted, }, - TimeStampToken: asn1.RawValue{ + TimestampToken: asn1.RawValue{ FullBytes: token, }, } @@ -208,7 +208,7 @@ func TestValidateResponse(t *testing.T) { Status: pki.StatusInfo{ Status: pki.StatusGranted, }, - TimeStampToken: asn1.RawValue{ + TimestampToken: asn1.RawValue{ FullBytes: token, }, } @@ -227,7 +227,7 @@ func TestValidateResponse(t *testing.T) { Status: pki.StatusInfo{ Status: pki.StatusGranted, }, - TimeStampToken: asn1.RawValue{ + TimestampToken: asn1.RawValue{ FullBytes: token, }, } @@ -264,7 +264,7 @@ func TestValidateResponse(t *testing.T) { Status: pki.StatusInfo{ Status: pki.StatusGranted, }, - TimeStampToken: asn1.RawValue{ + TimestampToken: asn1.RawValue{ FullBytes: token, }, } @@ -286,7 +286,7 @@ func TestValidateResponse(t *testing.T) { Status: pki.StatusInfo{ Status: pki.StatusGranted, }, - TimeStampToken: asn1.RawValue{ + TimestampToken: asn1.RawValue{ FullBytes: token, }, } @@ -308,7 +308,7 @@ func TestValidateResponse(t *testing.T) { Status: pki.StatusInfo{ Status: pki.StatusGranted, }, - TimeStampToken: asn1.RawValue{ + TimestampToken: asn1.RawValue{ FullBytes: token, }, } @@ -332,7 +332,7 @@ func TestValidateResponse(t *testing.T) { Status: pki.StatusInfo{ Status: pki.StatusGranted, }, - TimeStampToken: asn1.RawValue{ + TimestampToken: asn1.RawValue{ FullBytes: token, }, } @@ -355,7 +355,7 @@ func TestValidateResponse(t *testing.T) { Status: pki.StatusInfo{ Status: pki.StatusGranted, }, - TimeStampToken: asn1.RawValue{ + TimestampToken: asn1.RawValue{ FullBytes: token, }, } diff --git a/timestamp.go b/timestamp.go new file mode 100644 index 0000000..0f6c2c7 --- /dev/null +++ b/timestamp.go @@ -0,0 +1,45 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// 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. + +package tspclient + +import "time" + +// Timestamp denotes the time at which the timestamp token was created by the TSA +// +// Reference: RFC 3161 2.4.2 +type Timestamp struct { + // Value is the GenTime of TSTInfo + Value time.Time + + // Accuracy is the Accuracy of TSTInfo + Accuracy time.Duration +} + +// BoundedBefore returns true if the upper limit of the time at which the +// timestamp token was created is before or equal to u. +// +// Reference: RFC 3161 2.4.2 +func (t *Timestamp) BoundedBefore(u time.Time) bool { + timestampUpperLimit := t.Value.Add(t.Accuracy) + return timestampUpperLimit.Before(u) || timestampUpperLimit.Equal(u) +} + +// BoundedAfter returns true if the lower limit of the time at which the +// timestamp token was created is after or equal to u. +// +// Reference: RFC 3161 2.4.2 +func (t *Timestamp) BoundedAfter(u time.Time) bool { + timestampLowerLimit := t.Value.Add(-t.Accuracy) + return timestampLowerLimit.After(u) || timestampLowerLimit.Equal(u) +} diff --git a/timestamp_test.go b/timestamp_test.go new file mode 100644 index 0000000..57a9756 --- /dev/null +++ b/timestamp_test.go @@ -0,0 +1,85 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// 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. + +package tspclient + +import ( + "testing" + "time" +) + +func TestTimestamp(t *testing.T) { + // timestamp range: + // [time.Date(2021, time.September, 17, 14, 9, 8, 0, time.UTC), + // time.Date(2021, time.September, 17, 14, 9, 12, 0, time.UTC)] + timestamp := Timestamp{ + Value: time.Date(2021, time.September, 17, 14, 9, 10, 0, time.UTC), + Accuracy: 2 * time.Second, + } + u1 := time.Date(2021, time.September, 17, 14, 9, 7, 0, time.UTC) + u2 := time.Date(2021, time.September, 17, 14, 9, 8, 0, time.UTC) + u3 := time.Date(2021, time.September, 17, 14, 9, 9, 0, time.UTC) + u4 := time.Date(2021, time.September, 17, 14, 9, 10, 0, time.UTC) + u5 := time.Date(2021, time.September, 17, 14, 9, 11, 0, time.UTC) + u6 := time.Date(2021, time.September, 17, 14, 9, 12, 0, time.UTC) + u7 := time.Date(2021, time.September, 17, 14, 9, 13, 0, time.UTC) + + if timestamp.BoundedBefore(u1) { + t.Fatal("timestamp.BoundedBefore expected false, but got true") + } + if !timestamp.BoundedAfter(u1) { + t.Fatal("timestamp.BoundedAfter expected true, but got false") + } + + if timestamp.BoundedBefore(u2) { + t.Fatal("timestamp.BoundedBefore expected false, but got true") + } + if !timestamp.BoundedAfter(u2) { + t.Fatal("timestamp.BoundedAfter expected true, but got false") + } + + if timestamp.BoundedBefore(u3) { + t.Fatal("timestamp.BoundedBefore expected false, but got true") + } + if timestamp.BoundedAfter(u3) { + t.Fatal("timestamp.BoundedAfter expected false, but got true") + } + + if timestamp.BoundedBefore(u4) { + t.Fatal("timestamp.BoundedBefore expected false, but got true") + } + if timestamp.BoundedAfter(u4) { + t.Fatal("timestamp.BoundedAfter expected false, but got true") + } + + if timestamp.BoundedBefore(u5) { + t.Fatal("timestamp.BoundedBefore expected false, but got true") + } + if timestamp.BoundedAfter(u5) { + t.Fatal("timestamp.BoundedAfter expected false, but got true") + } + + if !timestamp.BoundedBefore(u6) { + t.Fatal("timestamp.BoundedBefore expected true, but got false") + } + if timestamp.BoundedAfter(u6) { + t.Fatal("timestamp.BoundedAfter expected false, but got true") + } + + if !timestamp.BoundedBefore(u7) { + t.Fatal("timestamp.BoundedBefore expected true, but got false") + } + if timestamp.BoundedAfter(u7) { + t.Fatal("timestamp.BoundedAfter expected false, but got true") + } +} diff --git a/token.go b/token.go index 216b5c4..f63dccb 100644 --- a/token.go +++ b/token.go @@ -214,16 +214,31 @@ type TSTInfo struct { Extensions []pkix.Extension `asn1:"optional,tag:1"` } -// Validate validates tst and returns the GenTime and Accuracy. +// Validate validates tst and returns the Timestamp on success. // tst MUST be valid and the time stamped datum MUST match message. -func (tst *TSTInfo) Validate(message []byte) (time.Time, time.Duration, error) { +func (tst *TSTInfo) Validate(message []byte) (*Timestamp, error) { if err := tst.validate(message); err != nil { - return time.Time{}, 0, err + return nil, err } - accuracy := time.Duration(tst.Accuracy.Seconds)*time.Second + - time.Duration(tst.Accuracy.Milliseconds)*time.Millisecond + - time.Duration(tst.Accuracy.Microseconds)*time.Microsecond - return tst.GenTime, accuracy, nil + + var accuracy time.Duration + // References: + // https://github.com/notaryproject/specifications/blob/main/specs/trust-store-trust-policy.md#steps + if tst.Accuracy.Seconds == 0 && + tst.Accuracy.Microseconds == 0 && + tst.Accuracy.Milliseconds == 0 && + oid.BaselineTimestampPolicy.Equal(tst.Policy) { + accuracy = 1 * time.Second + } else { + accuracy = time.Duration(tst.Accuracy.Seconds)*time.Second + + time.Duration(tst.Accuracy.Milliseconds)*time.Millisecond + + time.Duration(tst.Accuracy.Microseconds)*time.Microsecond + } + + return &Timestamp{ + Value: tst.GenTime, + Accuracy: accuracy, + }, nil } // validate checks tst against RFC 3161. diff --git a/token_test.go b/token_test.go index 4ef212c..5a4ef78 100644 --- a/token_test.go +++ b/token_test.go @@ -192,7 +192,7 @@ func TestGetSigningCertificate(t *testing.T) { } } -func TestTimestamp(t *testing.T) { +func TestValidate(t *testing.T) { timestampToken, err := getTimestampTokenFromPath("testdata/TimeStampToken.p7s") if err != nil { t.Fatal(err) @@ -202,7 +202,7 @@ func TestTimestamp(t *testing.T) { t.Fatal(err) } expectedErrMsg := "invalid TSTInfo: mismatched message" - if _, _, err := tstInfo.Validate([]byte("invalid")); err == nil || err.Error() != expectedErrMsg { + if _, err := tstInfo.Validate([]byte("invalid")); err == nil || err.Error() != expectedErrMsg { t.Fatalf("expected error %s, but got %v", expectedErrMsg, err) } @@ -214,16 +214,38 @@ func TestTimestamp(t *testing.T) { if err != nil { t.Fatal(err) } - timestamp, accuracy, err := tstInfo.Validate([]byte("notation")) + timestamp, err := tstInfo.Validate([]byte("notation")) if err != nil { t.Fatalf("expected nil error, but got %v", err) } - expectedTimestamp := time.Date(2021, time.September, 17, 14, 9, 10, 0, time.UTC) - if timestamp != expectedTimestamp { - t.Fatalf("expected timestamp %s, but got %s", expectedTimestamp, timestamp) + expectedTimestampValue := time.Date(2021, time.September, 17, 14, 9, 10, 0, time.UTC) + expectedTimestampAccuracy := time.Second + if timestamp.Value != expectedTimestampValue { + t.Fatalf("expected timestamp value %s, but got %s", expectedTimestampValue, timestamp.Value) } - if accuracy.Seconds() != 1 { - t.Fatalf("expected 1s accuracy, but got %s", accuracy) + if timestamp.Accuracy != expectedTimestampAccuracy { + t.Fatalf("expected timestamp accuracy %s, but got %s", expectedTimestampAccuracy, timestamp.Accuracy) + } + + timestampToken, err = getTimestampTokenFromPath("testdata/TimeStampToken.p7s") + if err != nil { + t.Fatal(err) + } + tstInfo, err = timestampToken.Info() + if err != nil { + t.Fatal(err) + } + tstInfo.Accuracy = Accuracy{} + tstInfo.Policy = oid.BaselineTimestampPolicy + timestamp, err = tstInfo.Validate([]byte("notation")) + if err != nil { + t.Fatalf("expected nil error, but got %v", err) + } + if timestamp.Value != expectedTimestampValue { + t.Fatalf("expected timestamp value %s, but got %s", expectedTimestampValue, timestamp.Value) + } + if timestamp.Accuracy != expectedTimestampAccuracy { + t.Fatalf("expected timestamp accuracy %s, but got %s", expectedTimestampAccuracy, timestamp.Accuracy) } }