From 70660b5e147d5790fac3116e72babed7d666f88a Mon Sep 17 00:00:00 2001 From: InfiniteLoopSpace <35842605+InfiniteLoopSpace@users.noreply.github.com> Date: Fri, 16 Nov 2018 13:57:11 +0100 Subject: [PATCH] Added support for Time-Stamp Protocol; fork of https://github.com/mastahyeti/cms/tree/master/timestamp. --- timestamp/LICENSE.md | 21 ++++++ timestamp/info.go | 99 +++++++++++++++++++++++++++++ timestamp/pkistatusinfo.go | 80 +++++++++++++++++++++++ timestamp/request.go | 123 ++++++++++++++++++++++++++++++++++++ timestamp/response.go | 45 +++++++++++++ timestamp/timestamp.go | 78 +++++++++++++++++++++++ timestamp/timestamp_test.go | 27 ++++++++ 7 files changed, 473 insertions(+) create mode 100644 timestamp/LICENSE.md create mode 100644 timestamp/info.go create mode 100644 timestamp/pkistatusinfo.go create mode 100644 timestamp/request.go create mode 100644 timestamp/response.go create mode 100644 timestamp/timestamp.go create mode 100644 timestamp/timestamp_test.go diff --git a/timestamp/LICENSE.md b/timestamp/LICENSE.md new file mode 100644 index 0000000..7800c58 --- /dev/null +++ b/timestamp/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Ben Toews. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/timestamp/info.go b/timestamp/info.go new file mode 100644 index 0000000..3438d18 --- /dev/null +++ b/timestamp/info.go @@ -0,0 +1,99 @@ +package timestamp + +import ( + "crypto/x509/pkix" + "encoding/asn1" + "math/big" + "time" + + asn "github.com/InfiniteLoopSpace/go_S-MIME/asn1" + cms "github.com/InfiniteLoopSpace/go_S-MIME/cms/protocol" + oid "github.com/InfiniteLoopSpace/go_S-MIME/oid" +) + +// TSTInfo ::= SEQUENCE { +// version INTEGER { v1(1) }, +// policy TSAPolicyId, +// messageImprint MessageImprint, +// -- MUST have the same value as the similar field in +// -- TimeStampReq +// serialNumber INTEGER, +// -- Time-Stamping users MUST be ready to accommodate integers +// -- up to 160 bits. +// genTime GeneralizedTime, +// accuracy Accuracy OPTIONAL, +// ordering BOOLEAN DEFAULT FALSE, +// nonce INTEGER OPTIONAL, +// -- MUST be present if the similar field was present +// -- in TimeStampReq. In that case it MUST have the same value. +// tsa [0] GeneralName OPTIONAL, +// extensions [1] IMPLICIT Extensions OPTIONAL } +type TSTInfo struct { + Version int + Policy asn1.ObjectIdentifier + MessageImprint MessageImprint + SerialNumber *big.Int + GenTime time.Time `asn1:"generalized"` + Accuracy Accuracy `asn1:"optional"` + Ordering bool `asn1:"optional,default:false"` + Nonce *big.Int `asn1:"optional"` + TSA asn1.RawValue `asn1:"tag:0,optional"` + Extensions []pkix.Extension `asn1:"tag:1,optional"` +} + +// ParseInfo parses an Info out of a CMS EncapsulatedContentInfo. +func ParseInfo(enci cms.EncapsulatedContentInfo) (TSTInfo, error) { + i := TSTInfo{} + if !enci.EContentType.Equal(oid.TSTInfo) { + return i, cms.ErrWrongType + } + + if rest, err := asn.Unmarshal(enci.EContent, &i); err != nil { + return i, err + } else if len(rest) > 0 { + return i, cms.ErrTrailingData + } + + return i, nil +} + +// Before checks if the latest time the signature could have been generated at +// is before the specified time. For example, you might check that a signature +// was made *before* a certificate's not-after date. +func (i *TSTInfo) Before(t time.Time) bool { + return i.genTimeMax().Before(t) +} + +// After checks if the earlier time the signature could have been generated at +// is before the specified time. For example, you might check that a signature +// was made *after* a certificate's not-before date. +func (i *TSTInfo) After(t time.Time) bool { + return i.genTimeMin().After(t) +} + +// genTimeMax is the latest time at which the token could have been generated +// based on the included GenTime and Accuracy attributes. +func (i *TSTInfo) genTimeMax() time.Time { + return i.GenTime.Add(i.Accuracy.Duration()) +} + +// genTimeMin is the earliest time at which the token could have been generated +// based on the included GenTime and Accuracy attributes. +func (i *TSTInfo) genTimeMin() time.Time { + return i.GenTime.Add(-i.Accuracy.Duration()) +} + +// Accuracy of the timestamp +type Accuracy struct { + Seconds int `asn1:"optional"` + Millis int `asn1:"tag:0,optional"` + Micros int `asn1:"tag:1,optional"` +} + +// Duration returns this Accuracy as a time.Duration. +func (a Accuracy) Duration() time.Duration { + return 0 + + time.Duration(a.Seconds)*time.Second + + time.Duration(a.Millis)*time.Millisecond + + time.Duration(a.Micros)*time.Microsecond +} diff --git a/timestamp/pkistatusinfo.go b/timestamp/pkistatusinfo.go new file mode 100644 index 0000000..079f57d --- /dev/null +++ b/timestamp/pkistatusinfo.go @@ -0,0 +1,80 @@ +package timestamp + +import ( + "encoding/asn1" + "fmt" + "strings" + + cms "github.com/InfiniteLoopSpace/go_S-MIME/cms/protocol" +) + +// PKIStatusInfo ::= SEQUENCE { +// status PKIStatus, +// statusString PKIFreeText OPTIONAL, +// failInfo PKIFailureInfo OPTIONAL } +type PKIStatusInfo struct { + Status int + StatusString PKIFreeText `asn1:"optional"` + FailInfo asn1.BitString `asn1:"optional"` +} + +// PKIFreeText ::= SEQUENCE SIZE (1..MAX) OF UTF8String +type PKIFreeText []asn1.RawValue + +// GetError represents an unsuccessful PKIStatusInfo as an error. +func (si PKIStatusInfo) GetError() error { + if si.Status == 0 { + return nil + } + return si +} + +// Error implements the error interface. +func (si PKIStatusInfo) Error() string { + fiStr := "" + if si.FailInfo.BitLength > 0 { + fibin := make([]byte, si.FailInfo.BitLength) + for i := range fibin { + if si.FailInfo.At(i) == 1 { + fibin[i] = byte('1') + } else { + fibin[i] = byte('0') + } + } + fiStr = fmt.Sprintf(" FailInfo(0b%s)", string(fibin)) + } + + statusStr := "" + if len(si.StatusString) > 0 { + if strs, err := si.StatusString.Strings(); err == nil { + statusStr = fmt.Sprintf(" StatusString(%s)", strings.Join(strs, ",")) + } + } + + return fmt.Sprintf("Bad TimeStampResp: Status(%d)%s%s", si.Status, statusStr, fiStr) +} + +// Append returns a new copy of the PKIFreeText with the provided string +// appended. +func (ft PKIFreeText) Append(t string) PKIFreeText { + return append(ft, asn1.RawValue{ + Class: asn1.ClassUniversal, + Tag: asn1.TagUTF8String, + Bytes: []byte(t), + }) +} + +// Strings decodes the PKIFreeText into a []string. +func (ft PKIFreeText) Strings() ([]string, error) { + strs := make([]string, len(ft)) + + for i := range ft { + if rest, err := asn1.Unmarshal(ft[i].FullBytes, &strs[i]); err != nil { + return nil, err + } else if len(rest) != 0 { + return nil, cms.ErrTrailingData + } + } + + return strs, nil +} diff --git a/timestamp/request.go b/timestamp/request.go new file mode 100644 index 0000000..f7f6e1b --- /dev/null +++ b/timestamp/request.go @@ -0,0 +1,123 @@ +package timestamp + +import ( + "bytes" + "fmt" + "io" + "net/http" + + "crypto" + "crypto/rand" + "crypto/x509/pkix" + "encoding/asn1" + "math/big" + + cms "github.com/InfiniteLoopSpace/go_S-MIME/cms/protocol" + oid "github.com/InfiniteLoopSpace/go_S-MIME/oid" +) + +// TimeStampReq ::= SEQUENCE { +// version INTEGER { v1(1) }, +// messageImprint MessageImprint, +// --a hash algorithm OID and the hash value of the data to be +// --time-stamped +// reqPolicy TSAPolicyId OPTIONAL, +// nonce INTEGER OPTIONAL, +// certReq BOOLEAN DEFAULT FALSE, +// extensions [0] IMPLICIT Extensions OPTIONAL } +type TimeStampReq struct { + Version int + MessageImprint MessageImprint + ReqPolicy asn1.ObjectIdentifier `asn1:"optional"` + Nonce *big.Int `asn1:"optional"` + CertReq bool `asn1:"optional,default:false"` + Extensions []pkix.Extension `asn1:"tag:1,optional"` +} + +func newTSRequest(msg []byte, hash crypto.Hash) (TimeStampReq, error) { + + mi, err := NewMessageImprint(hash, msg) + if err != nil { + return TimeStampReq{}, err + } + + return TimeStampReq{ + Version: 1, + CertReq: true, + Nonce: GenerateNonce(), + MessageImprint: mi, + }, nil +} + +// GenerateNonce generates a new nonce for this TSR. +func GenerateNonce() *big.Int { + buf := make([]byte, nonceBytes) + if _, err := rand.Read(buf); err != nil { + panic(err) + } + + return new(big.Int).SetBytes(buf[:]) +} + +// Do sends this timestamp request to the specified timestamp service, returning +// the parsed response. +func (req TimeStampReq) Do(url string) (TimeStampResp, error) { + var nilResp TimeStampResp + + reqDER, err := asn1.Marshal(req) + if err != nil { + return nilResp, err + } + + httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(reqDER)) + if err != nil { + return nilResp, err + } + httpReq.Header.Add("Content-Type", contentTypeTSQuery) + + HTTP := http.DefaultClient + + httpResp, err := HTTP.Do(httpReq) + if err != nil { + return nilResp, err + } + if ct := httpResp.Header.Get("Content-Type"); ct != contentTypeTSReply { + return nilResp, fmt.Errorf("Bad content-type: %s", ct) + } + + buf := bytes.NewBuffer(make([]byte, 0, httpResp.ContentLength)) + if _, err = io.Copy(buf, httpResp.Body); err != nil { + return nilResp, err + } + + return ParseResponse(buf.Bytes()) +} + +//MessageImprint ::= SEQUENCE { +// hashAlgorithm AlgorithmIdentifier, +// hashedMessage OCTET STRING } +type MessageImprint struct { + HashAlgorithm pkix.AlgorithmIdentifier + HashedMessage []byte +} + +// NewMessageImprint creates a new MessageImprint, digesting msg using the specified hash. +func NewMessageImprint(hash crypto.Hash, msg []byte) (MessageImprint, error) { + digestAlgorithm := oid.HashToDigestAlgorithm[hash] + if len(digestAlgorithm) == 0 { + return MessageImprint{}, cms.ErrUnsupported + } + + if !hash.Available() { + return MessageImprint{}, cms.ErrUnsupported + } + h := hash.New() + if _, err := h.Write(msg); err != nil { + return MessageImprint{}, err + } + + return MessageImprint{ + HashAlgorithm: pkix.AlgorithmIdentifier{Algorithm: digestAlgorithm}, + HashedMessage: h.Sum(nil), + }, nil +} diff --git a/timestamp/response.go b/timestamp/response.go new file mode 100644 index 0000000..6b193cf --- /dev/null +++ b/timestamp/response.go @@ -0,0 +1,45 @@ +package timestamp + +import ( + asn "github.com/InfiniteLoopSpace/go_S-MIME/asn1" + cms "github.com/InfiniteLoopSpace/go_S-MIME/cms/protocol" +) + +//TimeStampResp ::= SEQUENCE { +// status PKIStatusInfo, +// timeStampToken TimeStampToken OPTIONAL } +type TimeStampResp struct { + Status PKIStatusInfo + TimeStampToken cms.ContentInfo `asn1:"optional"` +} + +// ParseResponse parses a ASN.1 encoded TimeStampResp. +func ParseResponse(der []byte) (TimeStampResp, error) { + var resp TimeStampResp + + rest, err := asn.Unmarshal(der, &resp) + if err != nil { + return resp, err + } + if len(rest) > 0 { + return resp, cms.ErrTrailingData + } + + return resp, nil +} + +// Info returns the timestampinfo from a response. +func (r TimeStampResp) Info() (TSTInfo, error) { + var nilInfo TSTInfo + + if err := r.Status.GetError(); err != nil { + return nilInfo, err + } + + sd, err := r.TimeStampToken.SignedDataContent() + if err != nil { + return nilInfo, err + } + + return ParseInfo(sd.EncapContentInfo) +} diff --git a/timestamp/timestamp.go b/timestamp/timestamp.go new file mode 100644 index 0000000..780c45a --- /dev/null +++ b/timestamp/timestamp.go @@ -0,0 +1,78 @@ +// Package timestamp implements the timestamp protocol rfc 3161 +package timestamp + +import ( + "crypto" + "crypto/x509" + "time" + + asn1 "github.com/InfiniteLoopSpace/go_S-MIME/asn1" + cms "github.com/InfiniteLoopSpace/go_S-MIME/cms/protocol" + oid "github.com/InfiniteLoopSpace/go_S-MIME/oid" +) + +const ( + contentTypeTSQuery = "application/timestamp-query" + contentTypeTSReply = "application/timestamp-reply" + nonceBytes = 16 +) + +var ( + //Opts are options for timestamp certificate verficiation. + Opts = x509.VerifyOptions{ + Intermediates: x509.NewCertPool(), + CurrentTime: time.Now(), + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping}, + } +) + +// FetchTSToken tries to fetch a TSTokem of the given msg with hash using the given URL. +func FetchTSToken(url string, msg []byte, hash crypto.Hash) (tsToken cms.ContentInfo, err error) { + req, err := newTSRequest(msg, hash) + if err != nil { + return + } + + resp, err := req.Do(url) + if err != nil { + return + } + + if err = resp.Status.GetError(); err != nil { + return + } + + sd, err := resp.TimeStampToken.SignedDataContent() + + if err != nil { + return + } + + _, err = sd.Verify(Opts, nil) + + return resp.TimeStampToken, err +} + +// VerfiyTS verfies the given TSToken and returns the TSTInfo. +func VerfiyTS(ci cms.ContentInfo) (info TSTInfo, err error) { + if !ci.ContentType.Equal(oid.SignedData) { + err = cms.ErrUnsupported + return + } + + sd := cms.SignedData{} + + _, err = asn1.Unmarshal(ci.Content.Bytes, &sd) + if err != nil { + return + } + + _, err = sd.Verify(Opts, nil) + if err != nil { + return + } + + info, err = ParseInfo(sd.EncapContentInfo) + + return +} diff --git a/timestamp/timestamp_test.go b/timestamp/timestamp_test.go new file mode 100644 index 0000000..b2be870 --- /dev/null +++ b/timestamp/timestamp_test.go @@ -0,0 +1,27 @@ +package timestamp + +import ( + "crypto" + "fmt" + "testing" +) + +func TestTimeStamp(t *testing.T) { + + testMSG := []byte("Hallo Welt!") + + timeStampServers := []string{"http://zeitstempel.dfn.de", "http://timestamp.digicert.com"} + + for _, url := range timeStampServers { + resp, err := FetchTSToken(url, testMSG, crypto.SHA256) + if err != nil { + t.Error(err) + } + info, err := VerfiyTS(resp) + if err != nil { + t.Error(err) + } + fmt.Println(info.GenTime) + } + +}