Files
buildx/vendor/github.com/digitorus/timestamp/timestamp.go
2026-01-14 09:03:42 -08:00

681 lines
20 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/x509"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"io"
"math/big"
"strconv"
"strings"
"time"
"github.com/digitorus/pkcs7"
)
// FailureInfo contains the failure details of an Time-Stamp request. See
// https://tools.ietf.org/html/rfc3161#section-2.4.2
type FailureInfo int
const (
// UnknownFailureInfo mean that no known failure info was provided
UnknownFailureInfo FailureInfo = -1
// 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
)
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"
}
}
// Status contains the status of an Time-Stamp request. See
// https://tools.ietf.org/html/rfc3161#section-2.4.2
type Status int
const (
// Granted PKIStatus contains the value zero a TimeStampToken, as requested,
// is present.
Granted Status = 0
// GrantedWithMods PKIStatus contains the value one a TimeStampToken, with
// modifications, is present.
GrantedWithMods Status = 1
// Rejection PKIStatus
Rejection Status = 2
// Waiting PKIStatus
Waiting Status = 3
// RevocationWarning PKIStatus
RevocationWarning Status = 4
// RevocationNotification PKIStatus
RevocationNotification Status = 5
)
func (s Status) String() string {
switch s {
case Granted:
return "the request is granted"
case GrantedWithMods:
return "the request is granted with modifications"
case Rejection:
return "the request is rejected"
case Waiting:
return "the request is waiting"
case RevocationWarning:
return "revocation is imminent"
case RevocationNotification:
return "revocation has occurred"
default:
return "unknown status: " + strconv.Itoa(int(s))
}
}
// 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 {
// Timestamp token part of raw ASN.1 DER content.
RawToken []byte
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 {
var fis string
fi := resp.Status.FailureInfo()
if fi != UnknownFailureInfo {
fis = fi.String()
}
return nil, fmt.Errorf("%s: %s (%v)",
resp.Status.Status.String(),
strings.Join(resp.Status.StatusString, ","),
fis)
}
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{
RawToken: bytes,
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
}
_, err = h.Write(b[:n])
if err != nil {
return nil, fmt.Errorf("failed to create hash")
}
}
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()
}
// CreateResponseWithOpts 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) CreateResponseWithOpts(signingCert *x509.Certificate, priv crypto.Signer, opts crypto.SignerOpts) ([]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, opts)
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
}
// 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.
//
// This function is equivalent to CreateResponseWithOpts, using a SHA256 hash.
//
// Deprecated: Use CreateResponseWithOpts instead.
func (t *Timestamp) CreateResponse(signingCert *x509.Certificate, priv crypto.Signer) ([]byte, error) {
return t.CreateResponseWithOpts(signingCert, priv, crypto.SHA256)
}
// CreateErrorResponse is used to create response other than granted and granted with mod status
func CreateErrorResponse(pkiStatus Status, pkiFailureInfo FailureInfo) ([]byte, error) {
var bs asn1.BitString
setFlag(&bs, int(pkiFailureInfo))
timestampRes := response{
Status: pkiStatusInfo{
Status: pkiStatus,
FailInfo: bs,
},
}
tspResponseBytes, err := asn1.Marshal(timestampRes)
if err != nil {
return nil, err
}
return tspResponseBytes, nil
}
func setFlag(bs *asn1.BitString, i int) {
for l := len(bs.Bytes); l < 4; l++ {
(*bs).Bytes = append((*bs).Bytes, byte(0))
(*bs).BitLength = len((*bs).Bytes) * 8
}
b := i / 8
p := uint(7 - (i - 8*b))
(*bs).Bytes[b] = (*bs).Bytes[b] | (1 << p)
bs.BitLength = asn1BitLength(bs.Bytes)
bs.Bytes = bs.Bytes[0 : (bs.BitLength/8)+1]
}
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 {
if t.Accuracy < time.Microsecond {
// Round up to 1 microsecond if accuracy is lower than 1 microsecond but greater than 0 nanosecond
tstInfo.Accuracy.Microseconds = 1
} else {
seconds := t.Accuracy.Truncate(time.Second)
tstInfo.Accuracy.Seconds = int64(seconds.Seconds())
ms := (t.Accuracy - seconds).Truncate(time.Millisecond)
if ms != 0 {
tstInfo.Accuracy.Milliseconds = int64(ms.Milliseconds())
}
microSeconds := (t.Accuracy - seconds - ms).Truncate(time.Microsecond)
if microSeconds != 0 {
tstInfo.Accuracy.Microseconds = int64(microSeconds.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{{
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 (t *Timestamp) populateSigningCertificateV2Ext(certificate *x509.Certificate) ([]byte, error) {
if !t.HashAlgorithm.Available() {
return nil, x509.ErrUnsupportedAlgorithm
}
if t.HashAlgorithm.HashFunc() == crypto.SHA1 {
return nil, fmt.Errorf("for SHA1 use ESSCertID instead of ESSCertIDv2")
}
h := t.HashAlgorithm.HashFunc().New()
_, err := h.Write(certificate.Raw)
if err != nil {
return nil, fmt.Errorf("failed to create hash")
}
var hashAlg pkix.AlgorithmIdentifier
// HashAlgorithm defaults to SHA256
if t.HashAlgorithm.HashFunc() != crypto.SHA256 {
hashAlg = pkix.AlgorithmIdentifier{
Algorithm: hashOIDs[t.HashAlgorithm.HashFunc()],
Parameters: asn1.NullRawValue,
}
}
signingCertificateV2 := signingCertificateV2{
Certs: []essCertIDv2{{
HashAlgorithm: hashAlg,
CertHash: h.Sum(nil),
IssuerSerial: issuerAndSerial{
IssuerName: generalNames{
Name: asn1.RawValue{Tag: 4, Class: 2, IsCompound: true, Bytes: certificate.RawIssuer},
},
SerialNumber: certificate.SerialNumber,
},
}},
}
signingCertV2Bytes, err := asn1.Marshal(signingCertificateV2)
if err != nil {
return nil, err
}
return signingCertV2Bytes, nil
}
// digestAlgorithmToOID converts the hash func to the corresponding OID.
// This should have parity with [pkcs7.getHashForOID].
func digestAlgorithmToOID(hash crypto.Hash) (asn1.ObjectIdentifier, error) {
switch hash {
case crypto.SHA1:
return pkcs7.OIDDigestAlgorithmSHA1, nil
case crypto.SHA256:
return pkcs7.OIDDigestAlgorithmSHA256, nil
case crypto.SHA384:
return pkcs7.OIDDigestAlgorithmSHA384, nil
case crypto.SHA512:
return pkcs7.OIDDigestAlgorithmSHA512, nil
}
return nil, pkcs7.ErrUnsupportedAlgorithm
}
func (t *Timestamp) generateSignedData(tstInfo []byte, signer crypto.Signer, certificate *x509.Certificate, opts crypto.SignerOpts) ([]byte, error) {
signedData, err := pkcs7.NewSignedData(tstInfo)
if err != nil {
return nil, err
}
alg, err := digestAlgorithmToOID(opts.HashFunc())
if err != nil {
return nil, err
}
signedData.SetDigestAlgorithm(alg)
signedData.SetContentType(asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 1, 4})
signedData.GetSignedData().Version = 3
signingCertV2Bytes, err := t.populateSigningCertificateV2Ext(certificate)
if err != nil {
return nil, err
}
signerInfoConfig := pkcs7.SignerInfoConfig{
ExtraSignedAttributes: []pkcs7.Attribute{
{
Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 47},
Value: asn1.RawValue{FullBytes: signingCertV2Bytes},
},
},
}
if !t.AddTSACertificate {
signerInfoConfig.SkipCertificates = true
}
if len(t.Certificates) > 0 {
err = signedData.AddSignerChain(certificate, signer, t.Certificates, signerInfoConfig)
} else {
err = signedData.AddSigner(certificate, signer, signerInfoConfig)
}
if err != nil {
return nil, err
}
signature, err := signedData.Finish()
if err != nil {
return nil, err
}
return signature, nil
}
// copied from crypto/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
}