/// The maximum number of tickets to remember for a given connection. const MAX_TICKETS: usize = 4;
#[derive(Clone, Debug, PartialEq, Eq)] pubenum HandshakeState {
New,
InProgress,
AuthenticationPending, /// When encrypted client hello is enabled, the server might engage a fallback. /// This is the status that is returned. The included value is the public /// name of the server, which should be used to validated the certificate.
EchFallbackAuthenticationPending(String),
Authenticated(PRErrorCode),
Complete(SecretAgentInfo),
Failed(Error),
}
/// # Panics /// /// If `usize` is less than 32 bits and the value is too large. #[must_use] pubfn max_early_data(&self) -> usize {
usize::try_from(self.info.maxEarlyDataSize).unwrap()
}
/// Get the ECH public name that was used. This will only be available /// (that is, not `None`) if `ech_accepted()` returns `false`. /// In this case, certificate validation needs to use this name rather /// than the original name to validate the certificate. If /// that validation passes (that is, `SecretAgent::authenticated` is called /// with `AuthenticationStatus::Ok`), then the handshake will still fail. /// After the failed handshake, the state will be `Error::EchRetry`, /// which contains a valid ECH configuration. /// /// # Errors /// /// When the public name is not valid UTF-8. (Note: names should be ASCII.) pubfn ech_public_name(&self) -> Res<Option<&str>> { ifself.info.valuesSet & ssl::ssl_preinfo_ech == 0 || self.info.echPublicName.is_null() {
Ok(None)
} else { let n = unsafe { CStr::from_ptr(self.info.echPublicName) };
Ok(Some(n.to_str()?))
}
}
/// `SecretAgent` holds the common parts of client and server. #[derive(Debug)] #[allow(clippy::module_name_repetitions)] pubstruct SecretAgent {
fd: *mut ssl::PRFileDesc,
secrets: SecretHolder,
raw: Option<bool>,
io: Pin<Box<AgentIo>>,
state: HandshakeState,
/// Records whether authentication of certificates is required.
auth_required: Pin<Box<bool>>, /// Records any fatal alert that is sent by the stack.
alert: Pin<Box<Option<Alert>>>, /// The current time.
now: TimeHolder,
extension_handlers: Vec<ExtensionTracker>,
/// The encrypted client hello (ECH) configuration that is in use. /// Empty if ECH is not enabled.
ech_config: Vec<u8>,
}
// Create a new SSL file descriptor. // // Note that we create separate bindings for PRFileDesc as both // ssl::PRFileDesc and prio::PRFileDesc. This keeps the bindings // minimal, but it means that the two forms need casts to translate // between them. ssl::PRFileDesc is left as an opaque type, as the // ssl::SSL_* APIs only need an opaque type. fn create_fd(io: &mut Pin<Box<AgentIo>>) -> Res<*mut ssl::PRFileDesc> {
assert_initialized(); let label = CString::new("sslwrapper")?; let id = unsafe { prio::PR_GetUniqueIdentity(label.as_ptr()) };
let base_fd = unsafe { prio::PR_CreateIOLayerStub(id, METHODS) }; if base_fd.is_null() { return Err(Error::CreateSslSocket);
} let fd = unsafe {
(*base_fd).secret = as_c_void(io).cast();
ssl::SSL_ImportFD(null_mut(), base_fd.cast())
}; if fd.is_null() { unsafe { prio::PR_Close(base_fd) }; return Err(Error::CreateSslSocket);
}
Ok(fd)
}
#[allow(clippy::missing_const_for_fn)] unsafeextern"C"fn auth_complete_hook(
arg: *mut c_void,
_fd: *mut ssl::PRFileDesc,
_check_sig: ssl::PRBool,
_is_server: ssl::PRBool,
) -> ssl::SECStatus { let auth_required_ptr = arg.cast::<bool>();
*auth_required_ptr = true; // NSS insists on getting SECWouldBlock here rather than accepting // the usual combination of PR_WOULD_BLOCK_ERROR and SECFailure.
ssl::_SECStatus_SECWouldBlock
}
unsafeextern"C"fn alert_sent_cb(
fd: *const ssl::PRFileDesc,
arg: *mut c_void,
alert: *const ssl::SSLAlert,
) { let alert = alert.as_ref().unwrap(); if alert.level == 2 { // Fatal alerts demand attention. let st = arg.cast::<Option<Alert>>().as_mut().unwrap(); if st.is_none() {
*st = Some(alert.description);
} else {
qwarn!([format!("{fd:p}")], "duplicate alert {}", alert.description);
}
}
}
/// Set the versions that are supported. /// /// # Errors /// /// If the range of versions isn't supported. pubfn set_version_range(&mutself, min: Version, max: Version) -> Res<()> { let range = ssl::SSLVersionRange { min, max };
secstatus_to_res(unsafe { ssl::SSL_VersionRangeSet(self.fd, &range) })
}
/// Enable a set of ciphers. Note that the order of these is not respected. /// /// # Errors /// /// If NSS can't enable or disable ciphers. pubfn set_ciphers(&mutself, ciphers: &[Cipher]) -> Res<()> { ifself.state != HandshakeState::New {
qwarn!([self], "Cannot enable ciphers in state {:?}", self.state); return Err(Error::InternalError);
}
let all_ciphers = unsafe { ssl::SSL_GetImplementedCiphers() }; let cipher_count = usize::from(unsafe { ssl::SSL_GetNumImplementedCiphers() }); for i in0..cipher_count { let p = all_ciphers.wrapping_add(i);
secstatus_to_res(unsafe {
ssl::SSL_CipherPrefSet(self.fd, i32::from(*p), ssl::PRBool::from(false))
})?;
}
for c in ciphers {
secstatus_to_res(unsafe {
ssl::SSL_CipherPrefSet(self.fd, i32::from(*c), ssl::PRBool::from(true))
})?;
}
Ok(())
}
/// Set key exchange groups. /// /// # Errors /// /// If the underlying API fails (which shouldn't happen). pubfn set_groups(&mutself, groups: &[Group]) -> Res<()> { // SSLNamedGroup is a different size to Group, so copy one by one. let group_vec: Vec<_> = groups
.iter()
.map(|&g| ssl::SSLNamedGroup::Type::from(g))
.collect();
/// Set the number of additional key shares that will be sent in the client hello /// /// # Errors /// /// If the underlying API fails (which shouldn't happen). pubfn send_additional_key_shares(&mutself, count: usize) -> Res<()> {
secstatus_to_res(unsafe {
ssl::SSL_SendAdditionalKeyShares(self.fd, c_uint::try_from(count)?)
})
}
/// Set TLS options. /// /// # Errors /// /// Returns an error if the option or option value is invalid; i.e., never. pubfn set_option(&self, opt: ssl::Opt, value: bool) -> Res<()> {
opt.set(self.fd, value)
}
/// `set_alpn` sets a list of preferred protocols, starting with the most preferred. /// Though ALPN [RFC7301] permits octet sequences, this only allows for UTF-8-encoded /// strings. /// /// This asserts if no items are provided, or if any individual item is longer than /// 255 octets in length. /// /// # Errors /// /// This should always panic rather than return an error. /// /// # Panics /// /// If any of the provided `protocols` are more than 255 bytes long. /// /// [RFC7301]: https://datatracker.ietf.org/doc/html/rfc7301 pubfn set_alpn(&mutself, protocols: &[impl AsRef<str>]) -> Res<()> { // Validate and set length. letmut encoded_len = protocols.len(); for v in protocols {
assert!(v.as_ref().len() < 256);
assert!(!v.as_ref().is_empty());
encoded_len += v.as_ref().len();
}
// NSS inherited an idiosyncratic API as a result of having implemented NPN // before ALPN. For that reason, we need to put the "best" option last. let (first, rest) = protocols
.split_first()
.expect("at least one ALPN value needed"); for v in rest {
add(v.as_ref());
}
add(first.as_ref());
assert_eq!(encoded_len, encoded.len());
// Now give the result to NSS.
secstatus_to_res(unsafe {
ssl::SSL_SetNextProtoNego( self.fd,
encoded.as_slice().as_ptr(),
c_uint::try_from(encoded.len())?,
)
})
}
/// Install an extension handler. /// /// This can be called multiple times with different values for `ext`. The handler is provided /// as `Rc<RefCell<dyn T>>` so that the caller is able to hold a reference to the handler /// and later access any state that it accumulates. /// /// # Errors /// /// When the extension handler can't be successfully installed. pubfn extension_handler(
&mutself,
ext: Extension,
handler: Rc<RefCell<dyn ExtensionHandler>>,
) -> Res<()> { let tracker = unsafe { ExtensionTracker::new(self.fd, ext, handler) }?; self.extension_handlers.push(tracker);
Ok(())
}
// This function tracks whether handshake() or handshake_raw() was used // and prevents the other from being used. fn set_raw(&mutself, r: bool) -> Res<()> { ifself.raw.is_none() { self.secrets.register(self.fd)?; self.raw = Some(r);
Ok(())
} elseifself.raw.unwrap() == r {
Ok(())
} else {
Err(Error::MixedHandshakeMethod)
}
}
/// Get information about the connection. /// This includes the version, ciphersuite, and ALPN. /// /// Calling this function returns None until the connection is complete. #[must_use] pubconstfn info(&self) -> Option<&SecretAgentInfo> { matchself.state {
HandshakeState::Complete(ref info) => Some(info),
_ => None,
}
}
/// Get any preliminary information about the status of the connection. /// /// This includes whether 0-RTT was accepted and any information related to that. /// Calling this function collects all the relevant information. /// /// # Errors /// /// When the underlying socket functions fail. pubfn preinfo(&self) -> Res<SecretAgentPreInfo> {
SecretAgentPreInfo::new(self.fd)
}
/// Get the peer's certificate chain. #[must_use] pubfn peer_certificate(&self) -> Option<CertificateInfo> {
CertificateInfo::new(self.fd)
}
/// Return any fatal alert that the TLS stack might have sent. #[must_use] pubfn alert(&self) -> Option<&Alert> {
(*self.alert).as_ref()
}
/// Call this function to mark the peer as authenticated. /// /// # Panics /// /// If the handshake doesn't need to be authenticated. pubfn authenticated(&mutself, status: AuthenticationStatus) {
assert!(self.state.authentication_needed());
*self.auth_required = false; self.state = HandshakeState::Authenticated(status.into());
}
fn capture_error<T>(&mutself, res: Res<T>) -> Res<T> { iflet Err(e) = res { let e = ech::convert_ech_error(self.fd, e);
qwarn!([self], "error: {:?}", e); self.state = HandshakeState::Failed(e.clone());
Err(e)
} else {
res
}
}
fn update_state(&mutself, res: Res<()>) -> Res<()> { self.state = if is_blocked(&res) { if *self.auth_required { self.preinfo()?.ech_public_name()?.map_or(
HandshakeState::AuthenticationPending,
|public_name| {
HandshakeState::EchFallbackAuthenticationPending(public_name.to_owned())
},
)
} else {
HandshakeState::InProgress
}
} else { self.capture_error(res)?; let info = self.capture_error(SecretAgentInfo::new(self.fd))?;
HandshakeState::Complete(info)
};
qdebug!([self], "state -> {:?}", self.state);
Ok(())
}
/// Drive the TLS handshake, taking bytes from `input` and putting /// any bytes necessary into `output`. /// This takes the current time as `now`. /// On success a tuple of a `HandshakeState` and usize indicate whether the handshake /// is complete and how many bytes were written to `output`, respectively. /// If the state is `HandshakeState::AuthenticationPending`, then ONLY call this /// function if you want to proceed, because this will mark the certificate as OK. /// /// # Errors /// /// When the handshake fails this returns an error. pubfn handshake(&mutself, now: Instant, input: &[u8]) -> Res<Vec<u8>> { self.now.set(now)?; self.set_raw(false)?;
let rv = { // Within this scope, _h maintains a mutable reference to self.io. let _h = self.io.wrap(input); matchself.state {
HandshakeState::Authenticated(ref err) => unsafe {
ssl::SSL_AuthCertificateComplete(self.fd, *err)
},
_ => unsafe { ssl::SSL_ForceHandshake(self.fd) },
}
}; // Take before updating state so that we leave the output buffer empty // even if there is an error. let output = self.io.take_output(); self.update_state(secstatus_to_res(rv))?;
Ok(output)
}
/// Setup to receive records for raw handshake functions. fn setup_raw(&mutself) -> Res<Pin<Box<RecordList>>> { self.set_raw(true)?; self.capture_error(RecordList::setup(self.fd))
}
/// Drive the TLS handshake, but get the raw content of records, not /// protected records as bytes. This function is incompatible with /// `handshake()`; use either this or `handshake()` exclusively. /// /// Ideally, this only includes records from the current epoch. /// If you send data from multiple epochs, you might end up being sad. /// /// # Errors /// /// When the handshake fails this returns an error. pubfn handshake_raw(&mutself, now: Instant, input: Option<Record>) -> Res<RecordList> { self.now.set(now)?; let records = self.setup_raw()?;
// Fire off any authentication we might need to complete. iflet HandshakeState::Authenticated(ref err) = self.state { let result =
secstatus_to_res(unsafe { ssl::SSL_AuthCertificateComplete(self.fd, *err) });
qdebug!([self], "SSL_AuthCertificateComplete: {:?}", result); // This should return SECSuccess, so don't use update_state(). self.capture_error(result)?;
}
// Feed in any records. iflet Some(rec) = input { self.capture_error(rec.write(self.fd))?;
}
// Drive the handshake once more. let rv = secstatus_to_res(unsafe { ssl::SSL_ForceHandshake(self.fd) }); self.update_state(rv)?;
Ok(*Pin::into_inner(records))
}
/// # Panics /// /// If setup fails. #[allow(clippy::branches_sharing_code)] pubfn close(&mutself) { // It should be safe to close multiple times. ifself.fd.is_null() { return;
} ifself.raw == Some(true) { // Need to hold the record list in scope until the close is done. let _records = self.setup_raw().expect("Can only close"); unsafe { prio::PR_Close(self.fd.cast()) };
} else { // Need to hold the IO wrapper in scope until the close is done. let _io = self.io.wrap(&[]); unsafe { prio::PR_Close(self.fd.cast()) };
}; let _output = self.io.take_output(); self.fd = null_mut();
}
/// State returns the status of the handshake. #[must_use] pubconstfn state(&self) -> &HandshakeState {
&self.state
}
/// Take a read secret. This will only return a non-`None` value once. #[must_use] pubfn read_secret(&mutself, epoch: Epoch) -> Option<p11::SymKey> { self.secrets.take_read(epoch)
}
/// Take a write secret. #[must_use] pubfn write_secret(&mutself, epoch: Epoch) -> Option<p11::SymKey> { self.secrets.take_write(epoch)
}
/// Get the active ECH configuration, which is empty if ECH is disabled. #[must_use] pubfn ech_config(&self) -> &[u8] {
&self.ech_config
}
}
impl Drop for SecretAgent { fn drop(&mutself) { self.close();
}
}
/// A TLS Client. #[derive(Debug)] #[allow(clippy::box_collection)] // We need the Box. pubstruct Client {
agent: SecretAgent,
/// The name of the server we're attempting a connection to.
server_name: String, /// Records the resumption tokens we've received.
resumption: Pin<Box<Vec<ResumptionToken>>>,
}
impl Client { /// Create a new client agent. /// /// # Errors /// /// Errors returned if the socket can't be created or configured. pubfn new(server_name: impl Into<String>, grease: bool) -> Res<Self> { let server_name = server_name.into(); letmut agent = SecretAgent::new()?; let url = CString::new(server_name.as_bytes())?;
secstatus_to_res(unsafe { ssl::SSL_SetURL(agent.fd, url.as_ptr()) })?;
agent.ready(false, grease)?; letmut client = Self {
agent,
server_name,
resumption: Box::pin(Vec::new()),
};
client.ready()?;
Ok(client)
}
unsafeextern"C"fn resumption_token_cb(
fd: *mut ssl::PRFileDesc,
token: *const u8,
len: c_uint,
arg: *mut c_void,
) -> ssl::SECStatus { letmut info: MaybeUninit<ssl::SSLResumptionTokenInfo> = MaybeUninit::uninit(); let info_res = &ssl::SSL_GetResumptionTokenInfo(
token,
len,
info.as_mut_ptr(),
c_uint::try_from(mem::size_of::<ssl::SSLResumptionTokenInfo>()).unwrap(),
); if info_res.is_err() { // Ignore the token. return ssl::SECSuccess;
} let expiration_time = info.assume_init().expirationTime; if ssl::SSL_DestroyResumptionTokenInfo(info.as_mut_ptr()).is_err() { // Ignore the token. return ssl::SECSuccess;
} let resumption = arg.cast::<Vec<ResumptionToken>>().as_mut().unwrap(); let len = usize::try_from(len).unwrap(); letmut v = Vec::with_capacity(len);
v.extend_from_slice(null_safe_slice(token, len));
qdebug!(
[format!("{fd:p}")], "Got resumption token {}",
hex_snip_middle(&v)
);
/// Take a resumption token. #[must_use] pubfn resumption_token(&mutself) -> Option<ResumptionToken> {
(*self.resumption).pop()
}
/// Check if there are more resumption tokens. #[must_use] pubfn has_resumption_token(&self) -> bool {
!(*self.resumption).is_empty()
}
/// Enable resumption, using a token previously provided. /// /// # Errors /// /// Error returned when the resumption token is invalid or /// the socket is not able to use the value. pubfn enable_resumption(&mutself, token: impl AsRef<[u8]>) -> Res<()> { unsafe {
ssl::SSL_SetResumptionToken( self.agent.fd,
token.as_ref().as_ptr(),
c_uint::try_from(token.as_ref().len())?,
)
}
}
/// Enable encrypted client hello (ECH), using the encoded `ECHConfigList`. /// /// When ECH is enabled, a client needs to look for `Error::EchRetry` as a /// failure code. If `Error::EchRetry` is received when connecting, the /// connection attempt should be retried and the included value provided /// to this function (instead of what is received from DNS). /// /// Calling this function with an empty value for `ech_config_list` enables /// ECH greasing. When that is done, there is no need to look for `EchRetry` /// /// # Errors /// /// Error returned when the configuration is invalid. pubfn enable_ech(&mutself, ech_config_list: impl AsRef<[u8]>) -> Res<()> { let config = ech_config_list.as_ref();
qdebug!([self], "Enable ECH for a server: {}", hex_with_len(config)); self.ech_config = Vec::from(config); if config.is_empty() { unsafe { ech::SSL_EnableTls13GreaseEch(self.agent.fd, PRBool::from(true)) }
} else { unsafe {
ech::SSL_SetClientEchConfigs( self.agent.fd,
config.as_ptr(),
c_uint::try_from(config.len())?,
)
}
}
}
}
impl Deref for Client { type Target = SecretAgent; #[must_use] fn deref(&self) -> &SecretAgent {
&self.agent
}
}
/// `ZeroRttCheckResult` encapsulates the options for handling a `ClientHello`. #[derive(Clone, Debug, PartialEq, Eq)] pubenum ZeroRttCheckResult { /// Accept 0-RTT.
Accept, /// Reject 0-RTT, but continue the handshake normally.
Reject, /// Send `HelloRetryRequest` (probably not needed for QUIC).
HelloRetryRequest(Vec<u8>), /// Fail the handshake.
Fail,
}
/// A `ZeroRttChecker` is used by the agent to validate the application token (as provided by /// `send_ticket`) pubtrait ZeroRttChecker: std::fmt::Debug + std::marker::Unpin { fn check(&self, token: &[u8]) -> ZeroRttCheckResult;
}
/// Using `AllowZeroRtt` for the implementation of `ZeroRttChecker` means /// accepting 0-RTT always. This generally isn't a great idea, so this /// generates a strong warning when it is used. #[derive(Debug)] pubstruct AllowZeroRtt {} impl ZeroRttChecker for AllowZeroRtt { fn check(&self, _token: &[u8]) -> ZeroRttCheckResult {
qwarn!("AllowZeroRtt accepting 0-RTT");
ZeroRttCheckResult::Accept
}
}
unsafeextern"C"fn hello_retry_cb(
first_hello: PRBool,
client_token: *const u8,
client_token_len: c_uint,
retry_token: *mut u8,
retry_token_len: *mut c_uint,
retry_token_max: c_uint,
arg: *mut c_void,
) -> ssl::SSLHelloRetryRequestAction::Type { if first_hello == 0 { // On the second ClientHello after HelloRetryRequest, skip checks. return ssl::SSLHelloRetryRequestAction::ssl_hello_retry_accept;
}
let check_state = arg.cast::<ZeroRttCheckState>().as_mut().unwrap(); let token = null_safe_slice(client_token, usize::try_from(client_token_len).unwrap()); match check_state.checker.check(token) {
ZeroRttCheckResult::Accept => ssl::SSLHelloRetryRequestAction::ssl_hello_retry_accept,
ZeroRttCheckResult::Fail => ssl::SSLHelloRetryRequestAction::ssl_hello_retry_fail,
ZeroRttCheckResult::Reject => {
ssl::SSLHelloRetryRequestAction::ssl_hello_retry_reject_0rtt
}
ZeroRttCheckResult::HelloRetryRequest(tok) => { // Don't bother propagating errors from this, because it should be caught in // testing.
assert!(tok.len() <= usize::try_from(retry_token_max).unwrap()); let slc = std::slice::from_raw_parts_mut(retry_token, tok.len());
slc.copy_from_slice(&tok);
*retry_token_len = c_uint::try_from(tok.len()).unwrap();
ssl::SSLHelloRetryRequestAction::ssl_hello_retry_request
}
}
}
/// Enable 0-RTT. This shadows the function of the same name that can be accessed /// via the Deref implementation on Server. /// /// # Errors /// /// Returns an error if the underlying NSS functions fail. pubfn enable_0rtt(
&mutself,
anti_replay: &AntiReplay,
max_early_data: u32,
checker: Box<dyn ZeroRttChecker>,
) -> Res<()> { letmut check_state = Box::pin(ZeroRttCheckState::new(checker)); unsafe {
ssl::SSL_HelloRetryRequestCallback( self.agent.fd,
Some(Self::hello_retry_cb),
as_c_void(&mut check_state),
)
}?; unsafe { ssl::SSL_SetMaxEarlyDataSize(self.agent.fd, max_early_data) }?; self.zero_rtt_check = Some(check_state); self.agent.enable_0rtt()?;
anti_replay.config_socket(self.fd)?;
Ok(())
}
/// Send a session ticket to the client. /// This adds |extra| application-specific content into that ticket. /// The records that are sent are captured and returned. /// /// # Errors /// /// If NSS is unable to send a ticket, or if this agent is incorrectly configured. pubfn send_ticket(&mutself, now: Instant, extra: &[u8]) -> Res<RecordList> { self.agent.now.set(now)?; let records = self.setup_raw()?;
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.