From 827aa0e74e8623bcc3e4e0a1703c86672734498f Mon Sep 17 00:00:00 2001 From: Adam Wick Date: Mon, 6 Nov 2017 14:47:27 -0800 Subject: [PATCH] Initial parser. --- .gitignore | 3 + Cargo.toml | 10 + src/lib.rs | 753 ++++++++++++++++++++++++++++++++++++++++++++++++ test/key.bin | Bin 0 -> 1216 bytes test/server.bin | Bin 0 -> 890 bytes 5 files changed, 766 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/lib.rs create mode 100644 test/key.bin create mode 100644 test/server.bin diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6aa1064 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target/ +**/*.rs.bk +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..205e2e6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "simple_asn1" +version = "0.1.0" +authors = ["Adam Wick "] + +[dependencies] +num = "^0.1.40" + +[dev-dependencies] +quickcheck = "^0.4.1" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..76682f0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,753 @@ +extern crate num; +#[cfg(test)] +#[macro_use] +extern crate quickcheck; + +use num::{BigInt,BigUint,FromPrimitive,One,ToPrimitive,Zero}; +use std::iter::FromIterator; +use std::mem::size_of; + +#[derive(Clone,Debug,PartialEq)] +pub enum ASN1Block { + Boolean(ASN1Class, bool), + Integer(ASN1Class, BigInt), + BitString(ASN1Class, usize, Vec), + OctetString(ASN1Class, Vec), + Null(ASN1Class), + ObjectIdentifier(ASN1Class, Vec), + IA5String(ASN1Class, String), + UTF8String(ASN1Class, String), + Sequence(ASN1Class, Vec), + Set(ASN1Class, Vec), + Unknown(ASN1Class, BigUint, Vec) +} + +#[derive(Clone,Copy,Debug,PartialEq)] +pub enum ASN1Class { Universal, Application, ContextSpecific, Private } + +#[derive(Clone,Debug,PartialEq)] +pub enum ASN1DecodeErr { + EmptyBuffer, + BadBooleanLength, + LengthTooLarge, + UTF8DecodeFailure +} + +#[derive(Clone,Debug,PartialEq)] +pub enum ASN1EncodeErr { + ObjectIdentHasTooFewFields, + ObjectIdentVal1TooLarge, + ObjectIdentVal2TooLarge +} + +pub fn from_der(i: &[u8]) -> Result,ASN1DecodeErr> { + let mut result: Vec = Vec::new(); + let mut index: usize = 0; + let len = i.len(); + + while index < len { + let (tag, class) = decode_tag(i, &mut index); + let len = decode_length(i, &mut index)?; + let body = &i[index .. (index + len)]; + + match tag.to_u8() { + // BOOLEAN + Some(0x01) => { + if len != 1 { + return Err(ASN1DecodeErr::BadBooleanLength); + } + result.push(ASN1Block::Boolean(class, body[0] != 0)); + } + // INTEGER + Some(0x02) => { + let res = BigInt::from_signed_bytes_be(&body); + result.push(ASN1Block::Integer(class, res)); + } + // BIT STRING + Some(0x03) if body.len() == 0 => { + result.push(ASN1Block::BitString(class, 0, Vec::new())) + } + Some(0x03) => { + let bits = (&body[1..]).to_vec(); + let nbits = (bits.len() * 8) - (body[0] as usize); + result.push(ASN1Block::BitString(class, nbits, bits)); + } + // OCTET STRING + Some(0x04) => { + result.push(ASN1Block::OctetString(class, body.to_vec())); + } + // NULL + Some(0x05) => { + result.push(ASN1Block::Null(class)); + } + // OBJECT IDENTIFIER + Some(0x06) => { + let mut value1 = BigUint::zero(); + let mut value2 = BigUint::from_u8(body[0]).unwrap(); + let mut oidres = Vec::new(); + let mut bindex = 1; + + if body[0] >= 40 { + if body[0] < 80 { + value1 = BigUint::one(); + value2 = value2 - BigUint::from_u8(40).unwrap(); + } else { + value1 = BigUint::from_u8(2).unwrap(); + value2 = value2 - BigUint::from_u8(80).unwrap(); + } + } + + oidres.push(value1); + oidres.push(value2); + while bindex < body.len() { + oidres.push(decode_base127(body, &mut bindex)); + } + + result.push(ASN1Block::ObjectIdentifier(class, oidres)); + } + // UTF8STRING + Some(0x0C) => { + match String::from_utf8(body.to_vec()) { + Ok(v) => + result.push(ASN1Block::UTF8String(class, v)), + Err(_) => + return Err(ASN1DecodeErr::UTF8DecodeFailure) + } + } + // SEQUENCE + Some(0x10) => { + match from_der(body) { + Ok(items) => + result.push(ASN1Block::Sequence(class, items)), + Err(e) => + return Err(e) + } + } + // SET + Some(0x11) => { + match from_der(body) { + Ok(items) => + result.push(ASN1Block::Set(class, items)), + Err(e) => + return Err(e) + } + } + // IA5 (ASCII) STRING + Some(0x16) => { + let val = body.iter().map(|x| *x as char); + result.push(ASN1Block::IA5String(class, String::from_iter(val))) + } + // Dunno. + _ => { + result.push(ASN1Block::Unknown(class, tag, body.to_vec())); + } + } + index += len; + } + + if result.is_empty() { + Err(ASN1DecodeErr::EmptyBuffer) + } else { + Ok(result) + } +} + +fn decode_tag(i: &[u8], index: &mut usize) -> (BigUint, ASN1Class) { + let tagbyte = i[*index]; + let class = decode_class(tagbyte); + let basetag = tagbyte & 0b11111; + + *index += 1; + if basetag == 0b11111 { + let res = decode_base127(i, index); + (res, class) + } else { + (BigUint::from(basetag), class) + } +} + +fn decode_base127(i: &[u8], index: &mut usize) -> BigUint { + let mut res = BigUint::zero(); + + loop { + let nextbyte = i[*index]; + + *index += 1; + res = (res << 7) + BigUint::from(nextbyte & 0x7f); + if (nextbyte & 0x80) == 0 { + return res; + } + } +} + +fn decode_class(i: u8) -> ASN1Class { + match i >> 6 { + 0b00 => ASN1Class::Universal, + 0b01 => ASN1Class::Application, + 0b10 => ASN1Class::ContextSpecific, + 0b11 => ASN1Class::Private, + _ => panic!("The universe is broken.") + } +} + +fn decode_length(i: &[u8], index: &mut usize) -> Result { + let startbyte = i[*index]; + + // NOTE: Technically, this size can be much larger than a usize. + // However, our whole universe starts to break down if we get + // things that big. So we're boring, and only accept lengths + // that fit within a usize. + *index += 1; + if startbyte >= 0x80 { + let mut lenlen = (startbyte & 0x7f) as usize; + let mut res = 0; + + if lenlen > size_of::() { + return Err(ASN1DecodeErr::LengthTooLarge); + } + + while lenlen > 0 { + res = (res << 8) + (i[*index] as usize); + + *index += 1; + lenlen -= 1; + } + + Ok(res) + } else { + Ok(startbyte as usize) + } +} + +pub fn to_der(i: &ASN1Block) -> Result,ASN1EncodeErr> { + match i { + // BOOLEAN + &ASN1Block::Boolean(cl, val) => { + let inttag = BigUint::one(); + let mut tagbytes = encode_tag(cl, &inttag); + tagbytes.push(1); + tagbytes.push(if val { 0xFF } else { 0x00 }); + Ok(tagbytes) + } + // INTEGER + &ASN1Block::Integer(cl, ref int) => { + let mut base = int.to_signed_bytes_be(); + let mut lenbytes = encode_len(base.len()); + let inttag = BigUint::from_u8(0x02).unwrap(); + let mut tagbytes = encode_tag(cl, &inttag); + + let mut result = Vec::new(); + result.append(&mut tagbytes); + result.append(&mut lenbytes); + result.append(&mut base); + Ok(result) + } + // BIT STRING + &ASN1Block::BitString(cl, bits, ref vs) => { + let inttag = BigUint::from_u8(0x03).unwrap(); + let mut tagbytes = encode_tag(cl, &inttag); + + if bits == 0 { + tagbytes.push(0); + Ok(tagbytes) + } else { + let mut lenbytes = encode_len(vs.len() + 1); + let nbits = (vs.len() * 8) - bits; + + let mut result = Vec::new(); + result.append(&mut tagbytes); + result.append(&mut lenbytes); + result.push(nbits as u8); + result.extend(vs.iter()); + Ok(result) + } + } + // OCTET STRING + &ASN1Block::OctetString(cl, ref bytes) => { + let inttag = BigUint::from_u8(0x04).unwrap(); + let mut tagbytes = encode_tag(cl, &inttag); + let mut lenbytes = encode_len(bytes.len()); + + let mut result = Vec::new(); + result.append(&mut tagbytes); + result.append(&mut lenbytes); + result.extend(bytes.iter()); + Ok(result) + } + // NULL + &ASN1Block::Null(cl) => { + let inttag = BigUint::from_u8(0x05).unwrap(); + let mut result = encode_tag(cl, &inttag); + result.push(0); + Ok(result) + } + // OBJECT IDENTIFIER + &ASN1Block::ObjectIdentifier(cl, ref nums) => { + match (nums.get(0), nums.get(1)) { + (Some(v1), Some(v2)) => { + let two = BigUint::from_u8(2).unwrap(); + + // first, validate that the first two items meet spec + if v1 > &two { + return Err(ASN1EncodeErr::ObjectIdentVal1TooLarge) + } + + let u175 = BigUint::from_u8(175).unwrap(); + let u39 = BigUint::from_u8(39).unwrap(); + let bound = if v1 == &two { u175 } else { u39 }; + + if v2 > &bound { + return Err(ASN1EncodeErr::ObjectIdentVal2TooLarge); + } + + // the following unwraps must be safe, based on the + // validation above. + let value1 = v1.to_u8().unwrap(); + let value2 = v2.to_u8().unwrap(); + let byte1 = (value1 * 40) + value2; + + // now we can build all the rest of the body + let mut body = vec![byte1]; + for num in nums.iter().skip(2) { + let mut local = encode_base127(&num); + body.append(&mut local); + } + + // now that we have the body, we can build the header + let inttag = BigUint::from_u8(0x06).unwrap(); + let mut result = encode_tag(cl, &inttag); + let mut lenbytes = encode_len(body.len()); + + result.append(&mut lenbytes); + result.append(&mut body); + + Ok(result) + } + _ => { + Err(ASN1EncodeErr::ObjectIdentHasTooFewFields) + } + } + } + // SEQUENCE + &ASN1Block::Sequence(cl, ref items) => { + let mut body = Vec::new(); + + // put all the subsequences into a block + for x in items.iter() { + let mut bytes = to_der(x)?; + body.append(&mut bytes); + } + + let inttag = BigUint::from_u8(0x10).unwrap(); + let mut lenbytes = encode_len(body.len()); + let mut tagbytes = encode_tag(cl, &inttag); + + let mut res = Vec::new(); + res.append(&mut tagbytes); + res.append(&mut lenbytes); + res.append(&mut body); + Ok(res) + } + // SET + &ASN1Block::Set(cl, ref items) => { + let mut body = Vec::new(); + + // put all the subsequences into a block + for x in items.iter() { + let mut bytes = to_der(x)?; + body.append(&mut bytes); + } + + let inttag = BigUint::from_u8(0x11).unwrap(); + let mut lenbytes = encode_len(body.len()); + let mut tagbytes = encode_tag(cl, &inttag); + + let mut res = Vec::new(); + res.append(&mut tagbytes); + res.append(&mut lenbytes); + res.append(&mut body); + Ok(res) + } + // IA5String + &ASN1Block::IA5String(cl, ref str) => { + let mut body = Vec::new(); + + for c in str.chars() { + body.push(c as u8); + } + + let inttag = BigUint::from_u8(0x16).unwrap(); + let mut lenbytes = encode_len(body.len()); + let mut tagbytes = encode_tag(cl, &inttag); + + let mut res = Vec::new(); + res.append(&mut tagbytes); + res.append(&mut lenbytes); + res.append(&mut body); + Ok(res) + } + // UTF8STRING + &ASN1Block::UTF8String(cl, ref val) => { + let mut body = val.clone().into_bytes(); + let inttag = BigUint::from_u8(0x0C).unwrap(); + let mut lenbytes = encode_len(body.len()); + let mut tagbytes = encode_tag(cl, &inttag); + + let mut res = Vec::new(); + res.append(&mut tagbytes); + res.append(&mut lenbytes); + res.append(&mut body); + Ok(res) + } + // Unknown blocks + &ASN1Block::Unknown(class, ref tag, ref bytes) => { + let mut tagbytes = encode_tag(class, &tag); + let mut lenbytes = encode_len(bytes.len()); + + let mut res = Vec::new(); + res.append(&mut tagbytes); + res.append(&mut lenbytes); + res.extend(bytes.iter()); + Ok(res) + } + } +} + +fn encode_tag(c: ASN1Class, t: &BigUint) -> Vec { + let cbyte = encode_class(c); + + match t.to_u8() { + Some(x) if x < 31 => { + vec![cbyte | x] + } + _ => { + let mut res = encode_base127(t); + res.insert(0, cbyte | 0b00011111); + res + } + } +} + +fn encode_base127(v: &BigUint) -> Vec { + let mut acc = v.clone(); + let mut res = Vec::new(); + let u128 = BigUint::from_u8(128).unwrap(); + let zero = BigUint::zero(); + + while acc > zero { + // we build this vector backwards + let digit = &acc % &u128; + acc = acc >> 7; + + match digit.to_u8() { + None => + panic!("7 bits don't fit into 8, cause ..."), + Some(x) if res.is_empty() => + res.push(x), + Some(x) => + res.push(x | 0x80) + } + } + + res.reverse(); + res +} + +fn encode_class(c: ASN1Class) -> u8 { + match c { + ASN1Class::Universal => 0b00000000, + ASN1Class::Application => 0b01000000, + ASN1Class::ContextSpecific => 0b10000000, + ASN1Class::Private => 0b11000000, + } +} + + +fn encode_len(x: usize) -> Vec { + if x < 128 { + vec![x as u8] + } else { + let mut bstr = Vec::new(); + let mut work = x; + + // convert this into bytes, backwards + while work > 0 { + bstr.push(work as u8); + work = work >> 8; + } + + // encode the front of the length + let len = bstr.len() as u8; + bstr.push(len | 0x80); + + // and then reverse it into the right order + bstr.reverse(); + bstr + } +} + +#[cfg(test)] +mod tests { + use quickcheck::{Arbitrary,Gen}; + use std::fs::File; + use std::io::Read; + use super::*; + + impl Arbitrary for ASN1Class { + fn arbitrary(g: &mut G) -> ASN1Class { + match g.gen::() % 4 { + 0 => ASN1Class::Private, + 1 => ASN1Class::ContextSpecific, + 2 => ASN1Class::Universal, + 3 => ASN1Class::Application, + _ => panic!("I weep for a broken life.") + } + } + } + + quickcheck! { + fn class_encdec_roundtrips(c: ASN1Class) -> bool { + c == decode_class(encode_class(c.clone())) + } + + fn class_decenc_roundtrips(v: u8) -> bool { + (v & 0b11000000) == encode_class(decode_class(v)) + } + } + + #[derive(Clone,Debug)] + struct RandomUint { + x: BigUint + } + + impl Arbitrary for RandomUint { + fn arbitrary(g: &mut G) -> RandomUint { + let v = BigUint::from_u32(g.gen::()).unwrap(); + RandomUint{ x: v } + } + } + + quickcheck! { + fn tags_encdec_roundtrips(c: ASN1Class, t: RandomUint) -> bool { + let bytes = encode_tag(c, &t.x); + let mut zero = 0; + let (t2, c2) = decode_tag(&bytes[..], &mut zero); + (c == c2) && (t.x == t2) + } + + fn len_encdec_roundtrips(l: usize) -> bool { + let bytes = encode_len(l); + let mut zero = 0; + match decode_length(&bytes[..], &mut zero) { + Err(_) => false, + Ok(l2) => l == l2 + } + } + } + + #[derive(Clone,Debug)] + struct RandomInt { + x: BigInt + } + + impl Arbitrary for RandomInt { + fn arbitrary(g: &mut G) -> RandomInt { + let v = BigInt::from_i64(g.gen::()).unwrap(); + RandomInt{ x: v } + } + } + + type ASN1BlockGen = fn(&mut G, usize) -> ASN1Block; + + fn arb_boolean(g: &mut G, _d: usize) -> ASN1Block { + let c = ASN1Class::arbitrary(g); + let v = g.gen::(); + ASN1Block::Boolean(c, v) + } + + fn arb_integer(g: &mut G, _d: usize) -> ASN1Block { + let c = ASN1Class::arbitrary(g); + let d = RandomInt::arbitrary(g); + ASN1Block::Integer(c, d.x) + } + + fn arb_bitstr(g: &mut G, _d: usize) -> ASN1Block { + let class = ASN1Class::arbitrary(g); + let size = g.gen::() as usize % 16; + let maxbits = (size as usize) * 8; + let modbits = g.gen::() as usize % 8; + let nbits = if modbits > maxbits + { maxbits } + else { maxbits - modbits }; + let bytes = g.gen_iter::().take(size).collect(); + ASN1Block::BitString(class, nbits, bytes) + } + + fn arb_octstr(g: &mut G, _d: usize) -> ASN1Block { + let class = ASN1Class::arbitrary(g); + let size = g.gen::() as usize % 16; + let bytes = g.gen_iter::().take(size).collect(); + ASN1Block::OctetString(class, bytes) + } + + fn arb_null(g: &mut G, _d: usize) -> ASN1Block { + let class = ASN1Class::arbitrary(g); + ASN1Block::Null(class) + } + + fn arb_objid(g: &mut G, _d: usize) -> ASN1Block { + let class = ASN1Class::arbitrary(g); + let count = g.gen_range::(0, 40); + let val1 = g.gen::() % 3; + let v2mod = if val1 == 2 { 176 } else { 40 }; + let val2 = g.gen::() % v2mod; + let v1 = BigUint::from_u8(val1).unwrap(); + let v2 = BigUint::from_u8(val2).unwrap(); + let mut nums = vec![v1, v2]; + + for _ in 0..count { + let num = RandomUint::arbitrary(g); + nums.push(num.x); + } + + ASN1Block::ObjectIdentifier(class, nums) + } + + fn arb_seq(g: &mut G, d: usize) -> ASN1Block { + let class = ASN1Class::arbitrary(g); + let count = g.gen_range::(1, 64); + let mut items = Vec::new(); + + for _ in 0..count { + items.push(limited_arbitrary(g, d - 1)); + } + + ASN1Block::Sequence(class, items) + } + + fn arb_set(g: &mut G, d: usize) -> ASN1Block { + let class = ASN1Class::arbitrary(g); + let count = g.gen_range::(1, 64); + let mut items = Vec::new(); + + for _ in 0..count { + items.push(limited_arbitrary(g, d - 1)); + } + + ASN1Block::Set(class, items) + } + + fn arb_ia5(g: &mut G, _d: usize) -> ASN1Block { + let class = ASN1Class::arbitrary(g); + let count = g.gen_range::(0, 384); + let mut items = Vec::new(); + + for _ in 0..count { + items.push(g.gen::() as char); + } + + ASN1Block::IA5String(class, String::from_iter(items.iter())) + } + + fn arb_utf8(g: &mut G, _d: usize) -> ASN1Block { + let class = ASN1Class::arbitrary(g); + let val = String::arbitrary(g); + ASN1Block::UTF8String(class, val) + } + + fn arb_unknown(g: &mut G, _d: usize) -> ASN1Block { + let class = ASN1Class::arbitrary(g); + let tag = RandomUint::arbitrary(g); + let size = g.gen_range::(0, 128); + let items = g.gen_iter::().take(size).collect(); + + ASN1Block::Unknown(class, tag.x, items) + } + + fn limited_arbitrary(g: &mut G, d: usize) -> ASN1Block { + let mut possibles: Vec> = + vec![arb_boolean, + arb_integer, + arb_bitstr, + arb_octstr, + arb_null, + arb_objid, + arb_ia5, + arb_utf8, + arb_unknown]; + + if d > 0 { + possibles.push(arb_seq); + possibles.push(arb_set); + } + + match g.choose(&possibles[..]) { + Some(f) => f(g, d), + None => panic!("Couldn't generate arbitrary value.") + } + } + + impl Arbitrary for ASN1Block { + fn arbitrary(g: &mut G) -> ASN1Block { + limited_arbitrary(g, 2) + } + } + + quickcheck! { + fn encode_decode_roundtrips(v: ASN1Block) -> bool { + match to_der(&v) { + Err(e) => { + println!("Serialization error: {:?}", e); + false + } + Ok(bytes) => + match from_der(&bytes[..]) { + Err(e) => { + println!("Parse error: {:?}", e); + false + } + Ok(ref rvec) if rvec.len() == 1 => { + let v2 = rvec.get(0).unwrap(); + if &v != v2 { + println!("Original: {:?}", v); + println!("Constructed: {:?}", v2); + } + &v == v2 + } + Ok(_) => { + println!("Too many results returned."); + false + } + } + } + } + } + + fn result_int(v: i16) -> Result,ASN1DecodeErr> { + let val = BigInt::from(v); + Ok(vec![ASN1Block::Integer(ASN1Class::Universal, val)]) + } + + #[test] + fn base_integer_tests() { + assert_eq!(from_der(&vec![0x02,0x01,0x00]), result_int(0)); + assert_eq!(from_der(&vec![0x02,0x01,0x7F]), result_int(127)); + assert_eq!(from_der(&vec![0x02,0x02,0x00,0x80]), result_int(128)); + assert_eq!(from_der(&vec![0x02,0x02,0x01,0x00]), result_int(256)); + assert_eq!(from_der(&vec![0x02,0x01,0x80]), result_int(-128)); + assert_eq!(from_der(&vec![0x02,0x02,0xFF,0x7F]), result_int(-129)); + } + + fn can_parse(f: &str) -> Result,ASN1DecodeErr> { + let mut fd = File::open(f).unwrap(); + let mut buffer = Vec::new(); + fd.read_to_end(&mut buffer); + from_der(&buffer[..]) + } + + #[test] + fn x509_tests() { + assert!(can_parse("test/server.bin").is_ok()); + assert!(can_parse("test/key.bin").is_ok()); + } +} diff --git a/test/key.bin b/test/key.bin new file mode 100644 index 0000000000000000000000000000000000000000..2da67d621218712a9c4925ba36fd7707345874d9 GIT binary patch literal 1216 zcmXqLV%fvQ$Y8+B#;Mij(e|B}k&%&=fu)IMnL!iFA|^%#rY1&4hJ!y3v@O>*h-YC? zdvWiGIji8NULVa5*S;0364#C^eDnBum~2&h)$KV=Ax%^7J8qQwRX&5`TUFqPR+nFU zk~!w79-F0nYvuoQWrklfqXiRREr~I)%<5S4;F$cOvoosLyKhBC=XuR2KC=J9oEWFK zOD@GI-Rasmuk?Q6y??Dz}~od*xJy{^8VS=TF+aQW?u zkMAF8sk8MpCtSaCHtm!|<3_<44>j-TdjGi{WljF84qZDjbIyXk;?SkJw=~k(jg&&W ziz_recs=RxnB3ma41CMP z%*epV)WpcZG;xEyg#R`LA5NF5l@coqpZt@__<7ap@PbS6PxLjo+rbVM4Nk z?2r2=wxutal$g}r`g=<9-lL)#Yd1{Vx=>)D&a#?=(IG$XEj#q>(hIKdmrfjdXg^Q6 zeKnJTb9`IzgOJ!J9~IfsDl5bFXWKp|S)TcNLGF`9(4txElaD`_^$9lLbLxn5*1~<= z^?e6t*jE>qo{;4ePTX_g&@E8|!A>c*oJn<84tY2}XU4};_ShhQ9 zD4dA$j_Rs1FBCfCv*FB^U5!kQjSSC4dmMhJ&8o?mx%bGmf;}7LjB|sw%sZCy?eU$s z9LWP}XD=t2>@OBLV)-hZ@nVB*{b51%f{?wA;wy?)z51zDdFjs*N9TIEni-4k%GdX< z@|F8hBjakfyXo~Jwy#V|OD|d7K2-VY?v$;wj(;jOcb~rZjie&~7kO{5W1A|2DxWdV zSqgH)wa0HXr3_E{m2enLx$N?q*?;qYv#C9ACYt>CdQOz*>i_L!ZdWq8)@KS_lod{X zsb~~?|4!|BzeoKyolWc}S0}YBcU74ec}Df}3%}z@OiVmY;$3fjqW^Lw{1f;2R(&tL zhyP<$Ppr^lzf6J6+mt4RUbl5uNmzb#`{ZZ)rft6Y`Rhui#s+5RJ8R40pB;K`UvlTC z(x&B?QnMGZb_SdX$k}}=@xbDgj31Iq71a~@U3OW#sbB7AuKK1(!OX}nUe!c%l1RL< zYw3#=>$R_xVtNnsi6?sVY=7zZe16o;{w#)9A`Y6BTA^+e7d?(yrEGU-K}~Yw#8UsZ z&2dk{RPGqhYyA?GFngvYQ)2^*PQS@Xy^7?%-MRnN1hSWyUzW4^VX$%Xnwq_h6I^Bb zIXIkUKg&LAiTdF3GN7sP@WhQab%wI*)ZS|putlq~ra!QLHr?QAc+lbOoFFdFPihVa zE{f_euw4%|^tOC1?}G_P?O3Wazn_?-`}4nU zx$m9Z(^Y>=taq4x`+nRc>C!pAH!9Xx2$V9-6EyDEPf!V4={muDpI-j5M44Z$_B;P= i>Q?&tBXgGPlonab3l5PHvuc0mm*qV6tKn%n{R9B9-9XU* literal 0 HcmV?d00001 diff --git a/test/server.bin b/test/server.bin new file mode 100644 index 0000000000000000000000000000000000000000..17107db7ee329c43a720477d34f24a1588ed6fda GIT binary patch literal 890 zcmXqLVlFdiVv1wpWSG0>fPBX;>+=S@Y@Awc9&O)w85y}*84PL-xeYkkm_u3EgqcEv z4fzdtK^zVqHvgj3^!z+SK?8n}06PyyKz>n4PGVk)p{Ri{NQjGvCp06tv?x_a!80#e z&rrxf03^)K!x@raq!3(EnwDlLZy*b|j+0SLAhA3%IoqK$qqwvvRWCU|*Fa93*U;S1 zz{tqJ(8$EtAWEFq&;p4&(9UaOR6-6gMpg#qCPsb+gC<5UrY1&4hJ!y3v@O>*h-YC? zdvWiGIji8NULVa5*S;0364#C^eDnBum~2&h)$KV=Ax%^7J8qQwRX&5`TUFqPR+nFU zk~!w79-F0nYvuoQWrklfqXiRREr~I)%<5S4;F$cOvoosLyKhBC=XuR2KC=J9oEWFK zOD@GI-Rasmuk?Q6y??Dz}~od*xJy{^8VS=TF+aQW?u zkMAF8sk8MpCtSaCHtm!|<3_<44>j-TdjGi{WljF84qZDjbIyXk;?SkJw=~k(jg&&W ziz_recs=RxnB3ma41CMP z%*eoq975dS5MpG|dTL&Cv?)?^UEIQ*uQE74s+YQ$`}ViR8Qlz*VU!b)JG4h~yT{Yo zmH+O&DpTv2lUAP2u780$QP1lr>zf^gx}S6>7{wMl8m6DA-t2y__0K=6hc|d0W~^R# zu<5Ol-kLq#GBc7UZ%K67b!_H|O3tsv%U#v|daeph*yr~)_-nE2dOq_iiQ1AKwo|=@ zL#Ktju3I!E=XZUlzI#@VXygIjX@YlD{>NW0K5Z>4CB6Qk+_kHXYNwX%Z+4W}U?+5I zBjcN^SKm+b_}RQ=;nusRPfx70@4LQk5$8WR#MUc literal 0 HcmV?d00001