/*
 *
 *  Copyright (C) 2019-2024, OFFIS e.V.
 *  All rights reserved.  See COPYRIGHT file for details.
 *
 *  This software and supporting documentation were developed by
 *
 *    OFFIS e.V.
 *    R&D Division Health
 *    Escherweg 2
 *    D-26121 Oldenburg, Germany
 *
 *
 *  Module: dcmsign
 *
 *  Author: Marco Eichelberg
 *
 *  Purpose:
 *    classes: SiTimeStamp
 *
 */

#include "dcmtk/config/osconfig.h"

#ifdef WITH_OPENSSL

#include "dcmtk/dcmsign/sitstamp.h"

#include "dcmtk/dcmdata/dcerror.h"
#include "dcmtk/dcmdata/dcitem.h"
#include "dcmtk/dcmdata/dcdeftag.h"
#include "dcmtk/dcmsign/simdmac.h"
#include "dcmtk/dcmsign/sicert.h"
#include "dcmtk/dcmsign/sicertvf.h"
#include "dcmtk/dcmsign/dcsignat.h"
#include "dcmtk/ofstd/ofdatime.h"

BEGIN_EXTERN_C
#include <openssl/ts.h>
#include <openssl/err.h>
#include <openssl/rand.h>
#include <openssl/bn.h>
#include <openssl/x509.h>
END_EXTERN_C


/// maximum length of the integer nonce, in bytes
#define NONCE_LENGTH 8

/// simple RAII container class for a raw buffer
class RAIIBuffer
{
public:
  /** constructor, allocates buffer
   *  @param len buffer size, must be > 0
   */
  RAIIBuffer(size_t len)
  : buf_(new unsigned char[len])
  , len_(len)
  {
  }

  /// destructor
  virtual ~RAIIBuffer()
  {
    delete[] buf_;
  }

  /// buffer
  unsigned char *buf_;

  /// buffer size
  size_t len_;
};


SiTimeStamp::SiTimeStamp()
: tsq_policy_()
, tsq_mac_(EMT_SHA256)
, tsq_use_nonce_(OFTrue)
, tsq_certificate_requested_(OFTrue)
, tsq_(NULL)
, tsr_(NULL)
, ts_(NULL)
, tsinfo_(NULL)
, errorCode_(0)
, errorString_("")
{
}


SiTimeStamp::~SiTimeStamp()
{
  TS_REQ_free(tsq_);
  TS_RESP_free(tsr_);
  PKCS7_free(ts_);
  TS_TST_INFO_free(tsinfo_);
}

void SiTimeStamp::setPolicyOID(const char *oid)
{
  if (oid) tsq_policy_ = oid; else tsq_policy_ = "";
}


void SiTimeStamp::setNonce(OFBool nonce)
{
  tsq_use_nonce_ = nonce;
}


void SiTimeStamp::setCertificateRequested(OFBool creq)
{
  tsq_certificate_requested_ = creq;
}

void SiTimeStamp::setMAC(E_MACType mac)
{
  tsq_mac_ = mac;
}


