noxtls_pem/lib.rs
1// Copyright (c) 2019-2026, Argenox Technologies LLC
2// All rights reserved.
3//
4// SPDX-License-Identifier: GPL-2.0-only OR LicenseRef-Argenox-Commercial-License
5//
6// This file is part of the NoxTLS Library.
7//
8// This program is free software: you can redistribute it and/or modify
9// it under the terms of the GNU General Public License as published by the
10// Free Software Foundation; version 2 of the License.
11//
12// Alternatively, this file may be used under the terms of a commercial
13// license from Argenox Technologies LLC.
14//
15// See `noxtls/LICENSE` and `noxtls/LICENSE.md` in this repository for full details.
16// CONTACT: info@argenox.com
17
18#![cfg_attr(not(feature = "std"), no_std)]
19#![forbid(unsafe_code)]
20#![allow(clippy::incompatible_msrv)]
21
22//! PEM encoding and decoding helpers for certificates, keys, and generic DER payloads.
23//!
24//! Supports `no_std` builds with `alloc` and optional `std` helpers for filesystem I/O.
25
26#[cfg(not(feature = "std"))]
27#[macro_use]
28extern crate alloc;
29
30mod internal_alloc;
31
32#[cfg(not(feature = "std"))]
33use crate::internal_alloc::ToOwned;
34use crate::internal_alloc::{String, Vec};
35use noxtls_core::{Error, Result};
36#[cfg(feature = "std")]
37use std::path::Path;
38
39const BASE64_ALPHABET: &[u8; 64] =
40 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
41const PEM_LABEL_CERTIFICATE: &str = "CERTIFICATE";
42const PEM_LABEL_RSA_PRIVATE_KEY: &str = "RSA PRIVATE KEY";
43const PEM_LABEL_RSA_PUBLIC_KEY: &str = "RSA PUBLIC KEY";
44const PEM_LABEL_PRIVATE_KEY: &str = "PRIVATE KEY";
45const PEM_LABEL_EC_PRIVATE_KEY: &str = "EC PRIVATE KEY";
46const PEM_LABEL_PUBLIC_KEY: &str = "PUBLIC KEY";
47
48/// Converts certificate DER bytes into PEM `CERTIFICATE` armor.
49///
50/// # Arguments
51///
52/// * `der`: Raw DER certificate bytes.
53///
54/// # Returns
55///
56/// PEM text using `CERTIFICATE` label.
57///
58/// # Errors
59///
60/// Returns the same errors as [`der_to_pem`].
61///
62/// # Panics
63///
64/// This function does not panic.
65pub fn certificate_der_to_pem(der: &[u8]) -> Result<String> {
66 der_to_pem(der, PEM_LABEL_CERTIFICATE)
67}
68
69/// Parses one PEM `CERTIFICATE` block into DER bytes.
70///
71/// # Arguments
72///
73/// * `pem`: PEM certificate text containing exactly one certificate block.
74///
75/// # Returns
76///
77/// Raw DER certificate bytes.
78///
79/// # Errors
80///
81/// Returns the same errors as [`pem_to_der`].
82///
83/// # Panics
84///
85/// This function does not panic.
86pub fn certificate_pem_to_der(pem: &str) -> Result<Vec<u8>> {
87 pem_to_der(pem, PEM_LABEL_CERTIFICATE)
88}
89
90/// Parses all PEM `CERTIFICATE` blocks into DER bytes.
91///
92/// # Arguments
93///
94/// * `pem`: PEM text that may include certificate chains or mixed labels.
95///
96/// # Returns
97///
98/// DER certificate blocks for every `CERTIFICATE` marker in input order.
99///
100/// # Errors
101///
102/// Returns the same errors as [`pem_to_der_blocks`].
103///
104/// # Panics
105///
106/// This function does not panic.
107pub fn certificate_chain_pem_to_der_blocks(pem: &str) -> Result<Vec<Vec<u8>>> {
108 pem_to_der_blocks(pem, PEM_LABEL_CERTIFICATE)
109}
110
111/// Converts PKCS#1 RSA private-key DER bytes into PEM armor.
112///
113/// # Arguments
114///
115/// * `der`: DER bytes for `RSAPrivateKey`.
116///
117/// # Returns
118///
119/// PEM text using `RSA PRIVATE KEY` label.
120///
121/// # Errors
122///
123/// Returns the same errors as [`der_to_pem`].
124///
125/// # Panics
126///
127/// This function does not panic.
128pub fn rsa_private_key_der_to_pem_pkcs1(der: &[u8]) -> Result<String> {
129 der_to_pem(der, PEM_LABEL_RSA_PRIVATE_KEY)
130}
131
132/// Parses one PEM PKCS#1 RSA private-key block into DER bytes.
133///
134/// # Arguments
135///
136/// * `pem`: PEM text containing exactly one `RSA PRIVATE KEY` block.
137///
138/// # Returns
139///
140/// DER bytes for `RSAPrivateKey`.
141///
142/// # Errors
143///
144/// Returns the same errors as [`pem_to_der`].
145///
146/// # Panics
147///
148/// This function does not panic.
149pub fn rsa_private_key_pem_to_der_pkcs1(pem: &str) -> Result<Vec<u8>> {
150 pem_to_der(pem, PEM_LABEL_RSA_PRIVATE_KEY)
151}
152
153/// Converts PKCS#1 RSA public-key DER bytes into PEM armor.
154///
155/// # Arguments
156///
157/// * `der`: DER bytes for `RSAPublicKey`.
158///
159/// # Returns
160///
161/// PEM text using `RSA PUBLIC KEY` label.
162///
163/// # Errors
164///
165/// Returns the same errors as [`der_to_pem`].
166///
167/// # Panics
168///
169/// This function does not panic.
170pub fn rsa_public_key_der_to_pem_pkcs1(der: &[u8]) -> Result<String> {
171 der_to_pem(der, PEM_LABEL_RSA_PUBLIC_KEY)
172}
173
174/// Parses one PEM PKCS#1 RSA public-key block into DER bytes.
175///
176/// # Arguments
177///
178/// * `pem`: PEM text containing exactly one `RSA PUBLIC KEY` block.
179///
180/// # Returns
181/// DER bytes for `RSAPublicKey`.
182///
183/// # Errors
184///
185/// Returns the same errors as [`pem_to_der`].
186///
187/// # Panics
188///
189/// This function does not panic.
190pub fn rsa_public_key_pem_to_der_pkcs1(pem: &str) -> Result<Vec<u8>> {
191 pem_to_der(pem, PEM_LABEL_RSA_PUBLIC_KEY)
192}
193
194/// Converts PKCS#8 private-key DER bytes into PEM armor.
195///
196/// # Arguments
197///
198/// * `der`: DER bytes for `PrivateKeyInfo`.
199///
200/// # Returns
201/// PEM text using `PRIVATE KEY` label.
202///
203/// # Errors
204///
205/// Returns the same errors as [`der_to_pem`].
206///
207/// # Panics
208///
209/// This function does not panic.
210pub fn private_key_der_to_pem_pkcs8(der: &[u8]) -> Result<String> {
211 der_to_pem(der, PEM_LABEL_PRIVATE_KEY)
212}
213
214/// Parses one PEM PKCS#8 private-key block into DER bytes.
215///
216/// # Arguments
217///
218/// * `pem`: PEM text containing exactly one `PRIVATE KEY` block.
219///
220/// # Returns
221/// DER bytes for `PrivateKeyInfo`.
222///
223/// # Errors
224///
225/// Returns the same errors as [`pem_to_der`].
226///
227/// # Panics
228///
229/// This function does not panic.
230pub fn private_key_pem_to_der_pkcs8(pem: &str) -> Result<Vec<u8>> {
231 pem_to_der(pem, PEM_LABEL_PRIVATE_KEY)
232}
233
234/// Converts SEC1 EC private-key DER bytes into PEM armor.
235///
236/// # Arguments
237///
238/// * `der`: DER bytes for SEC1 `ECPrivateKey`.
239///
240/// # Returns
241/// PEM text using `EC PRIVATE KEY` label.
242///
243/// # Errors
244///
245/// Returns the same errors as [`der_to_pem`].
246///
247/// # Panics
248///
249/// This function does not panic.
250pub fn ec_private_key_der_to_pem_sec1(der: &[u8]) -> Result<String> {
251 der_to_pem(der, PEM_LABEL_EC_PRIVATE_KEY)
252}
253
254/// Parses one PEM SEC1 EC private-key block into DER bytes.
255///
256/// # Arguments
257///
258/// * `pem`: PEM text containing exactly one `EC PRIVATE KEY` block.
259///
260/// # Returns
261///
262/// DER bytes for SEC1 `ECPrivateKey`.
263///
264/// # Errors
265///
266/// Returns the same errors as [`pem_to_der`].
267///
268/// # Panics
269///
270/// This function does not panic.
271pub fn ec_private_key_pem_to_der_sec1(pem: &str) -> Result<Vec<u8>> {
272 pem_to_der(pem, PEM_LABEL_EC_PRIVATE_KEY)
273}
274
275/// Converts SubjectPublicKeyInfo DER bytes into PEM armor.
276///
277/// # Arguments
278///
279/// * `der`: DER bytes for public key SPKI structure.
280///
281/// # Returns
282/// PEM text using `PUBLIC KEY` label.
283///
284/// # Errors
285///
286/// Returns the same errors as [`der_to_pem`].
287///
288/// # Panics
289///
290/// This function does not panic.
291pub fn public_key_der_to_pem_spki(der: &[u8]) -> Result<String> {
292 der_to_pem(der, PEM_LABEL_PUBLIC_KEY)
293}
294
295/// Parses one PEM SPKI public-key block into DER bytes.
296///
297/// # Arguments
298///
299/// * `pem`: PEM text containing exactly one `PUBLIC KEY` block.
300///
301/// # Returns
302/// DER bytes for subject public key info.
303///
304/// # Errors
305///
306/// Returns the same errors as [`pem_to_der`].
307///
308/// # Panics
309///
310/// This function does not panic.
311pub fn public_key_pem_to_der_spki(pem: &str) -> Result<Vec<u8>> {
312 pem_to_der(pem, PEM_LABEL_PUBLIC_KEY)
313}
314
315/// Reads one PEM block from file and decodes DER payload for `label`.
316///
317/// # Arguments
318///
319/// * `path`: Filesystem path to a PEM file.
320/// * `label`: Expected PEM label.
321///
322/// # Returns
323///
324/// DER bytes for exactly one matching PEM block.
325///
326/// # Errors
327///
328/// Returns [`Error::ParseFailure`] if the file cannot be read as UTF-8; otherwise the same errors as [`pem_to_der`].
329///
330/// # Panics
331///
332/// This function does not panic.
333#[cfg(feature = "std")]
334pub fn pem_file_to_der(path: &Path, label: &str) -> Result<Vec<u8>> {
335 let pem = std::fs::read_to_string(path)
336 .map_err(|_| Error::ParseFailure("failed to read pem file"))?;
337 pem_to_der(&pem, label)
338}
339
340/// Reads all matching PEM blocks from file and decodes DER payloads for `label`.
341///
342/// # Arguments
343///
344/// * `path`: Filesystem path to a PEM file.
345/// * `label`: Expected PEM label.
346///
347/// # Returns
348/// DER bytes for each matching PEM block.
349///
350/// # Errors
351///
352/// Returns [`Error::ParseFailure`] if the file cannot be read, otherwise the same errors as [`pem_to_der_blocks`].
353///
354/// # Panics
355///
356/// This function does not panic.
357#[cfg(feature = "std")]
358pub fn pem_file_to_der_blocks(path: &Path, label: &str) -> Result<Vec<Vec<u8>>> {
359 let pem = std::fs::read_to_string(path)
360 .map_err(|_| Error::ParseFailure("failed to read pem file"))?;
361 pem_to_der_blocks(&pem, label)
362}
363
364/// Encodes DER as PEM and writes it to a file path.
365///
366/// # Arguments
367///
368/// * `path`: Destination path for PEM text.
369/// * `der`: DER bytes to encode.
370/// * `label`: PEM label to apply.
371///
372/// # Returns
373///
374/// `Ok(())` after the PEM text is written to `path`.
375///
376/// # Errors
377///
378/// Returns errors from [`der_to_pem`], or [`Error::ParseFailure`] if the file cannot be written.
379///
380/// # Panics
381///
382/// This function does not panic.
383#[cfg(feature = "std")]
384pub fn der_to_pem_file(path: &Path, der: &[u8], label: &str) -> Result<()> {
385 let pem = der_to_pem(der, label)?;
386 std::fs::write(path, pem).map_err(|_| Error::ParseFailure("failed to write pem file"))?;
387 Ok(())
388}
389
390/// Writes raw DER bytes to a file path.
391///
392/// # Arguments
393///
394/// * `path`: Destination path for DER bytes.
395/// * `der`: DER bytes to write.
396///
397/// # Returns
398///
399/// `Ok(())` on successful write.
400///
401/// # Errors
402///
403/// Returns [`Error::InvalidLength`] for empty `der`, or [`Error::ParseFailure`] if the file cannot be written.
404///
405/// # Panics
406///
407/// This function does not panic.
408#[cfg(feature = "std")]
409pub fn der_to_file(path: &Path, der: &[u8]) -> Result<()> {
410 if der.is_empty() {
411 return Err(Error::InvalidLength("der input must not be empty"));
412 }
413 std::fs::write(path, der).map_err(|_| Error::ParseFailure("failed to write der file"))?;
414 Ok(())
415}
416
417/// Converts DER bytes into PEM armor with caller-provided label.
418///
419/// # Arguments
420///
421/// * `der`: Raw DER bytes to encode.
422/// * `label`: PEM label such as `CERTIFICATE` or `PUBLIC KEY`.
423///
424/// # Returns
425///
426/// UTF-8 PEM text including BEGIN/END markers and 64-column base64 lines.
427///
428/// # Errors
429///
430/// Returns [`Error::InvalidLength`] when `der` or `label` is empty, or [`Error::InvalidEncoding`] when `label` contains control characters.
431///
432/// # Panics
433///
434/// Panics if an internal `expect` on ASCII-only base64 chunk UTF-8 conversion fails (should be unreachable).
435pub fn der_to_pem(der: &[u8], label: &str) -> Result<String> {
436 if der.is_empty() {
437 return Err(Error::InvalidLength("der input must not be empty"));
438 }
439 if label.is_empty() {
440 return Err(Error::InvalidLength("pem label must not be empty"));
441 }
442 if label.chars().any(char::is_control) {
443 return Err(Error::InvalidEncoding(
444 "pem label contains invalid control character",
445 ));
446 }
447 let encoded = encode_base64(der);
448 let mut pem = String::new();
449 pem.push_str("-----BEGIN ");
450 pem.push_str(label);
451 pem.push_str("-----\n");
452 for chunk in encoded.as_bytes().chunks(64) {
453 let line =
454 core::str::from_utf8(chunk).expect("base64 output is always valid ascii and utf-8");
455 pem.push_str(line);
456 pem.push('\n');
457 }
458 pem.push_str("-----END ");
459 pem.push_str(label);
460 pem.push_str("-----\n");
461 Ok(pem)
462}
463
464/// Parses PEM armor into DER bytes and verifies expected label markers.
465///
466/// # Arguments
467///
468/// * `pem`: PEM text to parse.
469/// * `label`: Expected PEM label, for example `CERTIFICATE`.
470///
471/// # Returns
472///
473/// Raw DER bytes extracted from the single matching PEM payload.
474///
475/// # Errors
476///
477/// Returns errors from [`pem_to_der_blocks`], or [`Error::ParseFailure`] when zero or multiple blocks match `label`.
478///
479/// # Panics
480///
481/// This function does not panic.
482pub fn pem_to_der(pem: &str, label: &str) -> Result<Vec<u8>> {
483 let blocks = pem_to_der_blocks(pem, label)?;
484 if blocks.len() != 1 {
485 return Err(Error::ParseFailure(
486 "expected exactly one pem block for requested label",
487 ));
488 }
489 Ok(blocks[0].clone())
490}
491
492/// Parses all PEM blocks matching `label` into DER payload bytes.
493///
494/// # Arguments
495///
496/// * `pem`: PEM text to scan.
497/// * `label`: PEM label to collect, such as `CERTIFICATE`.
498///
499/// # Returns
500///
501/// Vector of DER payloads for each matching PEM block in input order.
502///
503/// # Errors
504///
505/// Returns [`Error::InvalidLength`] for empty `pem` or `label`, [`Error::InvalidEncoding`] for invalid labels or base64,
506/// or [`Error::ParseFailure`] for malformed markers, nesting, mismatched begin/end, missing end, or no matching blocks.
507///
508/// # Panics
509///
510/// This function does not panic.
511pub fn pem_to_der_blocks(pem: &str, label: &str) -> Result<Vec<Vec<u8>>> {
512 if pem.is_empty() {
513 return Err(Error::InvalidLength("pem input must not be empty"));
514 }
515 if label.is_empty() {
516 return Err(Error::InvalidLength("pem label must not be empty"));
517 }
518 if label.chars().any(char::is_control) {
519 return Err(Error::InvalidEncoding(
520 "pem label contains invalid control character",
521 ));
522 }
523
524 let mut active_label: Option<String> = None;
525 let mut payload = String::new();
526 let mut out = Vec::new();
527
528 for line in pem.lines() {
529 let trimmed = line.trim();
530 if trimmed.is_empty() {
531 continue;
532 }
533 if let Some(begin_label) = parse_pem_marker_label(trimmed, "BEGIN ") {
534 if active_label.is_some() {
535 return Err(Error::ParseFailure("nested pem begin marker"));
536 }
537 active_label = Some(begin_label.to_owned());
538 payload.clear();
539 continue;
540 }
541 if let Some(end_label) = parse_pem_marker_label(trimmed, "END ") {
542 let current_label = active_label
543 .as_deref()
544 .ok_or(Error::ParseFailure("pem end marker appears before begin"))?;
545 if current_label != end_label {
546 return Err(Error::ParseFailure("pem begin/end label mismatch"));
547 }
548 if current_label == label {
549 if payload.is_empty() {
550 return Err(Error::InvalidEncoding("pem payload is empty"));
551 }
552 out.push(decode_base64(&payload)?);
553 }
554 active_label = None;
555 payload.clear();
556 continue;
557 }
558 if active_label.is_none() {
559 continue;
560 }
561 payload.push_str(trimmed);
562 }
563
564 if active_label.is_some() {
565 return Err(Error::ParseFailure("pem end marker missing"));
566 }
567 if out.is_empty() {
568 return Err(Error::ParseFailure("pem begin/end markers not found"));
569 }
570 Ok(out)
571}
572
573/// Parses a PEM boundary line and returns the label between markers and `marker_kind`.
574///
575/// # Arguments
576///
577/// * `line` — Trimmed PEM line, for example `-----BEGIN CERTIFICATE-----`.
578/// * `marker_kind` — Either `"BEGIN "` or `"END "` including trailing space.
579///
580/// # Returns
581///
582/// `Some(label)` when the line matches `-----{marker_kind}{label}-----`; otherwise `None`.
583///
584/// # Panics
585///
586/// This function does not panic.
587fn parse_pem_marker_label<'a>(line: &'a str, marker_kind: &str) -> Option<&'a str> {
588 let prefix = "-----";
589 let suffix = "-----";
590 if !line.starts_with(prefix) || !line.ends_with(suffix) {
591 return None;
592 }
593 let inner = &line[prefix.len()..line.len() - suffix.len()];
594 let label = inner.strip_prefix(marker_kind)?.trim();
595 if label.is_empty() {
596 return None;
597 }
598 Some(label)
599}
600
601/// Encodes `input` into RFC 4648 base64 alphabet text without line breaks.
602///
603/// # Arguments
604///
605/// * `input` — Raw bytes to encode (any length).
606///
607/// # Returns
608///
609/// A `String` of only base64 alphabet characters (no newlines).
610///
611/// # Panics
612///
613/// This function does not panic.
614fn encode_base64(input: &[u8]) -> String {
615 let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
616 let mut idx = 0_usize;
617 while idx + 3 <= input.len() {
618 let n = (u32::from(input[idx]) << 16)
619 | (u32::from(input[idx + 1]) << 8)
620 | u32::from(input[idx + 2]);
621 out.push(BASE64_ALPHABET[((n >> 18) & 0x3F) as usize] as char);
622 out.push(BASE64_ALPHABET[((n >> 12) & 0x3F) as usize] as char);
623 out.push(BASE64_ALPHABET[((n >> 6) & 0x3F) as usize] as char);
624 out.push(BASE64_ALPHABET[(n & 0x3F) as usize] as char);
625 idx += 3;
626 }
627
628 let rem = input.len() - idx;
629 if rem == 1 {
630 let n = u32::from(input[idx]) << 16;
631 out.push(BASE64_ALPHABET[((n >> 18) & 0x3F) as usize] as char);
632 out.push(BASE64_ALPHABET[((n >> 12) & 0x3F) as usize] as char);
633 out.push('=');
634 out.push('=');
635 } else if rem == 2 {
636 let n = (u32::from(input[idx]) << 16) | (u32::from(input[idx + 1]) << 8);
637 out.push(BASE64_ALPHABET[((n >> 18) & 0x3F) as usize] as char);
638 out.push(BASE64_ALPHABET[((n >> 12) & 0x3F) as usize] as char);
639 out.push(BASE64_ALPHABET[((n >> 6) & 0x3F) as usize] as char);
640 out.push('=');
641 }
642 out
643}
644
645/// Decodes strict RFC 4648 base64 payload text (with optional padding) into raw bytes.
646///
647/// # Arguments
648///
649/// * `input` — Concatenated base64 payload characters from PEM body lines.
650///
651/// # Returns
652///
653/// On success, decoded bytes whose length respects padding rules.
654///
655/// # Errors
656///
657/// Returns [`Error::InvalidEncoding`] for invalid length, padding order, or character set violations.
658///
659/// # Panics
660///
661/// This function does not panic.
662fn decode_base64(input: &str) -> Result<Vec<u8>> {
663 if !input.len().is_multiple_of(4) {
664 return Err(Error::InvalidEncoding(
665 "pem base64 length must be divisible by 4",
666 ));
667 }
668 let bytes = input.as_bytes();
669 let mut out = Vec::with_capacity((bytes.len() / 4) * 3);
670
671 for (chunk_idx, chunk) in bytes.chunks_exact(4).enumerate() {
672 let is_last = chunk_idx + 1 == bytes.len() / 4;
673 let mut sextets = [0_u8; 4];
674 let mut pad_count = 0_u8;
675 for (i, byte) in chunk.iter().enumerate() {
676 if *byte == b'=' {
677 sextets[i] = 0;
678 pad_count = pad_count.saturating_add(1);
679 continue;
680 }
681 if pad_count != 0 {
682 return Err(Error::InvalidEncoding("invalid base64 padding order"));
683 }
684 sextets[i] = decode_base64_sextet(*byte)?;
685 }
686 if pad_count > 2 {
687 return Err(Error::InvalidEncoding("invalid base64 padding width"));
688 }
689 if !is_last && pad_count != 0 {
690 return Err(Error::InvalidEncoding(
691 "base64 padding only allowed in final quartet",
692 ));
693 }
694
695 let n = (u32::from(sextets[0]) << 18)
696 | (u32::from(sextets[1]) << 12)
697 | (u32::from(sextets[2]) << 6)
698 | u32::from(sextets[3]);
699 out.push(((n >> 16) & 0xFF) as u8);
700 if pad_count < 2 {
701 out.push(((n >> 8) & 0xFF) as u8);
702 }
703 if pad_count == 0 {
704 out.push((n & 0xFF) as u8);
705 }
706 }
707 Ok(out)
708}
709
710/// Maps one ASCII base64 character to its 6-bit sextet value.
711///
712/// # Arguments
713///
714/// * `byte` — Single ASCII code unit from a base64 quartet.
715///
716/// # Returns
717///
718/// On success, the decoded 6-bit value in `0..=63`.
719///
720/// # Errors
721///
722/// Returns [`Error::InvalidEncoding`] when `byte` is not in the base64 alphabet.
723///
724/// # Panics
725///
726/// This function does not panic.
727fn decode_base64_sextet(byte: u8) -> Result<u8> {
728 match byte {
729 b'A'..=b'Z' => Ok(byte - b'A'),
730 b'a'..=b'z' => Ok(26 + (byte - b'a')),
731 b'0'..=b'9' => Ok(52 + (byte - b'0')),
732 b'+' => Ok(62),
733 b'/' => Ok(63),
734 _ => Err(Error::InvalidEncoding(
735 "invalid base64 character in pem payload",
736 )),
737 }
738}