1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
//! Fluent builder for DER-encoded `TimeStampReq` (RFC 3161 §2.4.1).
//!
//! [`TimeStampReqBuilder`] assembles a complete `TimeStampReq` DER blob.
//! The caller supplies the hash algorithm OID (as a `&[u32]` component slice,
//! e.g. [`crate::oids::ID_SHA256`]) and the raw hash bytes.
//!
//! # Example
//!
//! ```rust,ignore
//! use synta_certificate::{TimeStampReqBuilder, oids};
//!
//! let hash = sha256_of(data);
//! let req_der = TimeStampReqBuilder::new()
//! .message_imprint(oids::ID_SHA256, &hash)
//! .nonce(b"\x01\x02\x03\x04\x05\x06\x07\x08")
//! .cert_req(true)
//! .build()
//! .unwrap();
//! ```
use synta::traits::Encode;
use crate::tsp_types::{MessageImprint, TimeStampReq};
// ── helpers ───────────────────────────────────────────────────────────────────
/// Encode a bare `AlgorithmIdentifier { algorithm OID, parameters NULL }` to DER.
fn encode_alg_id_null(oid_comps: &[u32]) -> Result<Vec<u8>, String> {
let oid = synta::ObjectIdentifier::new(oid_comps)
.map_err(|e| format!("invalid algorithm OID: {e}"))?;
let alg = crate::AlgorithmIdentifier {
algorithm: oid,
parameters: Some(synta::Element::Null(synta::Null)),
};
let mut enc = synta::Encoder::new(synta::Encoding::Der);
alg.encode(&mut enc)
.map_err(|e| format!("AlgorithmIdentifier encode failed: {e}"))?;
enc.finish()
.map_err(|e| format!("AlgorithmIdentifier finish failed: {e}"))
}
// ── TimeStampReqBuilder ───────────────────────────────────────────────────────
/// Fluent builder for a `TimeStampReq` DER structure (RFC 3161 §2.4.1).
///
/// Required field: `message_imprint`. All other fields are optional per the
/// RFC. Use [`crate::oids::ID_SHA256`] (or `ID_SHA384`, `ID_SHA512`, etc.)
/// as the hash algorithm OID component slice.
#[derive(Debug, Default)]
pub struct TimeStampReqBuilder {
/// Pre-encoded `MessageImprint` DER bytes.
message_imprint: Option<Vec<u8>>,
/// Optional `reqPolicy` OID component array.
req_policy: Option<Vec<u32>>,
/// Optional `nonce` bytes (big-endian INTEGER value).
nonce: Option<Vec<u8>>,
/// Optional `certReq` flag.
cert_req: Option<bool>,
/// First error encountered; subsequent builder calls are skipped.
error: Option<String>,
}
impl TimeStampReqBuilder {
/// Create a new, empty `TimeStampReqBuilder`.
pub fn new() -> Self {
Self::default()
}
/// Set the `messageImprint` from a hash algorithm OID and raw hash bytes.
///
/// `hash_alg_oid` is the OID component slice for the hash algorithm, e.g.
/// [`crate::oids::ID_SHA256`] for SHA-256. A `NULL` parameters field is
/// added automatically (standard for all SHA-2 hash algorithms).
/// `hashed_message` is the raw hash value (no OCTET STRING wrapper).
///
/// Use [`message_imprint_with_alg_der`](Self::message_imprint_with_alg_der)
/// to supply a pre-encoded `AlgorithmIdentifier` DER directly.
pub fn message_imprint(mut self, hash_alg_oid: &[u32], hashed_message: &[u8]) -> Self {
if self.error.is_some() {
return self;
}
let alg_bytes = match encode_alg_id_null(hash_alg_oid) {
Ok(b) => b,
Err(e) => {
self.error = Some(e);
return self;
}
};
self.set_message_imprint_from_alg_der(&alg_bytes, hashed_message)
}
/// Set the `messageImprint` from a pre-encoded `AlgorithmIdentifier` DER
/// TLV and raw hash bytes.
///
/// `alg_der` must be a complete `AlgorithmIdentifier` SEQUENCE TLV.
pub fn message_imprint_with_alg_der(self, alg_der: &[u8], hashed_message: &[u8]) -> Self {
if self.error.is_some() {
return self;
}
self.set_message_imprint_from_alg_der(alg_der, hashed_message)
}
fn set_message_imprint_from_alg_der(mut self, alg_der: &[u8], hashed_message: &[u8]) -> Self {
let alg_bytes = alg_der.to_vec();
let msg_bytes = hashed_message.to_vec();
let alg = match synta::Decoder::new(&alg_bytes, synta::Encoding::Der)
.decode::<crate::AlgorithmIdentifier<'_>>()
{
Ok(a) => a,
Err(e) => {
self.error = Some(format!("AlgorithmIdentifier decode failed: {e}"));
return self;
}
};
let hash_ref = synta::OctetStringRef::new(&msg_bytes);
let imprint = MessageImprint {
hash_algorithm: alg,
hashed_message: hash_ref,
};
let mut enc = synta::Encoder::new(synta::Encoding::Der);
match imprint.encode(&mut enc).and_then(|()| enc.finish()) {
Ok(bytes) => self.message_imprint = Some(bytes),
Err(e) => self.error = Some(format!("MessageImprint DER encoding failed: {e}")),
}
self
}
/// Set the optional `reqPolicy` OID by component slice.
///
/// Use this to request that the TSA use a specific policy.
pub fn req_policy(mut self, oid_components: &[u32]) -> Self {
if self.error.is_some() {
return self;
}
self.req_policy = Some(oid_components.to_vec());
self
}
/// Set the optional `nonce` value.
///
/// `nonce_bytes` is the big-endian two's-complement representation of
/// the nonce integer.
pub fn nonce(mut self, nonce_bytes: &[u8]) -> Self {
if self.error.is_some() {
return self;
}
self.nonce = Some(nonce_bytes.to_vec());
self
}
/// Set the `certReq` flag.
///
/// When `true`, requests that the TSA include its signing certificate
/// (and chain) in the time-stamp response.
pub fn cert_req(mut self, val: bool) -> Self {
if self.error.is_some() {
return self;
}
self.cert_req = Some(val);
self
}
/// Build the DER-encoded `TimeStampReq` SEQUENCE.
///
/// Returns `Err` if `message_imprint` was not set, if any OID was invalid,
/// or if DER encoding fails.
pub fn build(self) -> Result<Vec<u8>, String> {
if let Some(e) = self.error {
return Err(e);
}
let imprint_bytes = self
.message_imprint
.ok_or_else(|| "message_imprint is required".to_string())?;
// Re-decode the pre-encoded MessageImprint bytes.
let message_imprint = synta::Decoder::new(&imprint_bytes, synta::Encoding::Der)
.decode::<MessageImprint<'_>>()
.map_err(|e| format!("MessageImprint re-decode failed: {e}"))?;
let req_policy = if let Some(comps) = self.req_policy {
Some(
synta::ObjectIdentifier::new(&comps)
.map_err(|e| format!("invalid reqPolicy OID: {e}"))?,
)
} else {
None
};
let nonce = self.nonce.map(|b| synta::Integer::from_bytes(&b));
let cert_req = self.cert_req.map(synta::Boolean);
let req = TimeStampReq {
version: synta::Integer::from(1i64),
message_imprint,
req_policy,
nonce,
cert_req,
extensions: None,
};
let mut enc = synta::Encoder::new(synta::Encoding::Der);
req.encode(&mut enc)
.map_err(|e| format!("TimeStampReq DER encoding failed: {e}"))?;
enc.finish().map_err(|e| format!("DER finish failed: {e}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_minimal_req() {
let hash = [0xabu8; 32]; // fake SHA-256 hash
let der = TimeStampReqBuilder::new()
.message_imprint(crate::oids::ID_SHA256, &hash)
.build()
.expect("build should succeed");
let decoded =
crate::tsp_types::TimeStampReq::from_der(&der).expect("round-trip decode failed");
assert_eq!(decoded.version.as_i64().ok(), Some(1));
assert!(decoded.nonce.is_none());
assert!(decoded.cert_req.is_none());
}
#[test]
fn build_with_nonce_and_cert_req() {
let hash = [0xbbu8; 32];
let der = TimeStampReqBuilder::new()
.message_imprint(crate::oids::ID_SHA256, &hash)
.nonce(&[0x01, 0x02, 0x03, 0x04])
.cert_req(true)
.build()
.expect("build with nonce should succeed");
let decoded =
crate::tsp_types::TimeStampReq::from_der(&der).expect("round-trip decode failed");
assert!(decoded.nonce.is_some());
assert_eq!(decoded.cert_req, Some(synta::Boolean(true)));
}
#[test]
fn build_with_req_policy() {
let hash = [0xccu8; 32];
// Use a test policy OID (id-TEST-certPolicyOne = 1.3.6.1.5.5.7.13.1)
let policy_oid = &[1u32, 3, 6, 1, 5, 5, 7, 13, 1];
let der = TimeStampReqBuilder::new()
.message_imprint(crate::oids::ID_SHA256, &hash)
.req_policy(policy_oid)
.build()
.expect("build with req_policy should succeed");
let decoded =
crate::tsp_types::TimeStampReq::from_der(&der).expect("round-trip decode failed");
assert!(decoded.req_policy.is_some());
}
#[test]
fn missing_message_imprint_returns_error() {
let err = TimeStampReqBuilder::new().build();
assert!(err.is_err());
assert!(err.unwrap_err().contains("message_imprint"));
}
#[test]
fn invalid_policy_oid_returns_error() {
let hash = [0xddu8; 32];
// OID cannot start with arc > 2
let der = TimeStampReqBuilder::new()
.message_imprint(crate::oids::ID_SHA256, &hash)
.req_policy(&[99, 0, 1])
.build();
assert!(der.is_err());
}
}