Skip to main content

asx_rs/
credentials.rs

1//! Unified cross-protocol credential container for AS2/AS4 send paths.
2//!
3//! `PartnerCredentials` provides one zeroizing PEM bundle that can be projected
4//! into protocol-specific credential structs (`As2SendCredentials` /
5//! `As4SendCredentials`) or prepared directly against protocol policies.
6
7use crate::core::{ErrorCode, Result};
8use std::sync::Arc;
9use zeroize::Zeroize;
10
11#[cfg(feature = "as2")]
12use crate::as2::{As2PreparedSendCredentials, As2SendCredentials, As2SendPolicy};
13#[cfg(feature = "as4")]
14use crate::as4::{As4PreparedSendCredentials, As4SendCredentials, As4SendPolicy};
15
16/// Unified partner credential bundle for AS2/AS4 outbound messaging.
17///
18/// This type centralizes signing and encryption PEM material so one caller
19/// object can drive either protocol's send path.
20#[derive(Clone, Default)]
21pub struct PartnerCredentials {
22    /// PEM-encoded signing certificate.
23    pub signing_cert_pem: Option<Arc<[u8]>>,
24    /// PEM-encoded signing private key.
25    pub signing_key_pem: Option<Vec<u8>>,
26    /// PEM-encoded recipient certificate used for encryption.
27    pub recipient_cert_pem: Option<Arc<[u8]>>,
28}
29
30impl std::fmt::Debug for PartnerCredentials {
31    /// Never prints `signing_key_pem`.
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.debug_struct("PartnerCredentials")
34            .field("signing_cert_pem", &self.signing_cert_pem)
35            .field(
36                "signing_key_pem",
37                &crate::core::redact_present(self.signing_key_pem.is_some()),
38            )
39            .field("recipient_cert_pem", &self.recipient_cert_pem)
40            .finish()
41    }
42}
43
44impl Drop for PartnerCredentials {
45    fn drop(&mut self) {
46        if let Some(key) = self.signing_key_pem.as_mut() {
47            key.zeroize();
48        }
49    }
50}
51
52impl PartnerCredentials {
53    /// Project unified credentials into AS2 send credentials.
54    #[cfg(feature = "as2")]
55    pub fn to_as2_send_credentials(&self) -> As2SendCredentials {
56        As2SendCredentials {
57            signing_cert_pem: self.signing_cert_pem.clone(),
58            signing_key_pem: self.signing_key_pem.clone(),
59            recipient_cert_pem: self.recipient_cert_pem.clone(),
60        }
61    }
62
63    /// Project unified credentials into AS4 send credentials.
64    #[cfg(feature = "as4")]
65    pub fn to_as4_send_credentials(&self) -> As4SendCredentials {
66        As4SendCredentials {
67            signing_cert_pem: self.signing_cert_pem.clone(),
68            signing_key_pem: self.signing_key_pem.clone(),
69            recipient_cert_pem: self.recipient_cert_pem.clone(),
70        }
71    }
72
73    /// Prepare AS2 credentials once for repeated send operations.
74    #[cfg(feature = "as2")]
75    pub fn prepare_as2_for_policy(
76        &self,
77        policy: &As2SendPolicy,
78        stage: &'static str,
79        error_code: ErrorCode,
80    ) -> Result<As2PreparedSendCredentials> {
81        self.to_as2_send_credentials()
82            .prepare_for_policy(policy, stage, error_code)
83    }
84
85    /// Prepare AS4 credentials once for repeated send operations.
86    #[cfg(feature = "as4")]
87    pub fn prepare_as4_for_policy(
88        &self,
89        policy: &As4SendPolicy,
90        stage: &'static str,
91        error_code: ErrorCode,
92    ) -> Result<As4PreparedSendCredentials> {
93        self.to_as4_send_credentials()
94            .prepare_for_policy(policy, stage, error_code)
95    }
96
97    /// Import a `PartnerCredentials` bundle from a PKCS#12 (`.p12` / `.pfx`) file.
98    ///
99    /// Most enterprise PKI systems and many trading-partner onboarding portals
100    /// distribute key material as PKCS#12 bundles.  This constructor parses the
101    /// DER-encoded bundle, extracts the signing certificate and private key as
102    /// PEM, and stores them in the returned credential object.
103    ///
104    /// If the bundle contains a `safebag` of additional certificates they are
105    /// silently ignored — only the primary end-entity certificate and its
106    /// associated private key are extracted.  The `recipient_cert_pem` field is
107    /// left `None` and can be set by the caller afterwards via direct field
108    /// assignment.
109    ///
110    /// # Security
111    ///
112    /// The passphrase is accepted as a `&str` slice and is **not zeroized**
113    /// after use because OpenSSL copies it internally.  If passphrase hygiene
114    /// is critical, zero the source buffer after this call returns.
115    ///
116    /// # Errors
117    ///
118    /// Returns `ErrorCode::InvalidInput` if the DER bytes are not a valid
119    /// PKCS#12 bundle, or if the passphrase is incorrect.
120    #[cfg(any(feature = "as2", feature = "as4"))]
121    pub fn from_pkcs12(der: &[u8], passphrase: &str) -> crate::core::Result<Self> {
122        use crate::core::{AsxError, ErrorContext};
123        use openssl::pkcs12::Pkcs12;
124
125        let pkcs12 = Pkcs12::from_der(der).map_err(|e| {
126            AsxError::new(
127                ErrorCode::InvalidInput,
128                format!("PKCS#12 DER parsing failed: {e}"),
129                ErrorContext::new("credentials_from_pkcs12"),
130            )
131        })?;
132
133        let parsed = pkcs12.parse2(passphrase).map_err(|e| {
134            AsxError::new(
135                ErrorCode::InvalidInput,
136                format!("PKCS#12 passphrase or structure error: {e}"),
137                ErrorContext::new("credentials_from_pkcs12"),
138            )
139        })?;
140
141        let signing_cert_pem = parsed
142            .cert
143            .as_ref()
144            .map(|cert| {
145                cert.to_pem()
146                    .map_err(|e| {
147                        AsxError::new(
148                            ErrorCode::InvalidInput,
149                            format!("PKCS#12 certificate to PEM conversion failed: {e}"),
150                            ErrorContext::new("credentials_from_pkcs12"),
151                        )
152                    })
153                    .map(Arc::from)
154            })
155            .transpose()?;
156
157        let signing_key_pem = parsed
158            .pkey
159            .as_ref()
160            .map(|key| {
161                key.private_key_to_pem_pkcs8().map_err(|e| {
162                    AsxError::new(
163                        ErrorCode::InvalidInput,
164                        format!("PKCS#12 private key to PEM conversion failed: {e}"),
165                        ErrorContext::new("credentials_from_pkcs12"),
166                    )
167                })
168            })
169            .transpose()?;
170
171        Ok(Self {
172            signing_cert_pem,
173            signing_key_pem,
174            recipient_cert_pem: None,
175        })
176    }
177}
178
179#[cfg(feature = "as2")]
180impl From<As2SendCredentials> for PartnerCredentials {
181    fn from(mut value: As2SendCredentials) -> Self {
182        let result = Self {
183            signing_cert_pem: value.signing_cert_pem.take(),
184            signing_key_pem: value.signing_key_pem.take(),
185            recipient_cert_pem: value.recipient_cert_pem.take(),
186        };
187        // All fields have been moved out; skip As2SendCredentials::drop so the
188        // zeroize-on-drop does not run over already-None fields (no-op but wastes
189        // cycles).  The transferred bytes are exclusively owned by `result`
190        // (PartnerCredentials), which zeroizes them on its own Drop.
191        std::mem::forget(value);
192        result
193    }
194}
195
196#[cfg(feature = "as2")]
197impl From<PartnerCredentials> for As2SendCredentials {
198    fn from(value: PartnerCredentials) -> Self {
199        let mut value = value;
200        Self {
201            signing_cert_pem: value.signing_cert_pem.take(),
202            signing_key_pem: value.signing_key_pem.take(),
203            recipient_cert_pem: value.recipient_cert_pem.take(),
204        }
205    }
206}
207
208#[cfg(feature = "as4")]
209impl From<As4SendCredentials> for PartnerCredentials {
210    fn from(mut value: As4SendCredentials) -> Self {
211        let result = Self {
212            signing_cert_pem: value.signing_cert_pem.take(),
213            signing_key_pem: value.signing_key_pem.take(),
214            recipient_cert_pem: value.recipient_cert_pem.take(),
215        };
216        // Same rationale as From<As2SendCredentials>: skip the no-op zeroize
217        // on already-None fields; PartnerCredentials is the canonical zeroizer.
218        std::mem::forget(value);
219        result
220    }
221}
222
223#[cfg(feature = "as4")]
224impl From<PartnerCredentials> for As4SendCredentials {
225    fn from(value: PartnerCredentials) -> Self {
226        let mut value = value;
227        Self {
228            signing_cert_pem: value.signing_cert_pem.take(),
229            signing_key_pem: value.signing_key_pem.take(),
230            recipient_cert_pem: value.recipient_cert_pem.take(),
231        }
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::PartnerCredentials;
238    use std::sync::Arc;
239
240    #[cfg(feature = "as2")]
241    #[test]
242    fn partner_credentials_roundtrip_as2_projection() {
243        let src = PartnerCredentials {
244            signing_cert_pem: Some(Arc::from(b"cert" as &[u8])),
245            signing_key_pem: Some(b"key".to_vec()),
246            recipient_cert_pem: Some(Arc::from(b"recipient" as &[u8])),
247        };
248
249        let as2 = src.to_as2_send_credentials();
250        assert_eq!(as2.signing_cert_pem.as_deref(), Some(b"cert".as_slice()));
251        assert_eq!(as2.signing_key_pem.as_deref(), Some(b"key".as_slice()));
252        assert_eq!(
253            as2.recipient_cert_pem.as_deref(),
254            Some(b"recipient".as_slice())
255        );
256    }
257
258    #[cfg(feature = "as4")]
259    #[test]
260    fn partner_credentials_roundtrip_as4_projection() {
261        let src = PartnerCredentials {
262            signing_cert_pem: Some(Arc::from(b"cert" as &[u8])),
263            signing_key_pem: Some(b"key".to_vec()),
264            recipient_cert_pem: Some(Arc::from(b"recipient" as &[u8])),
265        };
266
267        let as4 = src.to_as4_send_credentials();
268        assert_eq!(as4.signing_cert_pem.as_deref(), Some(b"cert".as_slice()));
269        assert_eq!(as4.signing_key_pem.as_deref(), Some(b"key".as_slice()));
270        assert_eq!(
271            as4.recipient_cert_pem.as_deref(),
272            Some(b"recipient".as_slice())
273        );
274    }
275}