Added support for Time-Stamp Protocol; fork of https://github.com/mastahyeti/cms/tree/master/timestamp.
This commit is contained in:
parent
ca99f63569
commit
70660b5e14
21
timestamp/LICENSE.md
Normal file
21
timestamp/LICENSE.md
Normal 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
99
timestamp/info.go
Normal 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
|
||||||
|
}
|
80
timestamp/pkistatusinfo.go
Normal file
80
timestamp/pkistatusinfo.go
Normal 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
123
timestamp/request.go
Normal 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
45
timestamp/response.go
Normal 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
78
timestamp/timestamp.go
Normal 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
|
||||||
|
}
|
27
timestamp/timestamp_test.go
Normal file
27
timestamp/timestamp_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user