x509_tsp/
lib.rs

1use cmpv2::status::PkiStatusInfo;
2use cms::{
3    cert::x509::{
4        ext::{pkix::name::GeneralName, Extensions},
5        spki::AlgorithmIdentifier,
6    },
7    content_info::ContentInfo,
8};
9use der::{
10    asn1::{GeneralizedTime, Int, OctetString},
11    oid::ObjectIdentifier,
12    Any, Enumerated, Sequence,
13};
14
15#[derive(Clone, Copy, Debug, Enumerated, Eq, PartialEq, PartialOrd, Ord)]
16#[asn1(type = "INTEGER")]
17#[repr(u8)]
18pub enum TspVersion {
19    /// syntax version 0
20    V1 = 1,
21}
22
23/// ```text
24/// TimeStampReq ::= SEQUENCE  {
25///    version               INTEGER  { v1(1) },
26///    messageImprint        MessageImprint,
27///    reqPolicy             TSAPolicyId              OPTIONAL,
28///    nonce                 INTEGER                  OPTIONAL,
29///    certReq               BOOLEAN                  DEFAULT FALSE,
30///    extensions            [0] IMPLICIT Extensions  OPTIONAL  }
31/// ```
32#[derive(Clone, Debug, Eq, PartialEq, Sequence)]
33pub struct TimeStampReq {
34    pub version: TspVersion,
35    pub message_imprint: MessageImprint,
36    #[asn1(optional = "true")]
37    pub req_policy: Option<TsaPolicyId>,
38    #[asn1(optional = "true")]
39    pub nonce: Option<Int>,
40    #[asn1(default = "Default::default")]
41    pub cert_req: bool,
42    #[asn1(context_specific = "0", tag_mode = "IMPLICIT", optional = "true")]
43    pub extensions: Option<Extensions>,
44}
45
46/// ```text
47/// TSAPolicyId ::= OBJECT IDENTIFIER
48/// ```
49pub type TsaPolicyId = ObjectIdentifier;
50
51/// ```text
52/// MessageImprint ::= SEQUENCE  {
53///    hashAlgorithm                AlgorithmIdentifier,
54///    hashedMessage                OCTET STRING  }
55/// ```
56#[derive(Clone, Debug, Eq, PartialEq, Sequence)]
57pub struct MessageImprint {
58    pub hash_algorithm: AlgorithmIdentifier<Any>,
59    pub hashed_message: OctetString,
60}
61
62/// ```text
63/// TimeStampResp ::= SEQUENCE  {
64///     status                  PKIStatusInfo,
65///     timeStampToken          TimeStampToken     OPTIONAL  }
66/// ```
67#[derive(Clone, Debug, Eq, PartialEq, Sequence)]
68pub struct TimeStampResp<'a> {
69    pub status: PkiStatusInfo<'a>,
70    #[asn1(optional = "true")]
71    pub time_stamp_token: Option<TimeStampToken>,
72}
73
74/// ```text
75/// TimeStampToken ::= ContentInfo
76/// ```
77pub type TimeStampToken = ContentInfo;
78
79/// ```text
80/// TSTInfo ::= SEQUENCE  {
81///     version                      INTEGER  { v1(1) },
82///     policy                       TSAPolicyId,
83///     messageImprint               MessageImprint,
84///       -- MUST have the same value as the similar field in
85///       -- TimeStampReq
86///     serialNumber                 INTEGER,
87///       -- Time-Stamping users MUST be ready to accommodate integers
88///       -- up to 160 bits.
89///     genTime                      GeneralizedTime,
90///     accuracy                     Accuracy                 OPTIONAL,
91///     ordering                     BOOLEAN             DEFAULT FALSE,
92///     nonce                        INTEGER                  OPTIONAL,
93///       -- MUST be present if the similar field was present
94///       -- in TimeStampReq.  In that case it MUST have the same value.
95///     tsa                          [0] GeneralName          OPTIONAL,
96///     extensions                   [1] IMPLICIT Extensions   OPTIONAL  }
97/// ```
98#[derive(Clone, Debug, Eq, PartialEq, Sequence)]
99pub struct TstInfo {
100    pub version: TspVersion,
101    pub policy: TsaPolicyId,
102    pub message_imprint: MessageImprint,
103    pub serial_number: Int,
104    pub gen_time: GeneralizedTime,
105    #[asn1(optional = "true")]
106    pub accuracy: Option<Accuracy>,
107    #[asn1(default = "Default::default")]
108    pub ordering: bool,
109    #[asn1(optional = "true")]
110    pub nonce: Option<Int>,
111    #[asn1(context_specific = "0", tag_mode = "EXPLICIT", optional = "true")]
112    pub tsa: Option<GeneralName>,
113    #[asn1(context_specific = "1", tag_mode = "IMPLICIT", optional = "true")]
114    pub extensions: Option<Extensions>,
115}
116
117/// ```text
118/// Accuracy ::= SEQUENCE {
119///     seconds        INTEGER              OPTIONAL,
120///     millis     [0] INTEGER  (1..999)    OPTIONAL,
121///     micros     [1] INTEGER  (1..999)    OPTIONAL  }
122/// ```
123#[derive(Clone, Debug, Eq, PartialEq, Sequence)]
124pub struct Accuracy {
125    #[asn1(optional = "true")]
126    pub seconds: Option<u64>,
127    #[asn1(context_specific = "0", tag_mode = "IMPLICIT", optional = "true")]
128    pub millis: Option<i16>,
129    #[asn1(context_specific = "1", tag_mode = "IMPLICIT", optional = "true")]
130    pub micros: Option<i16>,
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use cmpv2::status::*;
137    use cms::signed_data::SignedData;
138    use der::oid::db::rfc5912::ID_SHA_256;
139    use der::{Decode, Encode};
140    use hex_literal::hex;
141
142    // Tests use keys generated via openssl as follows:
143    //  openssl ecparam -genkey -name secp384r1 -noout -out ec384-tsa-key.pem
144    //  openssl req -new -key ec384-tsa-key.pem -out ec384-tsa-key.csr -addext "extendedKeyUsage = critical, timeStamping"
145    //  openssl req -text -in ec384-tsa-key.csr -noout
146    //  openssl x509 -req -days 365 -in ec384-tsa-key.csr -signkey ec384-tsa-key.pem -out ec384-tsa-key.crt -copy_extensions copyall
147
148    // The following config file contributes TSA-related settings in tsa.cnf:
149    // [ tsa ]
150    // default_tsa = tsa_config1	# the default TSA section
151    //
152    // [ tsa_config1 ]
153    // # These are used by the TSA reply generation only.
154    // dir		= ./		# TSA root directory
155    // serial		= $dir/tsaserial	# The current serial number (mandatory)
156    // crypto_device	= builtin		# OpenSSL engine to use for signing
157    // signer_cert	= $dir/ec384-tsa-key.crt 	# The TSA signing certificate
158    // 					# (optional)
159    // certs		= $dir/ec384-tsa-key.crt	# Certificate chain to include in reply
160    // 					# (optional)
161    // signer_key	= $dir/ec384-tsa-key.pem # The TSA private key (optional)
162    // signer_digest  = sha256			# Signing digest to use. (Optional)
163    // default_policy	= 1.2.3.4.1		# Policy if request did not specify it
164    // 					# (optional)
165    // other_policies	= 1.2.3.4.5.6, 1.2.3.4.5.7	# acceptable policies (optional)
166    // digests     = sha1, sha256, sha384, sha512  # Acceptable message digests (mandatory)
167    // accuracy	= secs:1, millisecs:500, microsecs:100	# (optional)
168    // clock_precision_digits  = 0	# number of digits after dot. (optional)
169    // ordering		= yes	# Is ordering defined for timestamps?
170    // 				# (optional, default: no)
171    // tsa_name		= yes	# Must the TSA name be included in the reply?
172    // 				# (optional, default: no)
173    // ess_cert_id_chain	= no	# Must the ESS cert id chain be included?
174    // 				# (optional, default: no)
175    // ess_cert_id_alg		= sha1	# algorithm to compute certificate
176    // 				# identifier (optional, default: sha1)
177
178    #[test]
179    fn request_test() {
180        // openssl ts --query --data abc.txt -out query.tsq
181        let enc_req = hex!("30400201013031300D060960864801650304020105000420BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD0208314CFCE4E0651827");
182        let req = TimeStampReq::from_der(&enc_req).unwrap();
183        assert_eq!(req.version, TspVersion::V1);
184        assert_eq!(req.message_imprint.hash_algorithm.oid, ID_SHA_256);
185        assert_eq!(
186            req.message_imprint.hashed_message.as_bytes(),
187            hex!("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
188        );
189        assert_eq!(req.nonce.unwrap().as_bytes(), hex!("314CFCE4E0651827"));
190    }
191    #[test]
192    fn response_test() {
193        // openssl ts -reply -queryfile query.tsq -signer ec384-tsa-key.crt -inkey ec384-tsa-key.pem -out response.tsr -config tsa.cnf
194        let enc_resp = hex!("3082028430030201003082027B06092A864886F70D010702A082026C30820268020103310F300D060960864801650304020105003081C9060B2A864886F70D0109100104A081B90481B63081B302010106042A0304013031300D060960864801650304020105000420BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD020104180F32303233303630373131323632365A300A020101800201F48101640101FF0208314CFCE4E0651827A048A4463044310B30090603550406130255533113301106035504080C0A536F6D652D5374617465310D300B060355040A0C04546573743111300F06035504030C0854657374205453413182018430820180020101305C3044310B30090603550406130255533113301106035504080C0A536F6D652D5374617465310D300B060355040A0C04546573743111300F06035504030C08546573742054534102146A0DCC59137C11D1C2B092042B4BC51C0D634D24300D06096086480165030402010500A08198301A06092A864886F70D010903310D060B2A864886F70D0109100104301C06092A864886F70D010905310F170D3233303630373131323632365A302B060B2A864886F70D010910020C311C301A3018301604142F36B1B52456F5AC3A1CA09794AE3D0D64AD38C2302F06092A864886F70D01090431220420BAF4CCF82E9B5B3956EADCC87346B407684F26D82B68D0E7DE0D31EA79AF648C300A06082A8648CE3D0403020467306502305A6E1C175B20A93FAB25D14CC5F5A2836D726D6D4A964B66FFBFFCE46276A96475F1408728B3385DCA37C2BA46BE17E1023100C46B7F08D03409A8ECCFD7637765412C3C5EC050E0D39CF48F0F5015950342CB18D8434FF331BA4463C086297C37D07B");
195        let resp = TimeStampResp::from_der(&enc_resp).unwrap();
196        let content = resp.time_stamp_token.unwrap().content;
197        let sd = SignedData::from_der(&content.to_der().unwrap()).unwrap();
198        let encap = sd.encap_content_info.econtent.unwrap();
199        let tst = TstInfo::from_der(&encap.value()).unwrap();
200        assert_eq!(resp.status.status, PkiStatus::Accepted);
201        assert_eq!(tst.version, TspVersion::V1);
202        assert_eq!(tst.policy.to_string(), "1.2.3.4.1");
203        assert_eq!(tst.message_imprint.hash_algorithm.oid, ID_SHA_256);
204        assert_eq!(tst.serial_number.as_bytes(), hex!("04"));
205        assert_eq!(tst.gen_time.to_unix_duration().as_secs(), 1686137186);
206        let accuracy = tst.accuracy.unwrap();
207        assert_eq!(accuracy.seconds.unwrap(), 1);
208        assert_eq!(accuracy.millis.unwrap(), 500);
209        assert_eq!(accuracy.micros.unwrap(), 100);
210        assert!(tst.ordering);
211        assert_eq!(tst.nonce.unwrap().as_bytes(), hex!("314CFCE4E0651827"));
212        let gn = tst.tsa.unwrap();
213        let dn = match gn {
214            GeneralName::DirectoryName(n) => n,
215            _ => panic!(),
216        };
217        assert_eq!(
218            dn.to_string(),
219            "C=US,STATEORPROVINCENAME=Some-State,O=Test,CN=Test TSA"
220        );
221        assert_eq!(
222            tst.message_imprint.hashed_message.as_bytes(),
223            hex!("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
224        );
225        assert!(tst.extensions.is_none());
226    }
227}