cert: add name constraints ext
This commit is contained in:
parent
9aeecc968e
commit
a57998dc2d
|
@ -105,7 +105,6 @@ pub enum ExtensionValue<'a> {
|
|||
info: Vec<PolicyInformation<'a>>
|
||||
},
|
||||
|
||||
// Permitted subtrees and excluded subtrees are not implemented
|
||||
SubjectAlternativeName {
|
||||
general_names: Vec<GeneralName<'a>>,
|
||||
},
|
||||
|
@ -115,8 +114,13 @@ pub enum ExtensionValue<'a> {
|
|||
path_len_constraint: Option<u8>,
|
||||
},
|
||||
|
||||
// Permitted subtrees and excluded subtrees are not implemented
|
||||
// NameConstraints,
|
||||
NameConstraints {
|
||||
// Owns a list of acceptable/unacceptable GeneralNames
|
||||
// Maximum field should not exist, minimum field is always 0
|
||||
// Vector size of 0 equivalent to NIL
|
||||
permitted_subtrees: Vec<GeneralName<'a>>,
|
||||
excluded_subtrees: Vec<GeneralName<'a>>,
|
||||
},
|
||||
|
||||
// Policy mapping will not be supported
|
||||
// PolicyConstraints,
|
||||
|
|
|
@ -120,7 +120,7 @@ const CA_SIGNED_CERT: [u8; 0x0356] =
|
|||
"308203523082023a02146048517ee55aabd1e8f2bd7db1d91e679708e644300d06092a864886f70d01010b05003067310b30090603550406130255533113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c74643120301e06035504030c176578616d706c65732e63612e756c666865696d2e6e6574301e170d3230313130363034323035305a170d3230313230363034323035305a3064310b30090603550406130255533113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c7464311d301b06035504030c146578616d706c65732e756c666865696d2e6e657430820122300d06092a864886f70d01010105000382010f003082010a0282010100b2940671bfe7ace7416ba9d34018c229588e9d4eed8bd6623e44ab1239e8f1f0de9050b2f485a98e63f5b483330fb0b5abaeb33d11889033b0b684bf34696d28206bb361782c4b106a8d47874cbbdf971b5ab887bca508bccf250a1a811cee078464638e441941347d4c8885ac9b59d9fc9636276912b04d9e3ab29bd8ad319572ae54f0b6145c4d675a78607dcc4793a4d432f1c2a41ea29dd4f7262b6fe472dfaea51aca992b4624e73fa9901fa364fc5b721052ef3187e659d58d2706770d365380a7ebab6caac5b23271c01531fdf95368ee48af5383035f249be7c18f50ce9e52877558efe4b2e29f61328396e2a3b5e71309ad13d93d6ba3d5c3eb2b650203010001300d06092a864886f70d01010b0500038201010063c9ab0f5d2e164513e8e74b656ae4f48dd004c3ead9f1026b7741cbf02bb0efcf19e0fbf8a788dae059a2393167f016bafc0e3efd5c5b4c43079b6506eb67f17f44f9591503c7d1fdb77bf631894817393ea82610ad5106d23ec6bf1a6d96d749f05c0136cd71256617a51fe862529aee4a37d5f456dc7da8b220ff10ede4e87bc63e4589b3f81133a7f82ab900419e8a2d802d59e99cfbbd268702efd17616168b45b5211da0e644c29dcb92dbbf32b43586bbab05deb0261771605c52836363bd28ff9853d44436349f5ba11f2640bc9c42688e0d5eb6cac9f3f5e5f98652fa4f4ba52604371ec45f09d678e31d463285a4b3734f587f35a339920544f476"
|
||||
);
|
||||
|
||||
const SELF_SIGNED_WITH_SAN: [u8; 0x03E8] =
|
||||
const SELF_SIGNED_WITH_SAN: [u8; 0x065B] =
|
||||
hex_literal::hex!(
|
||||
"308203e4308202cca00302010202145e88e088cb38c6b2b61eb94eae6d2c2429382148300d06092a864886f70d01010b05003070310b3009060355040613025553310b300906035504080c0256413111300f06035504070c08536f6d654369747931123010060355040a0c094d79436f6d70616e7931133011060355040b0c0a4d794469766973696f6e3118301606035504030c0f7777772e636f6d70616e792e636f6d301e170d3230313130363039303230345a170d3232313130363039303230345a3070310b3009060355040613025553310b300906035504080c0256413111300f06035504070c08536f6d654369747931123010060355040a0c094d79436f6d70616e7931133011060355040b0c0a4d794469766973696f6e3118301606035504030c0f7777772e636f6d70616e792e636f6d30820122300d06092a864886f70d01010105000382010f003082010a0282010100e1d7d8ddb3ec0e334e7d8e5aff8272384e755488e887b1ac9c520a95fbfb75aff84e5d5bab76fe397d0bd73e9bf5358824ecf26c2040f4f04ccff04e60e5469294fb2b18fa8533d8826d1cac977d37f0673fb102fa583e78cb965271f66160b86889acf7add28804eeaeff385084961c3e60dda38f83dd7f37eead28dfedd715fbc57512e9f7c0b297eea2ad752483bc09d9dfd7d8466a5de3f875bb5b9a86b6f0cc4853bab323c0e31369d36f4e230d498198d800f2fefdc70525e5e8785262d90e67c442dc1c92ea22239dbdb0f5915b0e9ac05fd23a755e92030af606b0bf738bd5d9e7416c278785066057a3a828030229a5e6ec8f1a561fa6d4b03473650203010001a3763074300b0603551d0f04040302043030130603551d25040c300a06082b0601050507030130500603551d1104493047820f7777772e636f6d70616e792e6e6574820b636f6d70616e792e636f6d820b636f6d70616e792e6e6574810b626f6240636f6d70616e79810d616c69636540636f6d70616e79300d06092a864886f70d01010b050003820101008ee6ca325376df41ca6858c5ab6bc2d82d20b8d20bd7a6a007a62047f67e4c5988dc60e6e313a3d4456c87ac9b7d064af61012d9e5e630839471255166fb3f2de460dbf2ff88f2b26d385e605c7f47f8bd0a34e1ab4726e82642c5eba7e2b49a05c829704618362341ce02dca477b76e6d8919a4ec568aafa581a710220c7101d686f36f381b1851c06ed9f2c3de26befb95ea653dc7429c0abe8dd66b2328d7b41222b06237840835a15073d3f314c0d84b812fe7829e42da8174dac600845bf367685c5607196c2bcc06aad54eec531d93cfb6fea7995d65272cab575faaac7c9029a312e664ef0d3880fa4e0acaeee4c144ffb994a64118713def59360f6c"
|
||||
"308206573082043fa00302010202144d8eb521b65348ff483311fbd343303b15590153300d06092a864886f70d01010b05003073310b3009060355040613025553310b300906035504080c0256413114301206035504070c0b416e6f746865724369747931123010060355040a0c094d79436f6d70616e7931133011060355040b0c0a4d794469766973696f6e3118301606035504030c0f7777772e636f6d70616e792e636f6d301e170d3230313130393034353735315a170d3231313130393034353735315a3073310b3009060355040613025553310b300906035504080c0256413114301206035504070c0b416e6f746865724369747931123010060355040a0c094d79436f6d70616e7931133011060355040b0c0a4d794469766973696f6e3118301606035504030c0f7777772e636f6d70616e792e636f6d30820222300d06092a864886f70d01010105000382020f003082020a0282020100ac61268194dfd6fbeab3e05b6ac55568dd676701ddca6c6784e18c2fae2af11625a945aa3b22674e81dd52810994b6cef1317b99e2e6a9e9e92c4e63890a59f4837264425e44876db170e1cc403da9db7eef99ce406bd41099ff9ebd73c37cda1dbe0737f78b98fa6a5f3054c37a01dcf99cf6247675b40e85f990928ef064fc4f7b9770c93ea9f038a7e9fbdb1f93ffb5a01fabafdd1ec2f4c73dc84297b804b383bb5f6e9f0d9f1f437e8a26dd181778b64db6b2caa420a17b57590e5747008311e583497ce922b9f162bca1ecba1547280e2343fd2b8dabc014bef0490f651f9a676d49656bfc747a60d423702f28c0dfa7d0ee17049b10ddaf1e8c45f29debe34d23b5d6715a67e27faa238ac9f10a00ab3f93966bdbcc917b2b905e1e01c39d34c3b20fa6b2918e0272f0e3a77af45e0adb5380a752c35701b8c8081ed57ac42dbf875bfef09f01542ef58914387492697cafba758516c10522d3d7315b993cc096c3b1e6c072123a508b8aed5455067c99b69b88be39e6b65e668a65a515f0e84ead532061f22a51dac83280bb3c5decaa634f7d334de19aef1623642ea72e8817d4e09330d2507ff5a10805329b6296cd029b792cb42ab8499bc57a28f1480d04c84a4f5b8a11b8c60c053d643ca6bae999139a8cd84ef8decb413f9d6de1733bc477ab86aead3e1a1f208b1abfb9e62c63730c1f47e5d97c1c1ce8eb0203010001a381e23081df300b0603551d0f04040302043030130603551d25040c300a06082b0601050507030130760603551d11046f306d820f7777772e636f6d70616e792e6e6574820b636f6d70616e792e636f6d820b636f6d70616e792e6e6574810b626f6240636f6d70616e79810d616c69636540636f6d70616e79a01e06032a0304a0170c15736f6d65206f74686572206964656e7469666965728704c0a801c830430603551d1e043c303aa01c300e820c2e6578616d706c652e636f6d300a8708c0a80001ffffff00a11a300a8708c0aa0001ffffff00300c820a2e6c6f63616c686f7374300d06092a864886f70d01010b050003820201003b6b5e5aeb8a598a4400fb3f2afc4f3604b3a2319d56ed7e9b4bee20cf8f141326dd0db406b0462240cdbf64730ab2d3a4bdc17dc54554dd834ef929b672da638a4f2d9e9c8221f4b7140121b124a47d773d50e86aa3d111c824a449f704c1e79312fa2b558dfb7260f7ac9e450ebfb321e9d7554a802c824937403e3266f4d3743d848196b855663020c0e1f529fc4ca822b52a70e9a5987bcd3c62c2bf3f80915cb5060c1f077580ffd8f532ad0e807662f030ba6168fc91ccc8e61f78df9a0c7b5090c6ddc01213323cc5a651dea96f67c4b466e17cd522fabc5a7f80e88d4933eef79e2b24d735eab2e5618e14960c10acdc58d928b514a6f2221a9fc71e67f52816713abbe6b8dabcdfa06c83499ece822bdead9a1695d13f2da7391463177a4162fa7cf8aeae3e9182def0789c4fa63ddc6c75cccf23c7a1438569488e131205892cabb482902dd117cfcbcb9fdbb963d11d3de702c51f6ef9772756e9d4c94c1925a0752b0fa45dcb7fc07f1cfe7685ec6c52011c1a98c246cb774dec66ebfcd4d0f52ed3a24d86df593eb7e318edc4e8946e8100825eb363b00507f585f8ee6770f66f1b638c9129cbd9a3f8f862771b4bf0d7c1da2a8cfd592d30b3345fad00a3f5c20d6bd4e365a26f3f4091f8de03aa9900a2da36dde5098aa7689827f50197d42b4aebe3745a509c698f5b8cfe9635b00491a1c9218c7cd05758"
|
||||
);
|
||||
|
|
|
@ -73,6 +73,7 @@ pub const CERT_BASIC_CONSTRAINTS: &'static [u8] = &[85, 29, 19];
|
|||
pub const CERT_EXT_KEY_USAGE: &'static [u8] = &[85, 29, 37]; // 2.5.29.37
|
||||
pub const CERT_INHIBIT_ANY_POLICY: &'static [u8] = &[85, 29, 54]; // 2.5.29.54
|
||||
pub const CERT_SUBJECTALTNAME: &'static [u8] = &[85, 29, 17]; // 2.5.29.17
|
||||
pub const CERT_NAME_CONSTRAINTS: &'static [u8] = &[85, 29, 30]; // 2.5.29.30
|
||||
// Extended Key Extensions
|
||||
pub const ANY_EXTENDED_KEY_USAGE: &'static [u8] = &[85, 29, 37, 0]; // 2.5.29.37.0
|
||||
pub const ID_KP_SERVER_AUTH: &'static [u8] = &[43, 6, 1, 5, 5, 7, 3, 1]; // 1.3.6.1.5.5.7.3.1
|
||||
|
|
269
src/parse.rs
269
src/parse.rs
|
@ -952,6 +952,7 @@ pub fn parse_asn1_der_extensions(bytes: &[u8]) -> IResult<&[u8], Asn1DerExtensio
|
|||
|
||||
// Parser for an extension (Sequence: 0x30)
|
||||
pub fn parse_asn1_der_extension(bytes: &[u8]) -> IResult<&[u8], Asn1DerExtension> {
|
||||
log::info!("Extension: {:X?}\n", bytes);
|
||||
let (rest, (tag_val, _, value)) = parse_asn1_der_object(bytes)?;
|
||||
// Verify the tag_val is indeed 0x30
|
||||
if tag_val != 0x30 {
|
||||
|
@ -1004,6 +1005,12 @@ pub fn parse_asn1_der_extension(bytes: &[u8]) -> IResult<&[u8], Asn1DerExtension
|
|||
)(rem_ext_data)?;
|
||||
extension_value
|
||||
}
|
||||
oid::CERT_NAME_CONSTRAINTS => {
|
||||
let (_, extension_value) = complete(
|
||||
parse_asn1_der_name_constraints
|
||||
)(rem_ext_data)?;
|
||||
extension_value
|
||||
}
|
||||
// TODO: Parse extension value for recognized extensions
|
||||
_ => Asn1DerExtensionValue::Unrecognized
|
||||
};
|
||||
|
@ -1051,113 +1058,10 @@ pub fn parse_asn1_der_subject_alternative_name(bytes: &[u8]) -> IResult<&[u8], A
|
|||
let mut general_names: Vec<Asn1DerGeneralName> = Vec::new();
|
||||
|
||||
while names.len() != 0 {
|
||||
let (rest, (tag_val, _, name_value)) = parse_asn1_der_object(names)?;
|
||||
match tag_val {
|
||||
0x80 => {
|
||||
let (_, seq) = complete(
|
||||
parse_asn1_der_sequence
|
||||
)(name_value)?;
|
||||
let (_, (oid, (inner_tag_val, _, value))) = complete(
|
||||
tuple((
|
||||
parse_asn1_der_oid,
|
||||
parse_asn1_der_object
|
||||
))
|
||||
)(seq)?;
|
||||
if inner_tag_val != 0x80 {
|
||||
return Err(nom::Err::Error((bytes, ErrorKind::Verify)));
|
||||
}
|
||||
general_names.push(
|
||||
Asn1DerGeneralName::OtherName { type_id: oid, value }
|
||||
);
|
||||
},
|
||||
|
||||
0x81 => {
|
||||
general_names.push(
|
||||
Asn1DerGeneralName::RFC822Name(name_value)
|
||||
);
|
||||
},
|
||||
|
||||
0x82 => {
|
||||
general_names.push(
|
||||
Asn1DerGeneralName::DNSName(name_value)
|
||||
);
|
||||
},
|
||||
|
||||
0x83 => {
|
||||
general_names.push(
|
||||
Asn1DerGeneralName::X400Address(name_value)
|
||||
);
|
||||
},
|
||||
|
||||
0x84 => {
|
||||
general_names.push(
|
||||
Asn1DerGeneralName::DirectoryName(name_value)
|
||||
);
|
||||
},
|
||||
|
||||
0x85 => {
|
||||
let (_, seq) = complete(
|
||||
parse_asn1_der_sequence
|
||||
)(name_value)?;
|
||||
let (_, (
|
||||
(name_assigner_tag_val, _, name_assigner),
|
||||
party_name
|
||||
)) = complete(
|
||||
tuple((
|
||||
parse_asn1_der_object,
|
||||
opt(parse_asn1_der_object)
|
||||
))
|
||||
)(seq)?;
|
||||
|
||||
let general_name = if party_name.is_none() && name_assigner_tag_val == 0x81 {
|
||||
Asn1DerGeneralName::EDIPartyName {
|
||||
name_assigner: &[],
|
||||
party_name: name_assigner
|
||||
}
|
||||
} else if party_name.is_some() && name_assigner_tag_val == 0x80 {
|
||||
if let Some((party_name_tag_val, _, party_name_value)) = party_name {
|
||||
if party_name_tag_val == 0x81 {
|
||||
Asn1DerGeneralName::EDIPartyName {
|
||||
name_assigner,
|
||||
party_name: party_name_value
|
||||
}
|
||||
}
|
||||
else {
|
||||
return Err(nom::Err::Error((bytes, ErrorKind::Verify)))
|
||||
}
|
||||
} else {
|
||||
return Err(nom::Err::Error((bytes, ErrorKind::Verify)))
|
||||
}
|
||||
} else {
|
||||
return Err(nom::Err::Error((bytes, ErrorKind::Verify)))
|
||||
};
|
||||
|
||||
general_names.push(
|
||||
general_name
|
||||
);
|
||||
},
|
||||
|
||||
0x86 => {
|
||||
general_names.push(
|
||||
Asn1DerGeneralName::URI(name_value)
|
||||
);
|
||||
},
|
||||
|
||||
0x87 => {
|
||||
general_names.push(
|
||||
Asn1DerGeneralName::IPAddress(name_value)
|
||||
);
|
||||
},
|
||||
|
||||
0x88 => {
|
||||
general_names.push(
|
||||
Asn1DerGeneralName::RegisteredID(name_value)
|
||||
);
|
||||
},
|
||||
|
||||
_ => return Err(nom::Err::Error((bytes, ErrorKind::Verify)))
|
||||
}
|
||||
log::info!("Name bytes: {:X?}\nLength: {:?}\n", names, names.len());
|
||||
let (rest, general_name) = parse_asn1_der_general_name(names)?;
|
||||
|
||||
general_names.push(general_name);
|
||||
names = rest;
|
||||
}
|
||||
|
||||
|
@ -1167,6 +1071,159 @@ pub fn parse_asn1_der_subject_alternative_name(bytes: &[u8]) -> IResult<&[u8], A
|
|||
))
|
||||
}
|
||||
|
||||
// Parser for GeneralName
|
||||
pub fn parse_asn1_der_general_name(bytes: &[u8]) -> IResult<&[u8], Asn1DerGeneralName> {
|
||||
let (rest, (tag_val, _, name_value)) = parse_asn1_der_object(bytes)?;
|
||||
let general_name = match tag_val {
|
||||
0xA0 => { // Constructed type, contains type-id and value
|
||||
let (_, (oid, (inner_tag_val, _, value))) = complete(
|
||||
tuple((
|
||||
parse_asn1_der_oid,
|
||||
parse_asn1_der_object
|
||||
))
|
||||
)(name_value)?;
|
||||
if inner_tag_val != 0xA0 {
|
||||
return Err(nom::Err::Error((bytes, ErrorKind::Verify)));
|
||||
}
|
||||
log::info!("Parsed inner tag");
|
||||
// Further parse the value into an ASN.1 DER object
|
||||
let (_, (_, _, name_value)) = complete(
|
||||
parse_asn1_der_object
|
||||
)(value)?;
|
||||
Asn1DerGeneralName::OtherName { type_id: oid, value: name_value }
|
||||
},
|
||||
|
||||
0x81 => {
|
||||
Asn1DerGeneralName::RFC822Name(name_value)
|
||||
},
|
||||
|
||||
0x82 => {
|
||||
Asn1DerGeneralName::DNSName(name_value)
|
||||
},
|
||||
|
||||
0x83 => {
|
||||
Asn1DerGeneralName::X400Address(name_value)
|
||||
},
|
||||
|
||||
0x84 => {
|
||||
Asn1DerGeneralName::DirectoryName(name_value)
|
||||
},
|
||||
|
||||
0xA5 => {
|
||||
let (_, (
|
||||
(name_assigner_tag_val, _, name_assigner),
|
||||
party_name
|
||||
)) = complete(
|
||||
tuple((
|
||||
parse_asn1_der_object,
|
||||
opt(parse_asn1_der_object)
|
||||
))
|
||||
)(name_value)?;
|
||||
|
||||
let general_name = if party_name.is_none() && name_assigner_tag_val == 0x81 {
|
||||
Asn1DerGeneralName::EDIPartyName {
|
||||
name_assigner: &[],
|
||||
party_name: name_assigner
|
||||
}
|
||||
} else if party_name.is_some() && name_assigner_tag_val == 0x80 {
|
||||
if let Some((party_name_tag_val, _, party_name_value)) = party_name {
|
||||
if party_name_tag_val == 0x81 {
|
||||
Asn1DerGeneralName::EDIPartyName {
|
||||
name_assigner,
|
||||
party_name: party_name_value
|
||||
}
|
||||
}
|
||||
else {
|
||||
return Err(nom::Err::Error((bytes, ErrorKind::Verify)))
|
||||
}
|
||||
} else {
|
||||
return Err(nom::Err::Error((bytes, ErrorKind::Verify)))
|
||||
}
|
||||
} else {
|
||||
return Err(nom::Err::Error((bytes, ErrorKind::Verify)))
|
||||
};
|
||||
|
||||
general_name
|
||||
},
|
||||
|
||||
0x86 => {
|
||||
Asn1DerGeneralName::URI(name_value)
|
||||
},
|
||||
|
||||
0x87 => {
|
||||
Asn1DerGeneralName::IPAddress(name_value)
|
||||
},
|
||||
|
||||
0x88 => {
|
||||
Asn1DerGeneralName::RegisteredID(name_value)
|
||||
},
|
||||
|
||||
_ => return Err(nom::Err::Error((bytes, ErrorKind::Verify)))
|
||||
};
|
||||
|
||||
Ok((rest, general_name))
|
||||
}
|
||||
|
||||
// Parser for Name Constraints
|
||||
pub fn parse_asn1_der_name_constraints(bytes: &[u8]) -> IResult<&[u8], Asn1DerExtensionValue> {
|
||||
let (_, subtrees) = complete(
|
||||
parse_asn1_der_sequence
|
||||
)(bytes)?;
|
||||
|
||||
// Init name constraint extension
|
||||
let mut permitted_subtrees = Vec::new();
|
||||
let mut excluded_subtrees = Vec::new();
|
||||
|
||||
let (other_subtree, (mut tag_val, _, mut subtree)) = parse_asn1_der_object(subtrees)?;
|
||||
|
||||
if tag_val == 0xA0 {
|
||||
while subtree.len() != 0 {
|
||||
let (rest, permitted_names) = parse_asn1_der_sequence(subtree)?;
|
||||
|
||||
// Ignore the `minimum` field and `maximum` field
|
||||
// Simpily reject any certificate with these 2 field could be a solution
|
||||
let (_, general_name) = parse_asn1_der_general_name(permitted_names)?;
|
||||
permitted_subtrees.push(general_name);
|
||||
subtree = rest;
|
||||
}
|
||||
|
||||
// Move on to the excluded subtrees, or exit the procedure
|
||||
if other_subtree.len() == 0 {
|
||||
return Ok((
|
||||
&[],
|
||||
Asn1DerExtensionValue::NameConstraints {
|
||||
permitted_subtrees,
|
||||
excluded_subtrees
|
||||
}
|
||||
))
|
||||
} else {
|
||||
let (_, (second_tag_val, _, second_subtree)) = complete(parse_asn1_der_object)(other_subtree)?;
|
||||
tag_val = second_tag_val;
|
||||
subtree = second_subtree;
|
||||
}
|
||||
}
|
||||
|
||||
if tag_val == 0xA1 {
|
||||
while subtree.len() != 0 {
|
||||
let (rest, excluded_names) = parse_asn1_der_sequence(subtree)?;
|
||||
|
||||
// Ignore the `minimum` field and `maximum` field
|
||||
// Simpily reject any certificate with these 2 field could be a solution
|
||||
let (_, general_name) = parse_asn1_der_general_name(excluded_names)?;
|
||||
excluded_subtrees.push(general_name);
|
||||
subtree = rest;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok((
|
||||
&[],
|
||||
Asn1DerExtensionValue::NameConstraints {
|
||||
permitted_subtrees,
|
||||
excluded_subtrees
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
// Parser for CertificatePolicies Extension (sequence: 0x30)
|
||||
pub fn parse_asn1_der_certificate_policies(bytes: &[u8]) -> IResult<&[u8], Asn1DerExtensionValue> {
|
||||
let (rest, (tag_val, _, mut value)) = parse_asn1_der_object(bytes)?;
|
||||
|
|
Loading…
Reference in New Issue