OFCondition SiTimeStamp::create_ts_query(
    const unsigned char *inputData,
    unsigned long inputDataSize)
{

  // validate input parameters
  if (inputData == NULL || inputDataSize == 0) return EC_IllegalCall;

  // delete a previous time stamp query, if present
  if (tsq_)
  {
    TS_REQ_free(tsq_);
    tsq_ = NULL;
  }

  // create message digest object
  SiMAC *mac = NULL;  // dcmsign MAC object
  const EVP_MD *evpmd = NULL; // OpenSSL MAC object
  switch (tsq_mac_)
  {
    case EMT_SHA1:
      mac = new SiMDMAC(EMT_SHA1);
      evpmd = EVP_sha1();
      break;
    case EMT_RIPEMD160:
      mac = new SiMDMAC(EMT_RIPEMD160);
      evpmd = EVP_ripemd160();
      break;
    case EMT_MD5:
      mac = new SiMDMAC(EMT_MD5);
      evpmd = EVP_md5();
      break;
    case EMT_SHA256:
      mac = new SiMDMAC(EMT_SHA256);
      evpmd = EVP_sha256();
      break;
    case EMT_SHA384:
      mac = new SiMDMAC(EMT_SHA384);
      evpmd = EVP_sha384();
      break;
    case EMT_SHA512:
      mac = new SiMDMAC(EMT_SHA512);
      evpmd = EVP_sha512();
      break;
    default:
      DCMSIGN_WARN("Unsupported MAC algorithm for timestamp selected.");
      return SI_EC_UnsupportedMAC;
      break;
  }
  if (mac == NULL || evpmd == NULL) return EC_MemoryExhausted;

  // compute message digest
  OFCondition result = EC_Normal;
  RAIIBuffer md(mac->getSize());
  result = mac->initialize();
  if (result.good()) result = mac->digest(inputData, inputDataSize);
  if (result.good()) result = mac->finalize(md.buf_);

  // delete message digest object
  delete mac;

  if (result.good())
  {
    // create time stamp request, algorithm and message imprint objects
    tsq_ = TS_REQ_new();
    X509_ALGOR *algo = X509_ALGOR_new();
    TS_MSG_IMPRINT *msg_imprint = TS_MSG_IMPRINT_new();
    if (tsq_ && algo && msg_imprint)
    {
      // set time stamp query version (always 1)
      if (!TS_REQ_set_version(tsq_, 1)) result = SI_EC_OpenSSLFailure;

      // set message digest type
      if (result.good())
      {
        if (NULL == (algo->algorithm = OBJ_nid2obj(EVP_MD_type(evpmd)))) result = SI_EC_OpenSSLFailure;
      }

      // create parameter object
      if (result.good())
      {
        if (NULL == (algo->parameter = ASN1_TYPE_new())) result = EC_MemoryExhausted;
      }

      // copy algorithm object into message imprint
      if (result.good())
      {
        algo->parameter->type = V_ASN1_NULL;
        if (!TS_MSG_IMPRINT_set_algo(msg_imprint, algo)) result = SI_EC_OpenSSLFailure;
      }

      // copy message digest into message imprint
      if (result.good())
      {
        if (!TS_MSG_IMPRINT_set_msg(msg_imprint, md.buf_, OFstatic_cast(int, md.len_))) result = SI_EC_OpenSSLFailure;
      }

      // copy message imprint into time stamp query
      if (result.good())
      {
        if (!TS_REQ_set_msg_imprint(tsq_, msg_imprint)) result = SI_EC_OpenSSLFailure;
      }

      // set certificate requested flag
      if (result.good())
      {
        if (!TS_REQ_set_cert_req(tsq_, (tsq_certificate_requested_ ? 1: 0))) result = SI_EC_OpenSSLFailure;
      }
    }
    else result = EC_MemoryExhausted;

    // delete algorithm object
    X509_ALGOR_free(algo);

    // delete message imprint object
    TS_MSG_IMPRINT_free(msg_imprint);

    // now we check the optional components of a time stamp query

    // policy OID
    if (result.good() && (tsq_policy_.length() > 0))
    {
      ASN1_OBJECT *policy_obj = OBJ_txt2obj(tsq_policy_.c_str(), 0);
      if (policy_obj)
      {
        // copy policy object into time stamp query
        if (!TS_REQ_set_policy_id(tsq_, policy_obj)) result = SI_EC_OpenSSLFailure;

        // delete policy object
        ASN1_OBJECT_free(policy_obj);
      }
      else
      {
        DCMSIGN_WARN("Cannot convert " << tsq_policy_ << " to OID.");
        result = SI_EC_InvalidOID;
      }
    }

    // nonce (pseudo-random integer used as protection against replay attacks)
    if (result.good() && tsq_use_nonce_)
    {
      // create buffer for random data
      unsigned char nonce_buf[NONCE_LENGTH];

      // fill buffer with random data
      if (RAND_bytes(nonce_buf, NONCE_LENGTH) <= 0) result = SI_EC_OpenSSLFailure;

      if (result.good())
      {
        // create nonce object
        ASN1_INTEGER *nonce_asn1 = ASN1_INTEGER_new();

        if (nonce_asn1)
        {
          // free memory buffer of nonce_asn1 (we will create a new one)
          OPENSSL_free(nonce_asn1->data);

          // Find the first non-zero byte in the buffer
          int i = 0;
          for (i = 0; i < NONCE_LENGTH && !nonce_buf[i]; ++i)
            continue;

          // create a new memory buffer for nonce_asn1
          nonce_asn1->length = NONCE_LENGTH - i;
          nonce_asn1->data = OFreinterpret_cast(unsigned char *, OPENSSL_malloc(nonce_asn1->length + 1));
          if (NULL != nonce_asn1->data)
          {
              // copy random data into nonce. The first byte is guaranteed to be nonzero,
              // which is necessary because otherwise the field might violate DER encoding.
              memcpy(nonce_asn1->data, nonce_buf + i, nonce_asn1->length);
          }
          else result = EC_MemoryExhausted;

          if (result.good())
          {
            // copy policy object into time stamp query
            if (!TS_REQ_set_nonce(tsq_, nonce_asn1)) result = SI_EC_OpenSSLFailure;
          }

          // delete nonce object
          ASN1_INTEGER_free(nonce_asn1);
        }
        else result = EC_MemoryExhausted;
      }
    }
  }

  return result;
}

///helper structure for status bit arrays.
struct StatusMap
{
  int bit;
  const char *text;
};

/** status flags for timestamp response failures
 *  Source: OpenSSL, ts_rsp_print.c
 */
static StatusMap failure_map[] =
{
  {TS_INFO_BAD_ALG,
   "unrecognized or unsupported algorithm identifier"},
  {TS_INFO_BAD_REQUEST,
   "transaction not permitted or supported"},
  {TS_INFO_BAD_DATA_FORMAT,
   "the data submitted has the wrong format"},
  {TS_INFO_TIME_NOT_AVAILABLE,
   "the TSA's time source is not available"},
  {TS_INFO_UNACCEPTED_POLICY,
   "the requested TSA policy is not supported by the TSA"},
  {TS_INFO_UNACCEPTED_EXTENSION,
   "the requested extension is not supported by the TSA"},
  {TS_INFO_ADD_INFO_NOT_AVAILABLE,
   "the additional information requested could not be understood "
   "or is not available"},
  {TS_INFO_SYSTEM_FAILURE,
   "the request cannot be handled due to system failure"},
  {-1, NULL}
};

