Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quic ietf 4967 v8 #7130

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@
LUA_LIB_NAME="lua-5.1"
CFLAGS="${CFLAGS} -DOS_DARWIN"
CPPFLAGS="${CPPFLAGS} -I/opt/local/include"
LDFLAGS="${LDFLAGS} -L/opt/local/lib"
LDFLAGS="${LDFLAGS} -L/opt/local/lib -framework Security"
;;
*-*-linux*)
# Always compile with -fPIC on Linux for shared library support.
Expand Down
4 changes: 3 additions & 1 deletion rust/Cargo.toml.in
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ kerberos-parser = "~0.5.0"
ntp-parser = "~0.6.0"
ipsec-parser = "~0.7.0"
snmp-parser = "~0.6.0"
tls-parser = "~0.9.4"
tls-parser = "~0.11.0"
x509-parser = "~0.6.5"
libc = "~0.2.82"
sha2 = "~0.9.2"
Expand All @@ -53,6 +53,8 @@ md-5 = "~0.9.1"
regex = "~1.4.2"
lazy_static = "~1.4.0"
base64 = "~0.13.0"
# the feature quic is needed by rustls::quic::Keys to "decrypt" the first quic packets
rustls = { version="~0.20.3", default-features = false, features=["quic"] }

suricata-derive = { path = "./derive" }

Expand Down
17 changes: 16 additions & 1 deletion rust/src/quic/detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* 02110-1301, USA.
*/

use crate::quic::quic::{QuicTransaction};
use crate::quic::quic::QuicTransaction;
use std::ptr;

#[no_mangle]
Expand Down Expand Up @@ -48,6 +48,21 @@ pub unsafe extern "C" fn rs_quic_tx_get_sni(
}
}

#[no_mangle]
pub unsafe extern "C" fn rs_quic_tx_get_ja3(
tx: &QuicTransaction, buffer: *mut *const u8, buffer_len: *mut u32,
) -> u8 {
if let Some(ja3) = &tx.ja3 {
*buffer = ja3.as_ptr();
*buffer_len = ja3.len() as u32;
1
} else {
*buffer = ptr::null();
*buffer_len = 0;
0
}
}

#[no_mangle]
pub unsafe extern "C" fn rs_quic_tx_get_version(
tx: &QuicTransaction, buffer: *mut *const u8, buffer_len: *mut u32,
Expand Down
2 changes: 2 additions & 0 deletions rust/src/quic/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub enum QuicError {
StreamTagNoMatch(u32),
InvalidPacket,
Incomplete,
NotSupported,
NomError(ErrorKind),
}

Expand All @@ -45,6 +46,7 @@ impl fmt::Display for QuicError {
}
QuicError::Incomplete => write!(f, "Incomplete data"),
QuicError::InvalidPacket => write!(f, "Invalid packet"),
QuicError::NotSupported => write!(f, "Not supported case yet"),
QuicError::NomError(e) => write!(f, "Internal parser error {:?}", e),
}
}
Expand Down
184 changes: 181 additions & 3 deletions rust/src/quic/frames.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

use super::error::QuicError;
use crate::quic::parser::quic_var_uint;
use nom::bytes::complete::take;
use nom::combinator::{all_consuming, complete};
use nom::multi::{count, many0};
Expand All @@ -24,6 +25,12 @@ use nom::sequence::pair;
use nom::IResult;
use num::FromPrimitive;
use std::fmt;
use tls_parser::TlsMessage::Handshake;
use tls_parser::TlsMessageHandshake::{ClientHello, ServerHello};
use tls_parser::{
parse_tls_extensions, parse_tls_message_handshake, TlsCipherSuiteID, TlsExtension,
TlsExtensionType,
};

/// Tuple of StreamTag and offset
type TagOffset = (StreamTag, u32);
Expand Down Expand Up @@ -115,13 +122,184 @@ impl fmt::Display for StreamTag {
}
}

#[derive(Debug, PartialEq)]
pub(crate) struct Ack {
pub largest_acknowledged: u64,
pub ack_delay: u64,
pub ack_range_count: u64,
pub first_ack_range: u64,
}

#[derive(Debug, PartialEq)]
pub(crate) struct Crypto {
pub ciphers: Vec<TlsCipherSuiteID>,
// We remap the Vec<TlsExtension> from tls_parser::parse_tls_extensions because of
// the lifetime of TlsExtension due to references to the slice used for parsing
pub extv: Vec<QuicTlsExtension>,
pub ja3: String,
}

#[derive(Debug, PartialEq)]
pub(crate) enum Frame {
Padding,
Ack(Ack),
Crypto(Crypto),
Stream(Stream),
Unknown(Vec<u8>),
}

fn parse_ack_frame(input: &[u8]) -> IResult<&[u8], Frame, QuicError> {
let (rest, largest_acknowledged) = quic_var_uint(input)?;
let (rest, ack_delay) = quic_var_uint(rest)?;
let (rest, ack_range_count) = quic_var_uint(rest)?;
let (rest, first_ack_range) = quic_var_uint(rest)?;

if ack_range_count != 0 {
//TODO RFC9000 section 19.3.1. ACK Ranges
return Err(nom::Err::Error(QuicError::NotSupported));
}

Ok((
rest,
Frame::Ack(Ack {
largest_acknowledged,
ack_delay,
ack_range_count,
first_ack_range,
}),
))
}

