576 lines
17 KiB
Go
576 lines
17 KiB
Go
// Package timestamp implements the Time-Stamp Protocol (TSP) as specified in
|
|
// RFC3161 (Internet X.509 Public Key Infrastructure Time-Stamp Protocol (TSP)).
|
|
package timestamp
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/unidoc/pkcs7"
|
|
)
|
|
|
|
// FailureInfo contains the result of an Time-Stamp request. See
|
|
// https://tools.ietf.org/html/rfc3161#section-2.4.2
|
|
type FailureInfo int
|
|
|
|
const (
|
|
// BadAlgorithm defines an unrecognized or unsupported Algorithm Identifier
|
|
BadAlgorithm FailureInfo = 0
|
|
// BadRequest indicates that the transaction not permitted or supported
|
|
BadRequest FailureInfo = 2
|
|
// BadDataFormat means tha data submitted has the wrong format
|
|
BadDataFormat FailureInfo = 5
|
|
// TimeNotAvailable indicates that TSA's time source is not available
|
|
TimeNotAvailable FailureInfo = 14
|
|
// UnacceptedPolicy indicates that the requested TSA policy is not supported
|
|
// by the TSA
|
|
UnacceptedPolicy FailureInfo = 15
|
|
// UnacceptedExtension indicates that the requested extension is not supported
|
|
// by the TSA
|
|
UnacceptedExtension FailureInfo = 16
|
|
// AddInfoNotAvailable means that the information requested could not be
|
|
// understood or is not available
|
|
AddInfoNotAvailable FailureInfo = 17
|
|
// SystemFailure indicates that the request cannot be handled due to system
|
|
// failure
|
|
SystemFailure FailureInfo = 25
|
|
)
|
|
|
|
const (
|
|
// Granted PKIStatus contains the value zero a TimeStampToken, as requested, is present.
|
|
Granted int = 0
|
|
|
|
// GrantedWithMods PKIStatus contains the value one a TimeStampToken, with modifications, is present.
|
|
GrantedWithMods int = 1
|
|
|
|
// Rejection PKIStatus
|
|
Rejection int = 2
|
|
|
|
// Waiting PKIStatus
|
|
Waiting int = 3
|
|
|
|
// RevocationWarning PKIStatus
|
|
RevocationWarning int = 4
|
|
|
|
// RevocationNotification PKIStatus
|
|
RevocationNotification int = 5
|
|
)
|
|
|
|
func (f FailureInfo) String() string {
|
|
switch f {
|
|
case BadAlgorithm:
|
|
return "unrecognized or unsupported Algorithm Identifier"
|
|
case BadRequest:
|
|
return "transaction not permitted or supported"
|
|
case BadDataFormat:
|
|
return "the data submitted has the wrong format"
|
|
case TimeNotAvailable:
|
|
return "the TSA's time source is not available"
|
|
case UnacceptedPolicy:
|
|
return "the requested TSA policy is not supported by the TSA"
|
|
case UnacceptedExtension:
|
|
return "the requested extension is not supported by the TSA"
|
|
case AddInfoNotAvailable:
|
|
return "the additional information requested could not be understood or is not available"
|
|
case SystemFailure:
|
|
return "the request cannot be handled due to system failure"
|
|
default:
|
|
return "unknown failure: " + strconv.Itoa(int(f))
|
|
}
|
|
}
|
|
|
|
// ParseError results from an invalid Time-Stamp request or response.
|
|
type ParseError string
|
|
|
|
func (p ParseError) Error() string {
|
|
return string(p)
|
|
}
|
|
|
|
// Request represents an Time-Stamp request. See
|
|
// https://tools.ietf.org/html/rfc3161#section-2.4.1
|
|
type Request struct {
|
|
HashAlgorithm crypto.Hash
|
|
HashedMessage []byte
|
|
|
|
// Certificates indicates if the TSA needs to return the signing certificate
|
|
// and optionally any other certificates of the chain as part of the response.
|
|
Certificates bool
|
|
|
|
// The TSAPolicyOID field, if provided, indicates the TSA policy under
|
|
// which the TimeStampToken SHOULD be provided
|
|
TSAPolicyOID asn1.ObjectIdentifier
|
|
|
|
// The nonce, if provided, allows the client to verify the timeliness of
|
|
// the response.
|
|
Nonce *big.Int
|
|
|
|
// Extensions contains raw X.509 extensions from the Extensions field of the
|
|
// Time-Stamp request. When parsing requests, this can be used to extract
|
|
// non-critical extensions that are not parsed by this package. When
|
|
// marshaling OCSP requests, the Extensions field is ignored, see
|
|
// ExtraExtensions.
|
|
Extensions []pkix.Extension
|
|
|
|
// ExtraExtensions contains extensions to be copied, raw, into any marshaled
|
|
// OCSP response (in the singleExtensions field). Values override any
|
|
// extensions that would otherwise be produced based on the other fields. The
|
|
// ExtraExtensions field is not populated when parsing Time-Stamp requests,
|
|
// see Extensions.
|
|
ExtraExtensions []pkix.Extension
|
|
}
|
|
|
|
// ParseRequest parses an timestamp request in DER form.
|
|
func ParseRequest(bytes []byte) (*Request, error) {
|
|
var err error
|
|
var rest []byte
|
|
var req request
|
|
|
|
if rest, err = asn1.Unmarshal(bytes, &req); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(rest) > 0 {
|
|
return nil, ParseError("trailing data in Time-Stamp request")
|
|
}
|
|
|
|
if len(req.MessageImprint.HashedMessage) == 0 {
|
|
return nil, ParseError("Time-Stamp request contains no hashed message")
|
|
}
|
|
|
|
hashFunc := getHashAlgorithmFromOID(req.MessageImprint.HashAlgorithm.Algorithm)
|
|
if hashFunc == crypto.Hash(0) {
|
|
return nil, ParseError("Time-Stamp request uses unknown hash function")
|
|
}
|
|
|
|
return &Request{
|
|
HashAlgorithm: hashFunc,
|
|
HashedMessage: req.MessageImprint.HashedMessage,
|
|
Certificates: req.CertReq,
|
|
Nonce: req.Nonce,
|
|
TSAPolicyOID: req.ReqPolicy,
|
|
Extensions: req.Extensions,
|
|
}, nil
|
|
}
|
|
|
|
// Marshal marshals the Time-Stamp request to ASN.1 DER encoded form.
|
|
func (req *Request) Marshal() ([]byte, error) {
|
|
request := request{
|
|
Version: 1,
|
|
MessageImprint: messageImprint{
|
|
HashAlgorithm: pkix.AlgorithmIdentifier{
|
|
Algorithm: getOIDFromHashAlgorithm(req.HashAlgorithm),
|
|
Parameters: asn1.RawValue{
|
|
Tag: 5, /* ASN.1 NULL */
|
|
},
|
|
},
|
|
HashedMessage: req.HashedMessage,
|
|
},
|
|
CertReq: req.Certificates,
|
|
Extensions: req.ExtraExtensions,
|
|
}
|
|
|
|
if req.TSAPolicyOID != nil {
|
|
request.ReqPolicy = req.TSAPolicyOID
|
|
}
|
|
if req.Nonce != nil {
|
|
request.Nonce = req.Nonce
|
|
}
|
|
reqBytes, err := asn1.Marshal(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return reqBytes, nil
|
|
}
|
|
|
|
// Timestamp represents an Time-Stamp. See:
|
|
// https://tools.ietf.org/html/rfc3161#section-2.4.1
|
|
type Timestamp struct {
|
|
HashAlgorithm crypto.Hash
|
|
HashedMessage []byte
|
|
|
|
Time time.Time
|
|
Accuracy time.Duration
|
|
SerialNumber *big.Int
|
|
Policy asn1.ObjectIdentifier
|
|
Ordering bool
|
|
Nonce *big.Int
|
|
Qualified bool
|
|
|
|
Certificates []*x509.Certificate
|
|
|
|
// If set to true, includes TSA certificate in timestamp response
|
|
AddTSACertificate bool
|
|
|
|
// Extensions contains raw X.509 extensions from the Extensions field of the
|
|
// Time-Stamp. When parsing time-stamps, this can be used to extract
|
|
// non-critical extensions that are not parsed by this package. When
|
|
// marshaling time-stamps, the Extensions field is ignored, see
|
|
// ExtraExtensions.
|
|
Extensions []pkix.Extension
|
|
|
|
// ExtraExtensions contains extensions to be copied, raw, into any marshaled
|
|
// Time-Stamp response. Values override any extensions that would otherwise
|
|
// be produced based on the other fields. The ExtraExtensions field is not
|
|
// populated when parsing Time-Stamp responses, see Extensions.
|
|
ExtraExtensions []pkix.Extension
|
|
}
|
|
|
|
// ParseResponse parses an Time-Stamp response in DER form containing a
|
|
// TimeStampToken.
|
|
//
|
|
// Invalid signatures or parse failures will result in a ParseError. Error
|
|
// responses will result in a ResponseError.
|
|
func ParseResponse(bytes []byte) (*Timestamp, error) {
|
|
var err error
|
|
var rest []byte
|
|
var resp response
|
|
|
|
if rest, err = asn1.Unmarshal(bytes, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(rest) > 0 {
|
|
return nil, ParseError("trailing data in Time-Stamp response")
|
|
}
|
|
|
|
if resp.Status.Status > 0 {
|
|
return nil, ParseError(fmt.Sprintf("%s: %s",
|
|
FailureInfo(int(resp.Status.FailInfo.Bytes[0])).String(), resp.Status.StatusString))
|
|
}
|
|
|
|
if len(resp.TimeStampToken.Bytes) == 0 {
|
|
return nil, ParseError("no pkcs7 data in Time-Stamp response")
|
|
}
|
|
|
|
return Parse(resp.TimeStampToken.FullBytes)
|
|
}
|
|
|
|
// Parse parses an Time-Stamp in DER form. If the time-stamp contains a
|
|
// certificate then the signature over the response is checked.
|
|
//
|
|
// Invalid signatures or parse failures will result in a ParseError. Error
|
|
// responses will result in a ResponseError.
|
|
func Parse(bytes []byte) (*Timestamp, error) {
|
|
var addTSACertificate bool
|
|
p7, err := pkcs7.Parse(bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(p7.Certificates) > 0 {
|
|
if err = p7.Verify(); err != nil {
|
|
return nil, err
|
|
}
|
|
addTSACertificate = true
|
|
} else {
|
|
addTSACertificate = false
|
|
}
|
|
|
|
var inf tstInfo
|
|
if _, err = asn1.Unmarshal(p7.Content, &inf); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(inf.MessageImprint.HashedMessage) == 0 {
|
|
return nil, ParseError("Time-Stamp response contains no hashed message")
|
|
}
|
|
|
|
ret := &Timestamp{
|
|
HashedMessage: inf.MessageImprint.HashedMessage,
|
|
Time: inf.Time,
|
|
Accuracy: time.Duration((time.Second * time.Duration(inf.Accuracy.Seconds)) +
|
|
(time.Millisecond * time.Duration(inf.Accuracy.Milliseconds)) +
|
|
(time.Microsecond * time.Duration(inf.Accuracy.Microseconds))),
|
|
SerialNumber: inf.SerialNumber,
|
|
Policy: inf.Policy,
|
|
Ordering: inf.Ordering,
|
|
Nonce: inf.Nonce,
|
|
Certificates: p7.Certificates,
|
|
AddTSACertificate: addTSACertificate,
|
|
Extensions: inf.Extensions,
|
|
}
|
|
|
|
ret.HashAlgorithm = getHashAlgorithmFromOID(inf.MessageImprint.HashAlgorithm.Algorithm)
|
|
if ret.HashAlgorithm == crypto.Hash(0) {
|
|
return nil, ParseError("Time-Stamp response uses unknown hash function")
|
|
}
|
|
|
|
if oidInExtensions(asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 3}, inf.Extensions) {
|
|
ret.Qualified = true
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// RequestOptions contains options for constructing timestamp requests.
|
|
type RequestOptions struct {
|
|
// Hash contains the hash function that should be used when
|
|
// constructing the timestamp request. If zero, SHA-256 will be used.
|
|
Hash crypto.Hash
|
|
|
|
// Certificates sets Request.Certificates
|
|
Certificates bool
|
|
|
|
// The TSAPolicyOID field, if provided, indicates the TSA policy under
|
|
// which the TimeStampToken SHOULD be provided
|
|
TSAPolicyOID asn1.ObjectIdentifier
|
|
|
|
// The nonce, if provided, allows the client to verify the timeliness of
|
|
// the response.
|
|
Nonce *big.Int
|
|
}
|
|
|
|
func (opts *RequestOptions) hash() crypto.Hash {
|
|
if opts == nil || opts.Hash == 0 {
|
|
return crypto.SHA256
|
|
}
|
|
return opts.Hash
|
|
}
|
|
|
|
// CreateRequest returns a DER-encoded, timestamp request for the status of cert. If
|
|
// opts is nil then sensible defaults are used.
|
|
func CreateRequest(r io.Reader, opts *RequestOptions) ([]byte, error) {
|
|
hashFunc := opts.hash()
|
|
|
|
if !hashFunc.Available() {
|
|
return nil, x509.ErrUnsupportedAlgorithm
|
|
}
|
|
h := opts.hash().New()
|
|
|
|
b := make([]byte, h.Size())
|
|
for {
|
|
n, err := r.Read(b)
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
h.Write(b[:n])
|
|
}
|
|
|
|
req := &Request{
|
|
HashAlgorithm: opts.hash(),
|
|
HashedMessage: h.Sum(nil),
|
|
}
|
|
if opts != nil {
|
|
req.Certificates = opts.Certificates
|
|
}
|
|
if opts != nil && opts.TSAPolicyOID != nil {
|
|
req.TSAPolicyOID = opts.TSAPolicyOID
|
|
}
|
|
if opts != nil && opts.Nonce != nil {
|
|
req.Nonce = opts.Nonce
|
|
}
|
|
return req.Marshal()
|
|
}
|
|
|
|
// CreateResponse returns a DER-encoded timestamp response with the specified contents.
|
|
// The fields in the response are populated as follows:
|
|
//
|
|
// The responder cert is used to populate the responder's name field, and the
|
|
// certificate itself is provided alongside the timestamp response signature.
|
|
func (t *Timestamp) CreateResponse(signingCert *x509.Certificate, priv crypto.Signer) ([]byte, error) {
|
|
messageImprint := getMessageImprint(t.HashAlgorithm, t.HashedMessage)
|
|
|
|
tsaSerialNumber, err := generateTSASerialNumber()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tstInfo, err := t.populateTSTInfo(messageImprint, t.Policy, tsaSerialNumber, signingCert)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
signature, err := t.generateSignedData(tstInfo, priv, signingCert)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
timestampRes := response{
|
|
Status: pkiStatusInfo{
|
|
Status: Granted,
|
|
},
|
|
TimeStampToken: asn1.RawValue{FullBytes: signature},
|
|
}
|
|
tspResponseBytes, err := asn1.Marshal(timestampRes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tspResponseBytes, nil
|
|
}
|
|
|
|
//CreateErrorResponse is used to create response other than granted and granted with mod status
|
|
func CreateErrorResponse(pkiStatus int, pkiFailureInfo FailureInfo) ([]byte, error) {
|
|
timestampRes := response{
|
|
Status: pkiStatusInfo{
|
|
Status: pkiStatus,
|
|
FailInfo: asn1.BitString{Bytes: []byte{byte(pkiFailureInfo)}, BitLength: 8},
|
|
},
|
|
}
|
|
tspResponseBytes, err := asn1.Marshal(timestampRes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tspResponseBytes, nil
|
|
}
|
|
|
|
func getMessageImprint(hashAlgorithm crypto.Hash, hashedMessage []byte) messageImprint {
|
|
messageImprint := messageImprint{
|
|
HashAlgorithm: pkix.AlgorithmIdentifier{
|
|
Algorithm: getOIDFromHashAlgorithm(hashAlgorithm),
|
|
Parameters: asn1.NullRawValue,
|
|
},
|
|
HashedMessage: hashedMessage,
|
|
}
|
|
return messageImprint
|
|
}
|
|
|
|
func generateTSASerialNumber() (*big.Int, error) {
|
|
randomBytes := make([]byte, 20)
|
|
_, err := rand.Read(randomBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
serialNumber := big.NewInt(0)
|
|
serialNumber = serialNumber.SetBytes(randomBytes)
|
|
return serialNumber, nil
|
|
}
|
|
|
|
func (t *Timestamp) populateTSTInfo(messageImprint messageImprint, policyOID asn1.ObjectIdentifier, tsaSerialNumber *big.Int, tsaCert *x509.Certificate) ([]byte, error) {
|
|
dirGeneralName, err := asn1.Marshal(asn1.RawValue{Tag: 4, Class: 2, IsCompound: true, Bytes: tsaCert.RawSubject})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tstInfo := tstInfo{
|
|
Version: 1,
|
|
Policy: policyOID,
|
|
MessageImprint: messageImprint,
|
|
SerialNumber: tsaSerialNumber,
|
|
Time: t.Time,
|
|
TSA: asn1.RawValue{Tag: 0, Class: 2, IsCompound: true, Bytes: dirGeneralName},
|
|
Ordering: t.Ordering,
|
|
}
|
|
if t.Nonce != nil {
|
|
tstInfo.Nonce = t.Nonce
|
|
}
|
|
if t.Accuracy != 0 {
|
|
var accuracy accuracy
|
|
|
|
seconds := t.Accuracy.Truncate(time.Second)
|
|
accuracy.Seconds = int64(seconds)
|
|
ms := (t.Accuracy - seconds).Truncate(time.Millisecond)
|
|
if ms != 0 {
|
|
accuracy.Milliseconds = int64(ms)
|
|
}
|
|
microSeconds := (t.Accuracy - seconds - ms).Truncate(time.Microsecond)
|
|
if microSeconds != 0 {
|
|
accuracy.Microseconds = int64(microSeconds)
|
|
}
|
|
}
|
|
if len(t.ExtraExtensions) != 0 {
|
|
tstInfo.Extensions = t.ExtraExtensions
|
|
}
|
|
if t.Qualified && !oidInExtensions(asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 3}, t.ExtraExtensions) {
|
|
qcStatements := []qcStatement{
|
|
qcStatement{
|
|
StatementID: asn1.ObjectIdentifier{0, 4, 0, 19422, 1, 1},
|
|
},
|
|
}
|
|
asn1QcStats, err := asn1.Marshal(qcStatements)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tstInfo.Extensions = append(tstInfo.Extensions, pkix.Extension{
|
|
Id: []int{1, 3, 6, 1, 5, 5, 7, 1, 3},
|
|
Value: asn1QcStats,
|
|
Critical: false,
|
|
})
|
|
}
|
|
tstInfoBytes, err := asn1.Marshal(tstInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tstInfoBytes, nil
|
|
}
|
|
|
|
func populateSigningCertificateV2Ext(certificate *x509.Certificate) ([]byte, error) {
|
|
h := sha256.New()
|
|
h.Write(certificate.Raw)
|
|
|
|
signingCertificateV2 := signingCertificateV2{
|
|
Certs: []essCertIDv2{
|
|
essCertIDv2{
|
|
HashAlgorithm: pkix.AlgorithmIdentifier{
|
|
Algorithm: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1},
|
|
Parameters: asn1.NullRawValue,
|
|
},
|
|
CertHash: h.Sum(nil),
|
|
IssuerSerial: issuerAndSerial{
|
|
IssuerName: asn1.RawValue{FullBytes: certificate.RawIssuer},
|
|
SerialNumber: certificate.SerialNumber,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
signingCertV2Bytes, err := asn1.Marshal(signingCertificateV2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return signingCertV2Bytes, nil
|
|
}
|
|
|
|
func (t *Timestamp) generateSignedData(tstInfo []byte, privateKey crypto.PrivateKey, certificate *x509.Certificate) ([]byte, error) {
|
|
signedData, err := pkcs7.NewSignedDataWithContentType(asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 1, 4}, tstInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
signedData.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
|
|
|
|
signingCertV2Bytes, err := populateSigningCertificateV2Ext(certificate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if t.AddTSACertificate {
|
|
err = signedData.AddSigner(certificate, privateKey, pkcs7.SignerInfoConfig{
|
|
ExtraSignedAttributes: []pkcs7.Attribute{
|
|
pkcs7.Attribute{
|
|
Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 47},
|
|
Value: asn1.RawContent(signingCertV2Bytes),
|
|
},
|
|
},
|
|
})
|
|
} else {
|
|
err = signedData.AddSignerNoChain(certificate, privateKey, pkcs7.SignerInfoConfig{
|
|
ExtraSignedAttributes: []pkcs7.Attribute{
|
|
pkcs7.Attribute{
|
|
Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 47},
|
|
Value: asn1.RawContent(signingCertV2Bytes),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
signature, err := signedData.Finish()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return signature, nil
|
|
}
|
|
|
|
// copied from cryto/x509 package
|
|
// oidNotInExtensions reports whether an extension with the given oid exists in
|
|
// extensions.
|
|
func oidInExtensions(oid asn1.ObjectIdentifier, extensions []pkix.Extension) bool {
|
|
for _, e := range extensions {
|
|
if e.Id.Equal(oid) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|