OFCondition SiTimeStamp::check_ts_response(
    TS_REQ *tsq,
    TS_RESP *tsr,
    DcmItem& ditem)
{
  if (tsr == NULL) return EC_IllegalCall;

  // retrieve status structure from timestamp response.
  // This function does not create a copy that we need to release later.
  TS_STATUS_INFO *status = TS_RESP_get_status_info(tsr);
  if (status == NULL)
  {
    DCMSIGN_ERROR("unable to access status structure of timestamp response message");
    return SI_EC_InvalidTSR;
  }

  // evaluate status structure from timestamp response
  OFCondition result = EC_Normal;
  long status_value = ASN1_INTEGER_get(TS_STATUS_INFO_get0_status(status));

  if (status_value != 0) // status 0 means "granted", i.e. success
  {
     if (status_value == 1) DCMSIGN_WARN("timestamp response status: granted with modifications");
     else
     {
       result = SI_EC_InvalidTSR;
       switch (status_value)
       {
         case 2:
           DCMSIGN_ERROR("timestamp response status: rejected");
           break;
         case 3:
           DCMSIGN_ERROR("timestamp response status: waiting");
           break;
         case 4:
           DCMSIGN_ERROR("timestamp response status: revocation warning");
           break;
         case 5:
           DCMSIGN_ERROR("timestamp response status: revoked");
           break;
         default:
           DCMSIGN_ERROR("timestamp response status: unknown error code " << status_value);
           break;
       }
     }

     // if we have additional status text (optional), let's print it to the logger
     const unsigned char *c = NULL;
     for (int i = 0; i < sk_ASN1_UTF8STRING_num(TS_STATUS_INFO_get0_text(status)); ++i)
     {
       c = ASN1_STRING_get0_data(sk_ASN1_UTF8STRING_value(TS_STATUS_INFO_get0_text(status), i));
       if (c) DCMSIGN_WARN("timestamp response status text: " << c);
     }

     // if we have additional failure info flags (optional), let's print them to the logger
     if (TS_STATUS_INFO_get0_failure_info(status) != NULL)
     {
      const StatusMap *fmap = failure_map;
      for (; fmap->bit >= 0; ++fmap)
      {
        if (ASN1_BIT_STRING_get_bit(TS_STATUS_INFO_get0_failure_info(status), fmap->bit))
        {
          DCMSIGN_WARN("timestamp response status text: " << fmap->text);
        }
      }
    }
  }

  TS_TST_INFO *ts_info = NULL;

  // access TSTInfo structure (i.e. the content of the signed timestamp)
  if (result.good())
  {
    ts_info = TS_RESP_get_tst_info(tsr);
    if (ts_info == NULL)
    {
      DCMSIGN_ERROR("timestamp response does not contain TSTInfo structure");
      result = SI_EC_InvalidTSR;
    }
  }

  // check version number of timestamp response
  if (result.good())
  {
    if (TS_TST_INFO_get_version(ts_info) != 1)
    {
      DCMSIGN_ERROR("unsupported timestamp response version number " << TS_TST_INFO_get_version(ts_info));
      result =  SI_EC_InvalidTSR;
    }
  }

  // access TSR message imprint (MAC algorithm and hash)
  TS_MSG_IMPRINT *ts_info_imprint = NULL;
  if (result.good())
  {
    ts_info_imprint = TS_TST_INFO_get_msg_imprint(ts_info);
    if (ts_info_imprint == NULL)
    {
      DCMSIGN_ERROR("timestamp response validation failed: message imprint missing in timestamp response");
      result = SI_EC_InvalidTSR;
    }
  }

  // access TSR MAC algorithm OID
  X509_ALGOR *ts_info_algo = NULL;
  if (result.good())
  {
    ts_info_algo = TS_MSG_IMPRINT_get_algo(ts_info_imprint);
    if (ts_info_algo == NULL)
    {
      DCMSIGN_ERROR("timestamp response validation failed: message digest algorithm missing in timestamp response");
      result = SI_EC_InvalidTSR;
    }
  }

  // bail out if the timestamp response had an error status
  if (result.bad()) return result;

  // check if the timestamp response matches the timestamp query
  if (tsq == NULL)
  {
    DCMSIGN_WARN("timestamp query not available, will not check consistency with timestamp response");
  }
  else
  {
    // at this point, tsq and ts_info are guaranteed to be valid pointers.

    // access message imprints (MAC algorithm and hash)
    TS_MSG_IMPRINT *tsq_imprint = TS_REQ_get_msg_imprint(tsq);
    if (tsq_imprint)
    {
      // compare message imprints
      X509_ALGOR *tsq_algo = TS_MSG_IMPRINT_get_algo(tsq_imprint);
      if (tsq_algo == NULL)
      {
        DCMSIGN_ERROR("timestamp response validation failed: message digest algorithm missing in timestamp query");
        result = SI_EC_InvalidTSR;
      }
      else
      {
        if (OBJ_cmp(tsq_algo->algorithm, ts_info_algo->algorithm))
        {
          DCMSIGN_ERROR("timestamp response validation failed: message digest algorithms are different in timestamp query and response");
          result = SI_EC_InvalidTSR;
        }
        else
        {
          unsigned int tsq_len = ASN1_STRING_length(TS_MSG_IMPRINT_get_msg(tsq_imprint));
          unsigned int ts_info_len = ASN1_STRING_length(TS_MSG_IMPRINT_get_msg(ts_info_imprint));
          if (tsq_len != ts_info_len)
          {
            DCMSIGN_ERROR("timestamp response validation failed: message digest lengths are different in timestamp query and response");
            result = SI_EC_InvalidTSR;
          }
          else
          {
            if (memcmp(ASN1_STRING_get0_data(TS_MSG_IMPRINT_get_msg(tsq_imprint)),
                ASN1_STRING_get0_data(TS_MSG_IMPRINT_get_msg(ts_info_imprint)), tsq_len) != 0)
            {
              DCMSIGN_ERROR("timestamp response validation failed: message digests do not match in timestamp query and response");
              result = SI_EC_InvalidTSR;
            }
          }
        }
      }
    }
    else
    {
      DCMSIGN_ERROR("timestamp response validation failed: message imprint missing in timestamp query");
      result = SI_EC_InvalidTSR;
    }
    if (result.bad()) return result;

    // check nonces for consistency
    const ASN1_INTEGER *tsq_nonce = TS_REQ_get_nonce(tsq);
    if (tsq_nonce) // the timestamp query may or may not contain a nonce
    {
       // query contains a nonce, therefore, the response MUST also contain a nonce
       // with the same value
      const ASN1_INTEGER *ts_info_nonce = TS_TST_INFO_get_nonce(ts_info);
      if (ts_info_nonce == NULL)
      {
        DCMSIGN_ERROR("timestamp response validation failed: nonce is missing in timestamp response");
        result = SI_EC_InvalidTSR;
      }
      else
      {
        // compare nonces
        if (ASN1_INTEGER_cmp(tsq_nonce, ts_info_nonce) != 0)
        {
          DCMSIGN_ERROR("timestamp response validation failed: nonces are not equal in timestamp query and response");
          result = SI_EC_InvalidTSR;
        }
      }
    }
    if (result.bad()) return result;

    // check policy OIDs for consistency
    ASN1_OBJECT *tsq_policy = TS_REQ_get_policy_id(tsq);
    if (tsq_policy) // the timestamp query may or may not contain a policy OID
    {
      const ASN1_OBJECT *ts_info_policy = TS_TST_INFO_get_policy_id(ts_info);
      if (ts_info_policy == NULL)
      {
        DCMSIGN_ERROR("timestamp response validation failed: policy OID is missing in timestamp response");
        result = SI_EC_InvalidTSR;
      }
      else
      {
        // compare policy OIDs
        if (OBJ_cmp(tsq_policy, ts_info_policy) != 0)
        {
          DCMSIGN_ERROR("timestamp response validation failed: policy OIDs are not equal in timestamp query and response");
          result = SI_EC_InvalidTSR;
        }
      }
    }
    if (result.bad()) return result;
  }

  // the timestamp response looks OK and is consistent with the timestamp query;
  // now let's check consistency with the DICOM signature.

  // get the signature from the DICOM dataset
  const Uint8 *signature = NULL;
  unsigned long sigLength = 0;
  result = ditem.findAndGetUint8Array(DCM_Signature, signature, &sigLength);
  if (result.bad() || sigLength < 4 || (signature == NULL))
  {
    DCMSIGN_ERROR("timestamp response validation failed: signature in DICOM dataset not found or invalid");
    result = SI_EC_InvalidTSR;
  }

  // determine type of signature
  E_KeyType signatureType = EKT_none;
  if (result.good())
  {
    SiCertificate cert;
    result = cert.read(ditem);
    if (result.good()) signatureType = cert.getKeyType();
    else
    {
      DCMSIGN_ERROR("timestamp response validation failed: certificate in DICOM dataset not found or invalid");
      result = SI_EC_InvalidTSR;
    }
  }

  // if the signature type is DSA or ECDSA, we need to account for a possible pad byte
  if (result.good())
  {
    if (signatureType == EKT_DSA || signatureType == EKT_EC)
    {
      DcmSignature::adjustASN1SequenceLength(signature, sigLength);
    }
  }

  // now determine the MAC algorithm we need to feed the signature to
  SiMAC *mac = NULL;
  if (result.good())
  {
    const ASN1_OBJECT *mac_oid = NULL;
    const void *ppval = NULL;
    int pptype = 0;
    X509_ALGOR_get0(&mac_oid, &pptype, &ppval, ts_info_algo);
    if (mac_oid == NULL)
    {
      DCMSIGN_ERROR("timestamp response validation failed: cannot determine MAC algorithm in timestamp response");
      result = SI_EC_InvalidTSR;
    }
    else
    {
      int mac_nid = OBJ_obj2nid(mac_oid);
      const char *mac_name = OBJ_nid2ln(mac_nid);
      switch (mac_nid)
      {
        case NID_sha1:
          mac = new SiMDMAC(EMT_SHA1);
          break;
        case NID_ripemd160:
          mac = new SiMDMAC(EMT_RIPEMD160);
          break;
        case NID_md5:
          mac = new SiMDMAC(EMT_MD5);
          break;
        case NID_sha256:
          mac = new SiMDMAC(EMT_SHA256);
          break;
        case NID_sha384:
          mac = new SiMDMAC(EMT_SHA384);
          break;
        case NID_sha512:
          mac = new SiMDMAC(EMT_SHA512);
          break;
        default:
          DCMSIGN_ERROR("timestamp response validation failed: unsupported MAC algorithm " << ( mac_name ? mac_name : "(unknown)") << " in timestamp response");
          result = SI_EC_InvalidTSR;
          break;
      }
    }
  }

  // check that the lengths of both MACs are really the same
  if (result.good())
  {
    // mac is now guaranteed to point to a valid SiMAC object
    unsigned int ts_info_len = ASN1_STRING_length(TS_MSG_IMPRINT_get_msg(ts_info_imprint));
    if (mac->getSize() != ts_info_len)
    {
      DCMSIGN_ERROR("timestamp response validation failed: message digest length does not match in DICOM dataset and timestamp response");
      result = SI_EC_InvalidTSR;
    }
  }

  // compute MAC of digital signature and compare with the MAC in the TSR
  if (result.good())
  {
    RAIIBuffer md(mac->getSize());
    result = mac->initialize();
    if (result.good()) result = mac->digest(signature, sigLength);
    if (result.good()) result = mac->finalize(md.buf_);
    if (memcmp(md.buf_, ASN1_STRING_get0_data(TS_MSG_IMPRINT_get_msg(ts_info_imprint)), mac->getSize()) != 0)
    {
      DCMSIGN_ERROR("timestamp response validation failed: message digests do not match in DICOM dataset and timestamp response");
      result = SI_EC_InvalidTSR;
    }
  }

  // delete message digest object
  delete mac;

  // Done. If we have found not problem at this point, the TSR is successful and matches
  // both the TSQ (if present) and the DICOM dataset.
  return result;
}


