rcgen/
string.rs

1//! ASN.1 string types
2
3use std::fmt;
4use std::str::FromStr;
5
6use crate::{Error, InvalidAsn1String};
7
8/// ASN.1 `PrintableString` type.
9///
10/// Supports a subset of the ASCII printable characters (described below).
11///
12/// For the full ASCII character set, use
13/// [`Ia5String`][`crate::Ia5String`].
14///
15/// # Examples
16///
17/// You can create a `PrintableString` from [a literal string][`&str`] with [`PrintableString::try_from`]:
18///
19/// ```
20/// use rcgen::string::PrintableString;
21/// let hello = PrintableString::try_from("hello").unwrap();
22/// ```
23///
24/// # Supported characters
25///
26/// PrintableString is a subset of the [ASCII printable characters].
27/// For instance, `'@'` is a printable character as per ASCII but can't be part of [ASN.1's `PrintableString`].
28///
29/// The following ASCII characters/ranges are supported:
30///
31/// - `A..Z`
32/// - `a..z`
33/// - `0..9`
34/// - "` `" (i.e. space)
35/// - `\`
36/// - `(`
37/// - `)`
38/// - `+`
39/// - `,`
40/// - `-`
41/// - `.`
42/// - `/`
43/// - `:`
44/// - `=`
45/// - `?`
46///
47/// [ASCII printable characters]: https://en.wikipedia.org/wiki/ASCII#Printable_characters
48/// [ASN.1's `PrintableString`]: https://en.wikipedia.org/wiki/PrintableString
49#[derive(Debug, PartialEq, Eq, Hash, Clone)]
50pub struct PrintableString(String);
51
52impl PrintableString {
53	/// Extracts a string slice containing the entire `PrintableString`.
54	pub fn as_str(&self) -> &str {
55		&self.0
56	}
57}
58
59impl TryFrom<&str> for PrintableString {
60	type Error = Error;
61
62	/// Converts a `&str` to a [`PrintableString`].
63	///
64	/// Any character not in the [`PrintableString`] charset will be rejected.
65	/// See [`PrintableString`] documentation for more information.
66	///
67	/// The result is allocated on the heap.
68	fn try_from(input: &str) -> Result<Self, Error> {
69		input.to_string().try_into()
70	}
71}
72
73impl TryFrom<String> for PrintableString {
74	type Error = Error;
75
76	/// Converts a [`String`][`std::string::String`] into a [`PrintableString`]
77	///
78	/// Any character not in the [`PrintableString`] charset will be rejected.
79	/// See [`PrintableString`] documentation for more information.
80	///
81	/// This conversion does not allocate or copy memory.
82	fn try_from(value: String) -> Result<Self, Self::Error> {
83		for &c in value.as_bytes() {
84			match c {
85				b'A'..=b'Z'
86				| b'a'..=b'z'
87				| b'0'..=b'9'
88				| b' '
89				| b'\''
90				| b'('
91				| b')'
92				| b'+'
93				| b','
94				| b'-'
95				| b'.'
96				| b'/'
97				| b':'
98				| b'='
99				| b'?' => (),
100				_ => {
101					return Err(Error::InvalidAsn1String(
102						InvalidAsn1String::PrintableString(value),
103					))
104				},
105			}
106		}
107		Ok(Self(value))
108	}
109}
110
111impl FromStr for PrintableString {
112	type Err = Error;
113
114	fn from_str(s: &str) -> Result<Self, Self::Err> {
115		s.try_into()
116	}
117}
118
119impl AsRef<str> for PrintableString {
120	fn as_ref(&self) -> &str {
121		&self.0
122	}
123}
124
125impl fmt::Display for PrintableString {
126	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127		fmt::Display::fmt(self.as_str(), f)
128	}
129}
130
131impl PartialEq<str> for PrintableString {
132	fn eq(&self, other: &str) -> bool {
133		self.as_str() == other
134	}
135}
136
137impl PartialEq<String> for PrintableString {
138	fn eq(&self, other: &String) -> bool {
139		self.as_str() == other.as_str()
140	}
141}
142
143impl PartialEq<&str> for PrintableString {
144	fn eq(&self, other: &&str) -> bool {
145		self.as_str() == *other
146	}
147}
148
149impl PartialEq<&String> for PrintableString {
150	fn eq(&self, other: &&String) -> bool {
151		self.as_str() == other.as_str()
152	}
153}
154
155/// ASN.1 `IA5String` type.
156///
157/// # Examples
158///
159/// You can create a `Ia5String` from [a literal string][`&str`] with [`Ia5String::try_from`]:
160///
161/// ```
162/// use rcgen::string::Ia5String;
163/// let hello = Ia5String::try_from("hello").unwrap();
164/// ```
165///
166/// # Supported characters
167///
168/// Supports the [International Alphabet No. 5 (IA5)] character encoding, i.e.
169/// the 128 characters of the ASCII alphabet. (Note: IA5 is now
170/// technically known as the International Reference Alphabet or IRA as
171/// specified in the ITU-T's T.50 recommendation).
172///
173/// For UTF-8, use [`String`][`std::string::String`].
174///
175/// [International Alphabet No. 5 (IA5)]: https://en.wikipedia.org/wiki/T.50_(standard)
176#[derive(Debug, PartialEq, Eq, Hash, Clone)]
177pub struct Ia5String(String);
178
179impl Ia5String {
180	/// Extracts a string slice containing the entire `Ia5String`.
181	pub fn as_str(&self) -> &str {
182		&self.0
183	}
184}
185
186impl TryFrom<&str> for Ia5String {
187	type Error = Error;
188
189	/// Converts a `&str` to a [`Ia5String`].
190	///
191	/// Any character not in the [`Ia5String`] charset will be rejected.
192	/// See [`Ia5String`] documentation for more information.
193	///
194	/// The result is allocated on the heap.
195	fn try_from(input: &str) -> Result<Self, Error> {
196		input.to_string().try_into()
197	}
198}
199
200impl TryFrom<String> for Ia5String {
201	type Error = Error;
202
203	/// Converts a [`String`][`std::string::String`] into a [`Ia5String`]
204	///
205	/// Any character not in the [`Ia5String`] charset will be rejected.
206	/// See [`Ia5String`] documentation for more information.
207	fn try_from(input: String) -> Result<Self, Error> {
208		if !input.is_ascii() {
209			return Err(Error::InvalidAsn1String(InvalidAsn1String::Ia5String(
210				input,
211			)));
212		}
213		Ok(Self(input))
214	}
215}
216
217impl FromStr for Ia5String {
218	type Err = Error;
219
220	fn from_str(s: &str) -> Result<Self, Self::Err> {
221		s.try_into()
222	}
223}
224
225impl AsRef<str> for Ia5String {
226	fn as_ref(&self) -> &str {
227		&self.0
228	}
229}
230
231impl fmt::Display for Ia5String {
232	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233		fmt::Display::fmt(self.as_str(), f)
234	}
235}
236
237impl PartialEq<str> for Ia5String {
238	fn eq(&self, other: &str) -> bool {
239		self.as_str() == other
240	}
241}
242
243impl PartialEq<String> for Ia5String {
244	fn eq(&self, other: &String) -> bool {
245		self.as_str() == other.as_str()
246	}
247}
248
249impl PartialEq<&str> for Ia5String {
250	fn eq(&self, other: &&str) -> bool {
251		self.as_str() == *other
252	}
253}
254
255impl PartialEq<&String> for Ia5String {
256	fn eq(&self, other: &&String) -> bool {
257		self.as_str() == other.as_str()
258	}
259}
260
261/// ASN.1 `TeletexString` type.
262///
263/// # Examples
264///
265/// You can create a `TeletexString` from [a literal string][`&str`] with [`TeletexString::try_from`]:
266///
267/// ```
268/// use rcgen::string::TeletexString;
269/// let hello = TeletexString::try_from("hello").unwrap();
270/// ```
271///
272/// # Supported characters
273///
274/// The standard defines a complex character set allowed in this type. However, quoting the ASN.1
275/// [mailing list], "a sizable volume of software in the world treats TeletexString (T61String) as a
276/// simple 8-bit string with mostly Windows Latin 1 (superset of iso-8859-1) encoding".
277///
278/// `TeletexString` is included for backward compatibility, [RFC 5280] say it
279/// SHOULD NOT be used for certificates for new subjects.
280///
281/// [mailing list]: https://www.mail-archive.com/asn1@asn1.org/msg00460.html
282/// [RFC 5280]: https://datatracker.ietf.org/doc/html/rfc5280#page-25
283#[derive(Debug, PartialEq, Eq, Hash, Clone)]
284pub struct TeletexString(String);
285
286impl TeletexString {
287	/// Extracts a string slice containing the entire `TeletexString`.
288	pub fn as_str(&self) -> &str {
289		&self.0
290	}
291
292	/// Returns a byte slice of this `TeletexString`’s contents.
293	pub fn as_bytes(&self) -> &[u8] {
294		self.0.as_bytes()
295	}
296}
297
298impl TryFrom<&str> for TeletexString {
299	type Error = Error;
300
301	/// Converts a `&str` to a [`TeletexString`].
302	///
303	/// Any character not in the [`TeletexString`] charset will be rejected.
304	/// See [`TeletexString`] documentation for more information.
305	///
306	/// The result is allocated on the heap.
307	fn try_from(input: &str) -> Result<Self, Error> {
308		input.to_string().try_into()
309	}
310}
311
312impl TryFrom<String> for TeletexString {
313	type Error = Error;
314
315	/// Converts a [`String`][`std::string::String`] into a [`TeletexString`]
316	///
317	/// Any character not in the [`TeletexString`] charset will be rejected.
318	/// See [`TeletexString`] documentation for more information.
319	///
320	/// This conversion does not allocate or copy memory.
321	fn try_from(input: String) -> Result<Self, Error> {
322		// Check all bytes are visible
323		if !input.as_bytes().iter().all(|b| (0x20..=0x7f).contains(b)) {
324			return Err(Error::InvalidAsn1String(InvalidAsn1String::TeletexString(
325				input,
326			)));
327		}
328		Ok(Self(input))
329	}
330}
331
332impl FromStr for TeletexString {
333	type Err = Error;
334
335	fn from_str(s: &str) -> Result<Self, Self::Err> {
336		s.try_into()
337	}
338}
339
340impl AsRef<str> for TeletexString {
341	fn as_ref(&self) -> &str {
342		&self.0
343	}
344}
345
346impl fmt::Display for TeletexString {
347	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348		fmt::Display::fmt(self.as_str(), f)
349	}
350}
351
352impl PartialEq<str> for TeletexString {
353	fn eq(&self, other: &str) -> bool {
354		self.as_str() == other
355	}
356}
357
358impl PartialEq<String> for TeletexString {
359	fn eq(&self, other: &String) -> bool {
360		self.as_str() == other.as_str()
361	}
362}
363
364impl PartialEq<&str> for TeletexString {
365	fn eq(&self, other: &&str) -> bool {
366		self.as_str() == *other
367	}
368}
369
370impl PartialEq<&String> for TeletexString {
371	fn eq(&self, other: &&String) -> bool {
372		self.as_str() == other.as_str()
373	}
374}
375
376/// ASN.1 `BMPString` type.
377///
378/// # Examples
379///
380/// You can create a `BmpString` from [a literal string][`&str`] with [`BmpString::try_from`]:
381///
382/// ```
383/// use rcgen::string::BmpString;
384/// let hello = BmpString::try_from("hello").unwrap();
385/// ```
386///
387/// # Supported characters
388///
389/// Encodes Basic Multilingual Plane (BMP) subset of Unicode (ISO 10646),
390/// a.k.a. UCS-2.
391///
392/// Bytes are encoded as UTF-16 big-endian.
393///
394/// `BMPString` is included for backward compatibility, [RFC 5280] say it
395/// SHOULD NOT be used for certificates for new subjects.
396///
397/// [RFC 5280]: https://datatracker.ietf.org/doc/html/rfc5280#page-25
398#[derive(Debug, PartialEq, Eq, Hash, Clone)]
399pub struct BmpString(Vec<u8>);
400
401impl BmpString {
402	/// Returns a byte slice of this `BmpString`'s contents.
403	///
404	/// The inverse of this method is [`from_utf16be`].
405	///
406	/// [`from_utf16be`]: BmpString::from_utf16be
407	///
408	/// # Examples
409	///
410	/// ```
411	/// use rcgen::string::BmpString;
412	/// let s = BmpString::try_from("hello").unwrap();
413	///
414	/// assert_eq!(&[0, 104, 0, 101, 0, 108, 0, 108, 0, 111], s.as_bytes());
415	/// ```
416	pub fn as_bytes(&self) -> &[u8] {
417		&self.0
418	}
419
420	/// Decode a UTF-16BE–encoded vector `vec` into a `BmpString`, returning [Err](`std::result::Result::Err`) if `vec` contains any invalid data.
421	pub fn from_utf16be(vec: Vec<u8>) -> Result<Self, Error> {
422		if vec.len() % 2 != 0 {
423			return Err(Error::InvalidAsn1String(InvalidAsn1String::BmpString(
424				"Invalid UTF-16 encoding".to_string(),
425			)));
426		}
427
428		// FIXME: Update this when `array_chunks` is stabilized.
429		for maybe_char in char::decode_utf16(
430			vec.chunks_exact(2)
431				.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]])),
432		) {
433			// We check we only use the BMP subset of Unicode (the first 65 536 code points)
434			match maybe_char {
435				// Character is in the Basic Multilingual Plane
436				Ok(c) if (c as u64) < u64::from(u16::MAX) => (),
437				// Characters outside Basic Multilingual Plane or unpaired surrogates
438				_ => {
439					return Err(Error::InvalidAsn1String(InvalidAsn1String::BmpString(
440						"Invalid UTF-16 encoding".to_string(),
441					)));
442				},
443			}
444		}
445		Ok(Self(vec.to_vec()))
446	}
447}
448
449impl TryFrom<&str> for BmpString {
450	type Error = Error;
451
452	/// Converts a `&str` to a [`BmpString`].
453	///
454	/// Any character not in the [`BmpString`] charset will be rejected.
455	/// See [`BmpString`] documentation for more information.
456	///
457	/// The result is allocated on the heap.
458	fn try_from(value: &str) -> Result<Self, Self::Error> {
459		let capacity = value.len().checked_mul(2).ok_or_else(|| {
460			Error::InvalidAsn1String(InvalidAsn1String::BmpString(value.to_string()))
461		})?;
462
463		let mut bytes = Vec::with_capacity(capacity);
464
465		for code_point in value.encode_utf16() {
466			bytes.extend(code_point.to_be_bytes());
467		}
468
469		BmpString::from_utf16be(bytes)
470	}
471}
472
473impl TryFrom<String> for BmpString {
474	type Error = Error;
475
476	/// Converts a [`String`][`std::string::String`] into a [`BmpString`]
477	///
478	/// Any character not in the [`BmpString`] charset will be rejected.
479	/// See [`BmpString`] documentation for more information.
480	///
481	/// Parsing a `BmpString` allocates memory since the UTF-8 to UTF-16 conversion requires a memory allocation.
482	fn try_from(value: String) -> Result<Self, Self::Error> {
483		value.as_str().try_into()
484	}
485}
486
487impl FromStr for BmpString {
488	type Err = Error;
489
490	fn from_str(s: &str) -> Result<Self, Self::Err> {
491		s.try_into()
492	}
493}
494
495/// ASN.1 `UniversalString` type.
496///
497/// # Examples
498///
499/// You can create a `UniversalString` from [a literal string][`&str`] with [`UniversalString::try_from`]:
500///
501/// ```
502/// use rcgen::string::UniversalString;
503/// let hello = UniversalString::try_from("hello").unwrap();
504/// ```
505///
506/// # Supported characters
507///
508/// The characters which can appear in the `UniversalString` type are any of the characters allowed by
509/// ISO/IEC 10646 (Unicode).
510///
511/// Bytes are encoded like UTF-32 big-endian.
512///
513/// `UniversalString` is included for backward compatibility, [RFC 5280] say it
514/// SHOULD NOT be used for certificates for new subjects.
515///
516/// [RFC 5280]: https://datatracker.ietf.org/doc/html/rfc5280#page-25
517#[derive(Debug, PartialEq, Eq, Hash, Clone)]
518pub struct UniversalString(Vec<u8>);
519
520impl UniversalString {
521	/// Returns a byte slice of this `UniversalString`'s contents.
522	///
523	/// The inverse of this method is [`from_utf32be`].
524	///
525	/// [`from_utf32be`]: UniversalString::from_utf32be
526	///
527	/// # Examples
528	///
529	/// ```
530	/// use rcgen::string::UniversalString;
531	/// let s = UniversalString::try_from("hello").unwrap();
532	///
533	/// assert_eq!(&[0, 0, 0, 104, 0, 0, 0, 101, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 111], s.as_bytes());
534	/// ```
535	pub fn as_bytes(&self) -> &[u8] {
536		&self.0
537	}
538
539	/// Decode a UTF-32BE–encoded vector `vec` into a `UniversalString`, returning [Err](`std::result::Result::Err`) if `vec` contains any invalid data.
540	pub fn from_utf32be(vec: Vec<u8>) -> Result<UniversalString, Error> {
541		if vec.len() % 4 != 0 {
542			return Err(Error::InvalidAsn1String(
543				InvalidAsn1String::UniversalString("Invalid UTF-32 encoding".to_string()),
544			));
545		}
546
547		// FIXME: Update this when `array_chunks` is stabilized.
548		for maybe_char in vec
549			.chunks_exact(4)
550			.map(|chunk| u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
551		{
552			if core::char::from_u32(maybe_char).is_none() {
553				return Err(Error::InvalidAsn1String(
554					InvalidAsn1String::UniversalString("Invalid UTF-32 encoding".to_string()),
555				));
556			}
557		}
558
559		Ok(Self(vec))
560	}
561}
562
563impl TryFrom<&str> for UniversalString {
564	type Error = Error;
565
566	/// Converts a `&str` to a [`UniversalString`].
567	///
568	/// Any character not in the [`UniversalString`] charset will be rejected.
569	/// See [`UniversalString`] documentation for more information.
570	///
571	/// The result is allocated on the heap.
572	fn try_from(value: &str) -> Result<Self, Self::Error> {
573		let capacity = value.len().checked_mul(4).ok_or_else(|| {
574			Error::InvalidAsn1String(InvalidAsn1String::UniversalString(value.to_string()))
575		})?;
576
577		let mut bytes = Vec::with_capacity(capacity);
578
579		// A `char` is any ‘Unicode code point’ other than a surrogate code point.
580		// The code units for UTF-32 correspond exactly to Unicode code points.
581		// (https://www.unicode.org/reports/tr19/tr19-9.html#Introduction)
582		// So any `char` is a valid UTF-32, we just cast it to perform the conversion.
583		for char in value.chars().map(|char| char as u32) {
584			bytes.extend(char.to_be_bytes())
585		}
586
587		UniversalString::from_utf32be(bytes)
588	}
589}
590
591impl TryFrom<String> for UniversalString {
592	type Error = Error;
593
594	/// Converts a [`String`][`std::string::String`] into a [`UniversalString`]
595	///
596	/// Any character not in the [`UniversalString`] charset will be rejected.
597	/// See [`UniversalString`] documentation for more information.
598	///
599	/// Parsing a `UniversalString` allocates memory since the UTF-8 to UTF-32 conversion requires a memory allocation.
600	fn try_from(value: String) -> Result<Self, Self::Error> {
601		value.as_str().try_into()
602	}
603}
604
605#[cfg(test)]
606#[allow(clippy::unwrap_used)]
607mod tests {
608
609	use crate::{BmpString, Ia5String, PrintableString, TeletexString, UniversalString};
610
611	#[test]
612	fn printable_string() {
613		const EXAMPLE_UTF8: &str = "CertificateTemplate";
614		let printable_string = PrintableString::try_from(EXAMPLE_UTF8).unwrap();
615		assert_eq!(printable_string, EXAMPLE_UTF8);
616		assert!(PrintableString::try_from("@").is_err());
617		assert!(PrintableString::try_from("*").is_err());
618	}
619
620	#[test]
621	fn ia5_string() {
622		const EXAMPLE_UTF8: &str = "CertificateTemplate";
623		let ia5_string = Ia5String::try_from(EXAMPLE_UTF8).unwrap();
624		assert_eq!(ia5_string, EXAMPLE_UTF8);
625		assert!(Ia5String::try_from(String::from('\u{7F}')).is_ok());
626		assert!(Ia5String::try_from(String::from('\u{8F}')).is_err());
627	}
628
629	#[test]
630	fn teletext_string() {
631		const EXAMPLE_UTF8: &str = "CertificateTemplate";
632		let teletext_string = TeletexString::try_from(EXAMPLE_UTF8).unwrap();
633		assert_eq!(teletext_string, EXAMPLE_UTF8);
634		assert!(Ia5String::try_from(String::from('\u{7F}')).is_ok());
635		assert!(Ia5String::try_from(String::from('\u{8F}')).is_err());
636	}
637
638	#[test]
639	fn bmp_string() {
640		const EXPECTED_BYTES: &[u8] = &[
641			0x00, 0x43, 0x00, 0x65, 0x00, 0x72, 0x00, 0x74, 0x00, 0x69, 0x00, 0x66, 0x00, 0x69,
642			0x00, 0x63, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, 0x00, 0x54, 0x00, 0x65, 0x00, 0x6d,
643			0x00, 0x70, 0x00, 0x6c, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65,
644		];
645		const EXAMPLE_UTF8: &str = "CertificateTemplate";
646		let bmp_string = BmpString::try_from(EXAMPLE_UTF8).unwrap();
647		assert_eq!(bmp_string.as_bytes(), EXPECTED_BYTES);
648		assert!(BmpString::try_from(String::from('\u{FFFE}')).is_ok());
649		assert!(BmpString::try_from(String::from('\u{FFFF}')).is_err());
650	}
651
652	#[test]
653	fn universal_string() {
654		const EXPECTED_BYTES: &[u8] = &[
655			0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00,
656			0x00, 0x74, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x69,
657			0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00,
658			0x00, 0x65, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x6d,
659			0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00,
660			0x00, 0x74, 0x00, 0x00, 0x00, 0x65,
661		];
662		const EXAMPLE_UTF8: &str = "CertificateTemplate";
663		let universal_string = UniversalString::try_from(EXAMPLE_UTF8).unwrap();
664		assert_eq!(universal_string.as_bytes(), EXPECTED_BYTES);
665	}
666}