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
//! OCSP identity hashes through the shared digest contract.
use super::CertId;
use super::ResponderId;
use crate::Certificate;
use crate::algorithm::digest::{self, Algorithm, AsnDigest};
use crate::error::{CallbackError, CallbackResult, Error, ErrorCode};
use structured_asn1::digest::{OnceError, Resolution, matches_once};
use structured_asn1::memory::{MemoryError, Work};
// A provider output of the wrong width is located at the selector it hashed for.
fn once<E>(error: OnceError<E>, field: &'static str, at: u64) -> CallbackError<E> {
match error {
OnceError::Provider(error) => CallbackError::Callback(error),
OnceError::OutputWidth { .. } => {
CallbackError::Processing(Error::new(ErrorCode::DigestOutput, field, at))
}
}
}
/// Identity comparison is not responder authentication or a certificate verdict.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CertMatch {
/// Serial, issuer name and both original-byte hashes match.
Matches,
/// A completed semantic or digest comparison found different identity.
Differs,
/// The advertised fixed hash width contradicts a carried hash value.
MalformedHashWidth,
/// The encoded hash identifier writes parameters its algorithm does not take.
MalformedHashAlgorithm,
/// The consumer does not support the encoded hash algorithm.
UnsupportedHash,
}
/// Responder identity alone, without signature, authority or status acceptance.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResponderMatch {
/// Subject Name or imposed SHA-1 public-key hash identifies this certificate.
Matches,
/// Completed name or complete digest comparison differs.
Differs,
/// Written hash or provider-advertised SHA-1 width contradicts twenty octets.
MalformedHashWidth,
/// Protocol-imposed SHA-1 is unsupported by the selected provider/policy.
UnsupportedHash,
}
impl ResponderId<'_> {
/// Compare an original responder selector with the actual signer certificate.
/// RFC6960 §4.2.1 hashes subjectPublicKey contents with SHA-1, excluding
/// BIT STRING tag, length and unused-bits count. Name comparison reuses the
/// shared PKIX Name kernel. No signature recheck or standing-key fabrication.
/// <https://www.rfc-editor.org/rfc/rfc6960#section-4.2.1>
///
/// `field` and `at` identify the real enclosing selector in the caller's
/// source; callbacks preserve their own original processing/resource cause.
pub fn matches_certificate<'d, D: AsnDigest<'d> + ?Sized, E: Work>(
&self,
certificate: &Certificate<'_>,
digest: &'d D,
execution: &E,
field: &'static str,
at: u64,
) -> CallbackResult<ResponderMatch, D::Error> {
execution
.charge_work(1)
.map_err(|reason| Error::resource(reason, field, at))?;
let same = match self {
Self::Name(name) => name.matches(certificate.tbs().subject(), execution)?,
Self::KeyHash(expected) => {
if expected.len() != Algorithm::Sha1.width() {
return Ok(ResponderMatch::MalformedHashWidth);
}
let Resolution::Supported(identity) = digest::supported(digest, Algorithm::Sha1)
else {
return Ok(ResponderMatch::UnsupportedHash);
};
if digest
.width(&identity)
.is_some_and(|width| width != Algorithm::Sha1.width())
{
return Ok(ResponderMatch::MalformedHashWidth);
}
let input = certificate.tbs().public_key().key().as_bytes();
match matches_once(digest, &identity, input, expected, execution)
.map_err(|error| once(error, field, at))?
{
Resolution::Supported(same) => same,
Resolution::Unsupported | Resolution::Malformed => {
return Ok(ResponderMatch::UnsupportedHash);
}
}
}
};
Ok(if same {
ResponderMatch::Matches
} else {
ResponderMatch::Differs
})
}
}
impl CertId<'_> {
/// Match the actual target and candidate issuer without re-encoding names.
/// RFC6960 §4.1.1 hashes original target issuer DER and issuer public-key
/// contents. The caller retains their authenticated issuer/path provenance;
/// matching hashes does not verify the target certificate's signature.
/// <https://www.rfc-editor.org/rfc/rfc6960#section-4.1.1>
pub fn matches<'d, D: AsnDigest<'d> + ?Sized, E: Work>(
&self,
target: &Certificate<'_>,
issuer: &Certificate<'_>,
digest: &'d D,
execution: &E,
) -> CallbackResult<CertMatch, D::Error> {
const FIELD: &str = "ocspResponse.certID";
let at = self.span.start;
let left = self.serial.as_bytes();
let right = target.tbs().serial().as_bytes();
let work = u64::try_from(left.len().max(right.len()))
.ok()
.and_then(|n| n.checked_mul(2))
.and_then(|n| n.checked_add(1))
.ok_or_else(|| Error::resource(MemoryError::WorkExceeded, FIELD, at))?;
execution
.charge_work(work)
.map_err(|reason| Error::resource(reason, FIELD, at))?;
if left != right {
return Ok(CertMatch::Differs);
}
if !target
.tbs()
.issuer()
.matches(issuer.tbs().subject(), execution)?
{
return Ok(CertMatch::Differs);
}
let identity = match digest::resolve(digest, &self.algorithm, execution)? {
Resolution::Supported(identity) => identity,
Resolution::Unsupported => return Ok(CertMatch::UnsupportedHash),
Resolution::Malformed => return Ok(CertMatch::MalformedHashAlgorithm),
};
// A malformed known width is classified before either hashing callback.
if let Some(width) = digest.width(&identity)
&& (self.issuer_name_hash.len() != width || self.issuer_key_hash.len() != width)
{
return Ok(CertMatch::MalformedHashWidth);
}
for (input, expected) in [
(target.tbs().issuer().as_bytes(), self.issuer_name_hash),
(
issuer.tbs().public_key().key().as_bytes(),
self.issuer_key_hash,
),
] {
match matches_once(digest, &identity, input, expected, execution)
.map_err(|error| once(error, FIELD, at))?
{
Resolution::Supported(true) => {}
Resolution::Supported(false) => return Ok(CertMatch::Differs),
Resolution::Unsupported | Resolution::Malformed => {
return Ok(CertMatch::UnsupportedHash);
}
}
}
Ok(CertMatch::Matches)
}
}
#[cfg(test)]
mod tests;