OFCondition SiTimeStamp::load_ts_query(const char *fname)
{
  if (fname == NULL) return EC_InvalidFilename;
  TS_REQ_free(tsq_);
  tsq_ = NULL;

  OFCondition result = EC_Normal;
  BIO *in_bio = BIO_new_file(fname, "rb");
  if ((in_bio == NULL) || (NULL == (tsq_ = d2i_TS_REQ_bio(in_bio, NULL))))
  {
    DCMSIGN_ERROR("Unable to load timestamp query file '" << fname << "'");
    result = SI_EC_CannotRead;
  }
  BIO_free_all(in_bio);
  return result;
}


OFCondition SiTimeStamp::load_ts_response(const char *fname)
{
  if (fname == NULL) return EC_InvalidFilename;
  TS_RESP_free(tsr_);
  tsr_ = NULL;

  OFCondition result = EC_Normal;
  BIO *in_bio = BIO_new_file(fname, "rb");
  if ((in_bio == NULL) || (NULL == (tsr_ = d2i_TS_RESP_bio(in_bio, NULL))))
  {
    DCMSIGN_ERROR("Unable to load timestamp response file '" << fname << "'");
    result = SI_EC_CannotRead;
  }
  BIO_free_all(in_bio);
  return result;
}


OFCondition SiTimeStamp::write_ts_token(
    TS_RESP *tsr,
    DcmItem& ditem)
{
  if (tsr == NULL)
  {
    DCMSIGN_ERROR("Cannot insert timestamp response into DICOM dataset, not loaded");
    return EC_IllegalCall;
  }

  PKCS7 *ts_token = TS_RESP_get_token(tsr);
  if (ts_token == NULL)
  {
    DCMSIGN_ERROR("No timestamp token in the timestamp respone");
    return SI_EC_InvalidTSR;
  }

  // write timestamp token to buffer
  OFCondition result = EC_Normal;
  unsigned char *ts_token_buf = NULL;
  int len = i2d_PKCS7(ts_token, &ts_token_buf);

  if (len < 0)
  {
    DCMSIGN_ERROR("Error while serializing timestamp token");
    result = SI_EC_OpenSSLFailure;
  }

  if (result.good()) result = ditem.putAndInsertUint8Array(DCM_CertifiedTimestamp, ts_token_buf, len);
  if (result.good()) result = ditem.putAndInsertString(DCM_CertifiedTimestampType, "CMS_TSP");

  OPENSSL_free(ts_token_buf);
  return result;
}


