Added support for Time-Stamp Protocol; fork of https://github.com/mastahyeti/cms/tree/master/timestamp.

This commit is contained in:
InfiniteLoopSpace 2018-11-16 13:57:11 +01:00
parent ca99f63569
commit 70660b5e14
7 changed files with 473 additions and 0 deletions

21
timestamp/LICENSE.md Normal file
View File

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

99
timestamp/info.go Normal file
View File

@ -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
}

View File

@ -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
}

123
timestamp/request.go Normal file
View File

@ -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
}

45
timestamp/response.go Normal file
View File

@ -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)
}

78
timestamp/timestamp.go Normal file
View File

@ -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
}

View File

@ -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)
}
}