Skip to main content

hoike_sign/
verify.rs

1//! OCSP response signature verification.
2//!
3//! Verifies the signature on a DER-encoded `OcspResponse` by parsing the
4//! `BasicOcspResponse`, extracting the signer's public key from the embedded
5//! certificate chain, and dispatching to the correct verifier based on the
6//! `signatureAlgorithm` OID.
7
8use der::{Decode, Encode};
9use x509_ocsp::{BasicOcspResponse, OcspResponse, OcspResponseStatus};
10
11use crate::error::{Result, SignError};
12use crate::ml_dsa_bridge::{ML_DSA_44_OID, ML_DSA_65_OID, ML_DSA_87_OID};
13
14const ECDSA_WITH_SHA256_OID: &str = "1.2.840.10045.4.3.2";
15
16/// Verify the signature on a DER-encoded `OcspResponse`.
17///
18/// Extracts the `BasicOcspResponse`, re-encodes the `tbsResponseData` to DER,
19/// and verifies the signature using the public key from the first embedded
20/// certificate. Returns `Ok(())` if the signature is valid.
21pub fn verify_ocsp_response_signature(response_der: &[u8]) -> Result<()> {
22    let ocsp_resp = OcspResponse::from_der(response_der)
23        .map_err(|e| SignError::Verify(format!("parse OcspResponse: {e}")))?;
24
25    if ocsp_resp.response_status != OcspResponseStatus::Successful {
26        return Ok(());
27    }
28
29    let response_bytes = ocsp_resp
30        .response_bytes
31        .ok_or_else(|| SignError::Verify("successful response has no responseBytes".into()))?;
32
33    let basic = BasicOcspResponse::from_der(response_bytes.response.as_bytes())
34        .map_err(|e| SignError::Verify(format!("parse BasicOcspResponse: {e}")))?;
35
36    let tbs_der = basic
37        .tbs_response_data
38        .to_der()
39        .map_err(|e| SignError::Verify(format!("re-encode tbsResponseData: {e}")))?;
40
41    let sig_bytes = basic.signature.raw_bytes();
42
43    let sig_alg_oid = basic.signature_algorithm.oid.to_string();
44
45    let pub_key_bytes = extract_public_key(&basic)?;
46
47    match sig_alg_oid.as_str() {
48        ECDSA_WITH_SHA256_OID => verify_ecdsa_p256(&tbs_der, sig_bytes, &pub_key_bytes),
49        ML_DSA_44_OID => verify_ml_dsa::<ml_dsa::MlDsa44>(&tbs_der, sig_bytes, &pub_key_bytes),
50        ML_DSA_65_OID => verify_ml_dsa::<ml_dsa::MlDsa65>(&tbs_der, sig_bytes, &pub_key_bytes),
51        ML_DSA_87_OID => verify_ml_dsa::<ml_dsa::MlDsa87>(&tbs_der, sig_bytes, &pub_key_bytes),
52        other => Err(SignError::Verify(format!(
53            "unsupported signature algorithm: {other}"
54        ))),
55    }
56}
57
58fn extract_public_key(basic: &BasicOcspResponse) -> Result<Vec<u8>> {
59    let cert = basic
60        .certs
61        .as_ref()
62        .and_then(|c| c.first())
63        .ok_or(SignError::NoCert)?;
64
65    let raw = cert
66        .tbs_certificate
67        .subject_public_key_info
68        .subject_public_key
69        .raw_bytes();
70
71    Ok(raw.to_vec())
72}
73
74fn verify_ecdsa_p256(tbs_der: &[u8], sig_bytes: &[u8], pub_key_bytes: &[u8]) -> Result<()> {
75    use p256::ecdsa::{DerSignature, VerifyingKey};
76    use signature::Verifier;
77
78    let vk = VerifyingKey::from_sec1_bytes(pub_key_bytes)
79        .map_err(|e| SignError::Verify(format!("invalid P-256 public key: {e}")))?;
80
81    let sig = DerSignature::try_from(sig_bytes)
82        .map_err(|e| SignError::Verify(format!("invalid ECDSA DER signature: {e}")))?;
83
84    vk.verify(tbs_der, &sig)
85        .map_err(|_| SignError::Verify("ECDSA-P256 signature verification failed".into()))
86}
87
88fn verify_ml_dsa<P>(tbs_der: &[u8], sig_bytes: &[u8], pub_key_bytes: &[u8]) -> Result<()>
89where
90    P: ml_dsa::MlDsaParams,
91{
92    use ml_dsa::Verifier;
93
94    let encoded = ml_dsa::EncodedVerifyingKey::<P>::try_from(pub_key_bytes).map_err(|_| {
95        SignError::Verify(format!(
96            "invalid ML-DSA public key (expected {} bytes)",
97            std::mem::size_of::<ml_dsa::EncodedVerifyingKey<P>>()
98        ))
99    })?;
100
101    let vk = ml_dsa::VerifyingKey::<P>::decode(&encoded);
102
103    let sig = ml_dsa::Signature::<P>::try_from(sig_bytes)
104        .map_err(|_| SignError::Verify("invalid ML-DSA signature".into()))?;
105
106    vk.verify(tbs_der, &sig)
107        .map_err(|_| SignError::Verify("ML-DSA signature verification failed".into()))
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn verify_ecdsa_bundle_entries() {
116        use crate::{
117            demo_ecdsa_p256_key,
118            generate::GenerationConfig,
119            produce_bundle,
120            source::{CaIdentity, CertificateStatus, StatusSnapshot},
121        };
122        use std::collections::BTreeMap;
123
124        let mut signer = demo_ecdsa_p256_key();
125        let seal_key = crate::SealKey::EcdsaP256(demo_ecdsa_p256_key());
126        let seal_cert = crate::generate_seal_cert_for_key(&seal_key).unwrap();
127        let responder_cert = crate::generate_seal_cert(&signer).unwrap();
128
129        let ca = CaIdentity {
130            label: "test-ca".into(),
131            issuer_name_der: b"CN=Test CA".to_vec(),
132            issuer_key_bytes: vec![0x42; 32],
133        };
134
135        let mut entries = BTreeMap::new();
136        entries.insert(vec![0x01], CertificateStatus::Good);
137        entries.insert(vec![0x02], CertificateStatus::Good);
138        let snapshot = StatusSnapshot {
139            entries,
140            this_update: crate::source::unix_now().unwrap(),
141            next_update: Some(crate::source::unix_now().unwrap() + 86400),
142            ..Default::default()
143        };
144
145        let config = GenerationConfig {
146            producer_id: "test".into(),
147            epoch: 1,
148            ..Default::default()
149        };
150
151        let bundle_bytes = produce_bundle::<_, p256::ecdsa::DerSignature>(
152            &ca,
153            &snapshot,
154            &config,
155            &mut signer,
156            |m| crate::create_cms_seal(m, &seal_key, &seal_cert),
157            Some(&responder_cert),
158        )
159        .unwrap();
160
161        let bundle = ahu::Bundle::from_bytes(&bundle_bytes).unwrap();
162        let mut count = 0;
163        for record in &bundle.index {
164            if let Some(entry_bytes) = bundle.entry_bytes(record) {
165                verify_ocsp_response_signature(entry_bytes).unwrap();
166                count += 1;
167            }
168        }
169        assert!(count >= 2);
170    }
171
172    #[test]
173    fn verify_ml_dsa_signature_direct() {
174        use ml_dsa::{Keypair, MlDsa87, Signer as Signer3, SigningKey};
175
176        let sk = SigningKey::<MlDsa87>::from_seed((&[42u8; 32]).into());
177        let vk = sk.verifying_key();
178        let msg = b"test message for ML-DSA verification";
179
180        let sig = sk.sign(msg);
181        let sig_bytes = sig.encode();
182        let vk_encoded = vk.encode();
183
184        verify_ml_dsa::<MlDsa87>(msg, sig_bytes.as_ref(), vk_encoded.as_ref()).unwrap();
185    }
186
187    #[test]
188    fn verify_ml_dsa_44_signature_direct() {
189        use ml_dsa::{Keypair, MlDsa44, Signer as Signer3, SigningKey};
190
191        let sk = SigningKey::<MlDsa44>::from_seed((&[1u8; 32]).into());
192        let vk = sk.verifying_key();
193        let msg = b"ML-DSA-44 verification test";
194
195        let sig = sk.sign(msg);
196        let sig_bytes = sig.encode();
197        let vk_encoded = vk.encode();
198
199        verify_ml_dsa::<MlDsa44>(msg, sig_bytes.as_ref(), vk_encoded.as_ref()).unwrap();
200    }
201
202    #[test]
203    fn verify_wrong_message_fails() {
204        use ml_dsa::{Keypair, MlDsa65, Signer as Signer3, SigningKey};
205
206        let sk = SigningKey::<MlDsa65>::from_seed((&[5u8; 32]).into());
207        let vk = sk.verifying_key();
208
209        let sig = sk.sign(b"correct message");
210        let sig_bytes = sig.encode();
211        let vk_encoded = vk.encode();
212
213        let result =
214            verify_ml_dsa::<MlDsa65>(b"wrong message", sig_bytes.as_ref(), vk_encoded.as_ref());
215        assert!(result.is_err());
216    }
217
218    #[test]
219    fn verify_no_embedded_cert_errors() {
220        use crate::{
221            demo_ecdsa_p256_key,
222            generate::GenerationConfig,
223            produce_bundle,
224            source::{CaIdentity, CertificateStatus, StatusSnapshot},
225        };
226        use std::collections::BTreeMap;
227
228        let mut signer = demo_ecdsa_p256_key();
229        let seal_key = crate::SealKey::EcdsaP256(demo_ecdsa_p256_key());
230        let seal_cert = crate::generate_seal_cert_for_key(&seal_key).unwrap();
231
232        let ca = CaIdentity {
233            label: "test".into(),
234            issuer_name_der: b"CN=Test".to_vec(),
235            issuer_key_bytes: vec![0x42; 32],
236        };
237
238        let mut entries = BTreeMap::new();
239        entries.insert(vec![0x01], CertificateStatus::Good);
240        let snapshot = StatusSnapshot {
241            entries,
242            this_update: crate::source::unix_now().unwrap(),
243            next_update: Some(crate::source::unix_now().unwrap() + 86400),
244            ..Default::default()
245        };
246        let config = GenerationConfig {
247            producer_id: "test".into(),
248            epoch: 1,
249            ..Default::default()
250        };
251
252        let bundle_bytes = produce_bundle::<_, p256::ecdsa::DerSignature>(
253            &ca,
254            &snapshot,
255            &config,
256            &mut signer,
257            |m| crate::create_cms_seal(m, &seal_key, &seal_cert),
258            None,
259        )
260        .unwrap();
261
262        let bundle = ahu::Bundle::from_bytes(&bundle_bytes).unwrap();
263        for record in &bundle.index {
264            if let Some(entry_bytes) = bundle.entry_bytes(record) {
265                let result = verify_ocsp_response_signature(entry_bytes);
266                assert!(result.is_err());
267                assert!(matches!(
268                    result.unwrap_err(),
269                    crate::error::SignError::NoCert
270                ));
271            }
272        }
273    }
274}