OFCondition SiTimeStamp::read(DcmItem& item)
{
  OFString tstype;
  const Uint8 *tsbytes = NULL;
  unsigned long tslen = 0;

  // delete any prior timestamp ticket
  PKCS7_free(ts_);
  ts_ = NULL;
  TS_TST_INFO_free(tsinfo_);
  tsinfo_ = NULL;

  // check certified timestamp type
  OFCondition result = item.findAndGetOFString(DCM_CertifiedTimestampType, tstype);
  if (result.good())
  {
     if (tstype == "CMS_TSP")
     {
       // access certified timestamp
       result = item.findAndGetUint8Array(DCM_CertifiedTimestamp, tsbytes, &tslen);
       if (result.good())
       {
         // adjust for a possible pad byte
         DcmSignature::adjustASN1SequenceLength(tsbytes, tslen);
         const unsigned char *tsbytes_temp = tsbytes;
         ts_ = d2i_PKCS7(NULL, &tsbytes_temp, tslen);
         if (ts_ == NULL)
         {
           DCMSIGN_WARN("unable to read certified timestamp in dataset");
           result = SI_EC_InvalidTimestamp;
         }
         else
         {
           // PKCS7_to_TS_TST_INFO() is an "expensive" function that parses
           // the content of the signed timestamp ticket. Therefore, we cache
           // the result.
           tsinfo_ = PKCS7_to_TS_TST_INFO(ts_);
           if (tsinfo_ == NULL)
           {
             DCMSIGN_WARN("unable to read certified timestamp ticket in dataset");
             result = SI_EC_InvalidTimestamp;
           }
         }
       }
     }
     else
     {
       DCMSIGN_WARN("unknown certified timestamp type: " << tstype);
       result = SI_EC_UnknownTimestampType;
     }
  }
  return result;
}


OFBool SiTimeStamp::have_tsinfo() const
{
  return (tsinfo_ != NULL);
}

long SiTimeStamp::get_tsinfo_version() const
{
  if (tsinfo_)
    return TS_TST_INFO_get_version(tsinfo_);
    else return -1;
}


void SiTimeStamp::get_tsinfo_policy_oid(OFString& oid) const
{
#define TSINFO_BUFSIZE 200
  oid = "";
  if (tsinfo_)
  {
    char buf[TSINFO_BUFSIZE]; // The OpenSSL recommends a buffer of at least 80 characters
    buf[0]='\0'; // zero terminate buffer
    int len = OBJ_obj2txt(buf, TSINFO_BUFSIZE, TS_TST_INFO_get_policy_id(tsinfo_), 1);
    if (len > TSINFO_BUFSIZE)
    {
       DCMSIGN_WARN("timestamp policy OID truncated, length is " << len);
    }
    oid = buf;
  }
}


void SiTimeStamp::get_tsinfo_imprint_algorithm_name(OFString& mac) const
{
  mac = "";
  if (tsinfo_)
  {

    TS_MSG_IMPRINT *ts_info_imprint = TS_TST_INFO_get_msg_imprint(tsinfo_);
    if (ts_info_imprint == NULL)
    {
      DCMSIGN_WARN("timestamp imprint cannot be accessed");
    }
    else
    {
      X509_ALGOR *ts_info_algo = TS_MSG_IMPRINT_get_algo(ts_info_imprint);
      if (ts_info_algo == NULL)
      {
        DCMSIGN_WARN("timestamp imprint algorithm cannot be accessed");
      }
      else
      {
        char buf[TSINFO_BUFSIZE]; // The OpenSSL recommends a buffer of at least 80 characters
        buf[0]='\0'; // zero terminate buffer
        int len = OBJ_obj2txt(buf, TSINFO_BUFSIZE, ts_info_algo->algorithm, 0);
        if (len > TSINFO_BUFSIZE)
        {
           DCMSIGN_WARN("timestamp imprint algorithm name truncated, length is " << len);
        }
        mac = buf;
      }
    }
  }
}