#[derive(Clone, Debug, PartialEq)]
pub struct QuicTlsExtension {
pub etype: TlsExtensionType,
pub values: Vec<Vec<u8>>,
}

fn quic_tls_ja3_client_extends(ja3: &mut String, exts: Vec<TlsExtension>) {
ja3.push_str(",");
let mut dash = false;
for e in &exts {
match e {
TlsExtension::EllipticCurves(x) => {
for ec in x {
if dash {
ja3.push_str("-");
} else {
dash = true;
}
ja3.push_str(&ec.0.to_string());
}
}
_ => {}
}
}
ja3.push_str(",");
dash = false;
for e in &exts {
match e {
TlsExtension::EcPointFormats(x) => {
for ec in *x {
if dash {
ja3.push_str("-");
} else {
dash = true;
}
ja3.push_str(&ec.to_string());
}
}
_ => {}
}
}
}

// get interesting stuff out of parsed tls extensions
fn quic_get_tls_extensions(
input: Option<&[u8]>, ja3: &mut String, client: bool,
) -> Vec<QuicTlsExtension> {
let mut extv = Vec::new();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How often, if ever would we endup not adding anything to this vector?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when parse_tls_extensions fails, that is on malformed messages...
What are you thinking of ?

if let Some(extr) = input {
if let Ok((_, exts)) = parse_tls_extensions(extr) {
let mut dash = false;
for e in &exts {
let etype = TlsExtensionType::from(e);
if dash {
ja3.push_str("-");
} else {
dash = true;
}
ja3.push_str(&u16::from(etype).to_string());
let mut values = Vec::new();
match e {
TlsExtension::SNI(x) => {
for sni in x {
let mut value = Vec::new();
value.extend_from_slice(sni.1);
values.push(value);
}
}
TlsExtension::ALPN(x) => {
for alpn in x {
let mut value = Vec::new();
value.extend_from_slice(alpn);
values.push(value);
}
}
_ => {}
}
extv.push(QuicTlsExtension { etype, values })
}
if client {
quic_tls_ja3_client_extends(ja3, exts);
}
}
}
return extv;
}

fn parse_crypto_frame(input: &[u8]) -> IResult<&[u8], Frame, QuicError> {
let (rest, offset) = quic_var_uint(input)?;
let (rest, length) = quic_var_uint(rest)?;
let (rest, _) = take(offset as usize)(rest)?;
let (_, data) = take(length as usize)(rest)?;

if let Ok((rest, msg)) = parse_tls_message_handshake(data) {
if let Handshake(hs) = msg {
match hs {
ClientHello(ch) => {
let mut ja3 = String::with_capacity(256);
ja3.push_str(&u16::from(ch.version).to_string());
let mut dash = false;
for c in &ch.ciphers {
if dash {
ja3.push_str("-");
} else {
dash = true;
}
ja3.push_str(&u16::from(*c).to_string());
}
ja3.push_str(",");
let ciphers = ch.ciphers;
let extv = quic_get_tls_extensions(ch.ext, &mut ja3, true);
return Ok((rest, Frame::Crypto(Crypto { ciphers, extv, ja3 })));
}
ServerHello(sh) => {
let mut ja3 = String::with_capacity(256);
ja3.push_str(&u16::from(sh.version).to_string());
ja3.push_str(",");
ja3.push_str(&u16::from(sh.cipher).to_string());
ja3.push_str(",");
let ciphers = vec![sh.cipher];
let extv = quic_get_tls_extensions(sh.ext, &mut ja3, false);
return Ok((rest, Frame::Crypto(Crypto { ciphers, extv, ja3 })));
}
_ => {}
}
}
}
return Err(nom::Err::Error(QuicError::InvalidPacket));
}

fn parse_tag(input: &[u8]) -> IResult<&[u8], StreamTag, QuicError> {
let (rest, tag) = be_u32(input)?;

Expand Down Expand Up @@ -164,8 +342,6 @@ fn parse_crypto_stream(input: &[u8]) -> IResult<&[u8], Vec<TagValue>, QuicError>
}

fn parse_stream_frame(input: &[u8], frame_ty: u8) -> IResult<&[u8], Frame, QuicError> {
let rest = input;

// 0b1_f_d_ooo_ss
let fin = frame_ty & 0x40 == 0x40;
let has_data_length = frame_ty & 0x20 == 0x20;
Expand All @@ -180,7 +356,7 @@ fn parse_stream_frame(input: &[u8], frame_ty: u8) -> IResult<&[u8], Frame, QuicE

let stream_id_hdr_length = usize::from((frame_ty & 0x03) + 1);

let (rest, stream_id) = take(stream_id_hdr_length)(rest)?;
let (rest, stream_id) = take(stream_id_hdr_length)(input)?;
let (rest, offset) = take(offset_hdr_length)(rest)?;

let (rest, data_length) = if has_data_length {
Expand Down Expand Up @@ -221,6 +397,8 @@ impl Frame {
} else {
match frame_ty {
0x00 => (rest, Frame::Padding),
0x02 => parse_ack_frame(rest)?,
0x06 => parse_crypto_frame(rest)?,
_ => ([].as_ref(), Frame::Unknown(rest.to_vec())),
}
};
Expand Down
Loading