void SiTimeStamp::get_tsinfo_serial_number(OFString& serial) const
{
  serial = "";
  if (tsinfo_)
  {
    const ASN1_INTEGER *asn1serial = TS_TST_INFO_get_serial(tsinfo_);
    if (asn1serial == NULL)
    {
      DCMSIGN_WARN("timestamp serial number cannot be accessed");
    }
    else
    {
      BIGNUM *bnserial = ASN1_INTEGER_to_BN(asn1serial, NULL); // creates new object
      if (bnserial == NULL)
      {
        DCMSIGN_WARN("timestamp serial number cannot be converted to BIGNUM");
      }
      else
      {
        BIO *bio = BIO_new(BIO_s_mem());
        if (bio)
        {
          char *bufptr = NULL;
          BN_print(bio, bnserial);
          BIO_write(bio,"\0",1); // add terminating zero
          BIO_get_mem_data(bio, (char *)(&bufptr));
          if (bufptr)
          {
            serial = "0x";
            serial += bufptr;
          }
          BIO_free(bio);
        }
        else
        {
          DCMSIGN_WARN("timestamp serial number cannot be printed");
        }
        BN_free(bnserial);
      }
    }
  }
}


void SiTimeStamp::get_tsinfo_nonce(OFString& nonce) const
{
  nonce = "";
  if (tsinfo_)
  {
    const ASN1_INTEGER *asn1nonce = TS_TST_INFO_get_nonce(tsinfo_);
    if (asn1nonce) // nonce is an optional field, may be absent
    {
      BIGNUM *bnnonce = ASN1_INTEGER_to_BN(asn1nonce, NULL); // creates new object
      if (bnnonce == NULL)
      {
        DCMSIGN_WARN("timestamp nonce cannot be converted to BIGNUM");
      }
      else
      {
        BIO *bio = BIO_new(BIO_s_mem());
        if (bio)
        {
          char *bufptr = NULL;
          BN_print(bio, bnnonce);
          BIO_write(bio,"\0",1); // add terminating zero
          BIO_get_mem_data(bio, (char *)(&bufptr));
          if (bufptr)
          {
            nonce = "0x";
            nonce += bufptr;
          }
          BIO_free(bio);
        }
        else
        {
          DCMSIGN_WARN("timestamp nonce cannot be printed");
        }
        BN_free(bnnonce);
      }
    }
  }
}


void SiTimeStamp::get_tsinfo_tsa_name(OFString& tsa) const
{
  tsa = "";
  if (tsinfo_)
  {
    GENERAL_NAME *tsaname = TS_TST_INFO_get_tsa(tsinfo_);
    if (tsaname) // tsa is an optional field, may be absent
    {
      BIO *bio = BIO_new(BIO_s_mem());
      if (bio)
      {
        char *bufptr = NULL;
        GENERAL_NAME_print(bio, tsaname);
        BIO_write(bio,"\0",1); // add terminating zero
        BIO_get_mem_data(bio, (char *)(&bufptr));
        if (bufptr)
        {
          tsa = bufptr;
        }
        BIO_free(bio);
      }
      else
      {
        DCMSIGN_WARN("timestamp tsa cannot be printed");
      }
    }
  }
}


void SiTimeStamp::get_tsinfo_accuracy(OFString& accuracy) const
{
  accuracy = "";
  if (tsinfo_)
  {
    TS_ACCURACY *acc = TS_TST_INFO_get_accuracy(tsinfo_);
    if (acc) // accuracy is an optional field, may be absent
    {
      char buf[20];
      long sec = ASN1_INTEGER_get(TS_ACCURACY_get_seconds(acc));
      long msec = ASN1_INTEGER_get(TS_ACCURACY_get_millis(acc));
      long usec = ASN1_INTEGER_get(TS_ACCURACY_get_micros(acc));

      if (sec >= 0)
      {
        OFStandard::snprintf(buf, 20, "%ld", sec);
        accuracy = buf;
        if (usec > 0)
        {
          OFStandard::snprintf(buf, 20, ".%03ld%03lds", msec, usec);
          accuracy += buf;
        }
        else
        {
          OFStandard::snprintf(buf, 20, ".%03lds", msec);
          accuracy += buf;
        }
      }
    }
  }
}


OFBool SiTimeStamp::get_tsinfo_ordering() const
{
  OFBool result = OFFalse; // the default
  if (tsinfo_)
  {
    result = TS_TST_INFO_get_ordering(tsinfo_) ? OFTrue : OFFalse;
  }
  return result;
}


void SiTimeStamp::get_tsinfo_timestamp(OFString& ts) const
{
  ts = "";
  if (tsinfo_)
  {
    const ASN1_GENERALIZEDTIME *gtime = TS_TST_INFO_get_time(tsinfo_);
    if (gtime == NULL)
    {
      DCMSIGN_WARN("timestamp date/time cannot be accessed");
    }
    else
    {
      // ASN1_GENERALIZEDTIME is just an alias for ASN1_STRING,
      // so we can use ASN1_STRING_get0_data() to access the raw string
      const unsigned char *c = ASN1_STRING_get0_data(gtime);
      if (c) ts = OFreinterpret_cast(const char *, c);
    }
  }
}

int SiTimeStamp::get_tsinfo_numextensions() const
{
  int result = 0;
  if (tsinfo_)
  {
    result = TS_TST_INFO_get_ext_count(tsinfo_);
  }
  return result;
}


void SiTimeStamp::get_tsinfo_extension(OFString& ext, int idx) const
{
  ext = "";
  if (tsinfo_)
  {
    int numextensions = TS_TST_INFO_get_ext_count(tsinfo_);
    if (idx >= numextensions)
    {
      DCMSIGN_WARN("timestamp extension " << idx << " does not exist");
    }
    else
    {
      X509_EXTENSION *x509ext = TS_TST_INFO_get_ext(tsinfo_, idx);
      if (x509ext == NULL)
      {
        DCMSIGN_WARN("timestamp extension " << idx << " cannot be accessed");
      }
      else
      {
        BIO *bio = BIO_new(BIO_s_mem());
        if (bio)
        {
          char *bufptr = NULL;
          X509V3_EXT_print(bio, x509ext, X509V3_EXT_PARSE_UNKNOWN, 0 /* indent */);
          BIO_write(bio,"\0",1); // add terminating zero
          BIO_get_mem_data(bio, (char *)(&bufptr));
          if (bufptr)
          {
            ext = bufptr;
          }
          BIO_free(bio);
        }
        else
        {
          DCMSIGN_WARN("timestamp extension " << idx << " cannot be printed");
        }
      }
    }
  }
}


OFCondition SiTimeStamp::verifyTSSignature(SiCertificateVerifier& cv)
{
  // check if we have a timestamp object
  if (ts_ == NULL) return EC_IllegalCall;

  X509 *signerCert = NULL;
  OFCondition result = EC_Normal;
  OFString aString;
  errorCode_ = 0;

  // unfortunately, TS_RESP_verify_signature() only returns the certificate of the timestamp signer
  // if the signature verification succeeds. If verification fails, e.g. because the timestamp
  // certificate has expired, we have no way of accessing it
  if (! TS_RESP_verify_signature(ts_, cv.getUntrustedCerts(), cv.getTrustedCertStore(), &signerCert))
  {
    result = SI_EC_TimestampSignatureVerificationFailed;
    errorCode_ = ERR_get_error();
  }

  if (result.good() && signerCert)
  {
    // compare timestamp against validity period of the TSA certificate
    // The timestamp should have been created while the certificate is valid.
    const ASN1_TIME *notBefore = X509_get0_notBefore(signerCert);
    const ASN1_TIME *notAfter = X509_get0_notAfter(signerCert);
    const ASN1_GENERALIZEDTIME *gtime = TS_TST_INFO_get_time(tsinfo_);

    OFDateTime dt_notBefore;
    OFDateTime dt_notAfter;
    OFDateTime dt_gtime;

    if ((notBefore == NULL)||(notAfter == NULL)||(gtime == NULL)||
      SiCertificate::convertASN1Time(notBefore, dt_notBefore).bad() ||
      SiCertificate::convertASN1Time(notAfter, dt_notAfter).bad() ||
      SiCertificate::convertGeneralizedTime(gtime, dt_gtime).bad())
    {
      errorCode_ = -1;
      errorString_ = "comparison of timestamp date/time with TSA certificate notBefore/notAfter attribute failed, value possibly malformed.";
      DCMSIGN_ERROR(errorString_);
      result = SI_EC_TimestampSignatureVerificationFailed;
    }
    else
    {
      if (dt_gtime < dt_notBefore)
      {
        errorCode_ = -1;
        errorString_ = "timestamp created before start of TSA certificate validity.";
        DCMSIGN_ERROR(errorString_);
        result = SI_EC_TimestampSignatureVerificationFailed;
      }
      else if (dt_gtime > dt_notAfter)
      {
        errorCode_ = -1;
        errorString_ = "timestamp created after expiry of TSA certificate.";
        DCMSIGN_ERROR(errorString_);
        result = SI_EC_TimestampSignatureVerificationFailed;
      }
    }
  }

  // wrap certificate into a SiCertificate instance that will also free
  // the memory when it goes out of scope.
  // SiCertificate gracefully handles the case where signerCert is NULL.
  SiCertificate cert(signerCert);

  if (signerCert && dcmsignLogger.isEnabledFor(OFLogger::INFO_LOG_LEVEL))
  {
    DCMSIGN_INFO("  Timestamp signature certificate: ");
    if ((signerCert == NULL)||(cert.getKeyType()==EKT_none))
      DCMSIGN_INFO("      none");
    else
    {
      DCMSIGN_INFO("      X.509v" << cert.getX509Version());
      cert.getCertSubjectName(aString);
      DCMSIGN_INFO("      Subject                 : " << aString);
      cert.getCertIssuerName(aString);
      DCMSIGN_INFO("      Issued by               : " << aString);
      DCMSIGN_INFO("      Serial no.              : " << cert.getCertSerialNo());
      cert.getCertValidityNotBefore(aString);
      DCMSIGN_INFO("      Validity                : not before " << aString);
      cert.getCertValidityNotAfter(aString);
      DCMSIGN_INFO("      Validity                : not after " << aString);
      OFString ecname;
      switch (cert.getKeyType())
      {
        case EKT_RSA:
          DCMSIGN_INFO("      Public key              : RSA, " << cert.getCertKeyBits() << " bits");
          break;
        case EKT_DSA:
          DCMSIGN_INFO("      Public key              : DSA, " << cert.getCertKeyBits() << " bits");
          break;
        case EKT_EC:
          ecname = cert.getCertCurveName();
          if (ecname.length() > 0)
          {
            DCMSIGN_INFO("      Public key              : EC, curve " << ecname << ", " << cert.getCertKeyBits() << " bits");
          }
          else
          {
            DCMSIGN_INFO("      Public key              : EC, " << cert.getCertKeyBits() << " bits");
          }
          break;
        case EKT_DH:
          DCMSIGN_INFO("      Public key              : DH, " << cert.getCertKeyBits() << " bits");
          break;
        default:
        case EKT_none: // should never happen
          DCMSIGN_INFO("      Public key              : unknown type");
          break;
      }
    }
  }
  return result;
}


OFCondition SiTimeStamp::verifyTSToken(
    SiCertificateVerifier& cv,
    DcmItem& ditem,
    SiCertificate& cert)
{
  // check if we have a timestamp object
  if (ts_ == NULL) return EC_IllegalCall;

  OFCondition result = EC_Normal;
  errorCode_ = 0;

  // determine type of signature
  E_KeyType signatureType = cert.getKeyType();

  // create timestamp verification context
  TS_VERIFY_CTX *ctx = TS_VERIFY_CTX_new();

  if (ctx)
  {
    // these are the checks we want to perform
    TS_VERIFY_CTX_set_flags(ctx, TS_VFY_VERSION | TS_VFY_SIGNER | TS_VFY_DATA | TS_VFY_SIGNATURE);

    // get the signature from the DICOM dataset
    const Uint8 *signature = NULL;
    unsigned long sigLength = 0;
    result = ditem.findAndGetUint8Array(DCM_Signature, signature, &sigLength);
    if (result.bad() || sigLength < 4 || (signature == NULL))
    {
      DCMSIGN_ERROR("timestamp verification failed: signature in DICOM dataset not found or invalid");
      result = SI_EC_TimestampSignatureVerificationFailed;
    }

    // if the signature type is DSA or ECDSA, we need to account for a possible pad byte
    if (result.good())
    {
      if (signatureType == EKT_DSA || signatureType == EKT_EC)
      {
        DcmSignature::adjustASN1SequenceLength(signature, sigLength);
      }
    }

    if (result.good())
    {
      // In OpenSSL 1.0.1 and earlier, the first parameter was not const
      // We cast the const away, which should work with old and new versions
      BIO *bio = BIO_new_mem_buf(OFconst_cast(Uint8 *, signature), sigLength);
      if (bio)
      {
        // set the digital signature as the raw data against which
        // the imprint in the timestamp ticket should be checked
#ifdef HAVE_OPENSSL_PROTOTYPE_TS_VERIFY_CTX_SET0_STORE
        TS_VERIFY_CTX_set0_data(ctx, bio);
#else
        TS_VERIFY_CTX_set_data(ctx, bio);
#endif

        // set certificate store and stack. Needed to verify that
        // the TSA field in the timestamp ticket matches the signer certificate
        X509_STORE *tc_store = cv.getTrustedCertStore();
        stack_st_X509 *untrusted_certs = cv.getUntrustedCerts();
#ifdef HAVE_OPENSSL_PROTOTYPE_TS_VERIFY_CTX_SET0_STORE
        TS_VERIFY_CTX_set0_store(ctx, tc_store);
        X509_STORE_up_ref(tc_store);
        TS_VERIFY_CTX_set0_certs(ctx, X509_chain_up_ref(untrusted_certs));
#else
        TS_VERIFY_CTX_set_store(ctx, tc_store);
        TS_VERIFY_CTS_set_certs(ctx, untrusted_certs);
#endif

        // run the verification function
        if (! TS_RESP_verify_token(ctx, ts_))
        {
          result = SI_EC_TimestampSignatureVerificationFailed;
          errorCode_ = ERR_get_error();
        }

#ifndef HAVE_OPENSSL_PROTOTYPE_TS_VERIFY_CTX_SET0_STORE
         BIO_free(bio);
#endif
      }
      else
      {
        errorCode_ = -1;
        errorString_ = "timestamp verification failed: cannot create memory buffer";
        DCMSIGN_ERROR(errorString_);
        result = SI_EC_TimestampSignatureVerificationFailed;
      }
    }

#ifndef HAVE_OPENSSL_PROTOTYPE_TS_VERIFY_CTX_SET0_STORE
    // prevent TS_VERIFY_CTX_free from double-deleting our objects
    TS_VERIFY_CTX_set_data(ctx, NULL);
    TS_VERIFY_CTX_set_store(ctx, NULL);
    TS_VERIFY_CTS_set_certs(ctx, NULL);
#endif

    TS_VERIFY_CTX_free(ctx);
  }

  if (result.good())
  {
    // compare timestamp against validity period of the certificate
    // for the DICOM signature. The timestamp should have been created while the
    // certificate is valid.
    X509 *rawcert = cert.getRawCertificate();
    const ASN1_TIME *notBefore = X509_get0_notBefore(rawcert);
    const ASN1_TIME *notAfter = X509_get0_notAfter(rawcert);
    const ASN1_GENERALIZEDTIME *gtime = TS_TST_INFO_get_time(tsinfo_);

    OFDateTime dt_notBefore;
    OFDateTime dt_notAfter;
    OFDateTime dt_gtime;

    if ((notBefore == NULL)||(notAfter == NULL)||(gtime == NULL)||
      SiCertificate::convertASN1Time(notBefore, dt_notBefore).bad() ||
      SiCertificate::convertASN1Time(notAfter, dt_notAfter).bad() ||
      SiCertificate::convertGeneralizedTime(gtime, dt_gtime).bad())
    {
      errorCode_ = -1;
      errorString_ = "comparison of timestamp date/time with signer certificate notBefore/notAfter attribute failed, value possibly malformed.";
      DCMSIGN_ERROR(errorString_);
      result = SI_EC_TimestampSignatureVerificationFailed;
    }
    else
    {
      if (dt_gtime < dt_notBefore)
      {
        errorCode_ = -1;
        errorString_ = "timestamp created before start of signer certificate validity.";
        DCMSIGN_ERROR(errorString_);
        result = SI_EC_TimestampSignatureVerificationFailed;
      }
      else if (dt_gtime > dt_notAfter)
      {
        errorCode_ = -1;
        errorString_ = "timestamp created after expiry of signer certificate.";
        DCMSIGN_ERROR(errorString_);
        result = SI_EC_TimestampSignatureVerificationFailed;
      }
    }
  }

  return result;
}


void SiTimeStamp::lastError(OFString& err) const
{
  if (errorCode_ < 0) err = errorString_;
  else
  {
    const char *c = ERR_reason_error_string(errorCode_);
    if (c) err = c; else err = "";
  }
}


#else /* WITH_OPENSSL */

int sitstamp_cc_dummy_to_keep_linker_from_moaning = 0;

#endif
