From f37a2d3f6541ccee606b3cf772bcc2f58eb90e0d Mon Sep 17 00:00:00 2001 From: Rostyslav Romanets Date: Mon, 3 Aug 2026 23:37:18 +0200 Subject: [PATCH 1/2] feat(picky-krb): add IAKerb proxy message --- picky-asn1-x509/src/oids.rs | 1 + picky-krb/src/constants.rs | 10 ++ picky-krb/src/gss_api.rs | 203 ++++++++++++++++++++++++++++++++++-- picky-krb/src/messages.rs | 58 ++++++++++- 4 files changed, 262 insertions(+), 10 deletions(-) diff --git a/picky-asn1-x509/src/oids.rs b/picky-asn1-x509/src/oids.rs index 8a244516..7afc1068 100644 --- a/picky-asn1-x509/src/oids.rs +++ b/picky-asn1-x509/src/oids.rs @@ -213,6 +213,7 @@ define_oid! { KRB5 => krb5 => "1.2.840.113554.1.2.2", MS_KRB5 => ms_krb5 => "1.2.840.48018.1.2.2", KRB5_USER_TO_USER => krb5_user_to_user => "1.2.840.113554.1.2.2.3", + IAKERB5 => iakerb5 => "1.3.6.1.5.2.5", NTLM_SSP => ntlm_ssp => "1.3.6.1.4.1.311.2.2.10", NEGOEX => negoex => "1.3.6.1.4.1.311.2.2.30", SPNEGO => spnego => "1.3.6.1.5.5.2", diff --git a/picky-krb/src/constants.rs b/picky-krb/src/constants.rs index 09bd3624..1fc4b1ff 100644 --- a/picky-krb/src/constants.rs +++ b/picky-krb/src/constants.rs @@ -111,6 +111,10 @@ pub mod gss_api { //= [Authenticator Checksum](https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1) =// pub const AUTHENTICATOR_CHECKSUM_TYPE: [u8; 3] = [0x00, 0x80, 0x03]; + + /// [The IAKERB GSS-API](https://datatracker.ietf.org/doc/html/draft-ietf-kitten-iakerb-03#section-3) + /// IAKERB_PROXY 05 01 + pub const IAKERB_PROXY_TOKEN_ID: [u8; 2] = [0x05, 0x01]; } //= [Kerberos Change Password and Set Password Protocols](https://datatracker.ietf.org/doc/html/rfc3244) =// @@ -226,4 +230,10 @@ pub mod error_codes { pub const KDC_ERR_REVOCATION_STATUS_UNAVAILABLE: u32 = 74; pub const KDC_ERR_CLIENT_NAME_MISMATCH: u32 = 75; pub const KDC_ERR_KDC_NAME_MISMATCH: u32 = 76; + + //= [IAKERB Error Codes](https://datatracker.ietf.org/doc/html/draft-ietf-kitten-iakerb-03#section-3) =// + /// The IAKERB proxy could not find a KDC. + pub const KRB_AP_ERR_IAKERB_KDC_NOT_FOUND: u32 = 85; + /// The KDC did not respond to the IAKERB proxy. + pub const KRB_AP_ERR_IAKERB_KDC_NO_RESPONSE: u32 = 86; } diff --git a/picky-krb/src/gss_api.rs b/picky-krb/src/gss_api.rs index 8114dcdb..8e231bcc 100644 --- a/picky-krb/src/gss_api.rs +++ b/picky-krb/src/gss_api.rs @@ -8,11 +8,13 @@ use picky_asn1::wrapper::{ ObjectIdentifierAsn1, OctetStringAsn1, Optional, }; use picky_asn1_der::{Asn1DerError, Asn1RawDer}; +use picky_asn1_x509::oids::iakerb5; use serde::de::{self, DeserializeOwned}; -use serde::{Deserialize, Serialize, ser}; +use serde::{Deserialize, Serialize, Serializer, ser}; use thiserror::Error; -use crate::constants::gss_api::{MIC_FILLER, MIC_TOKEN_ID, WRAP_FILLER, WRAP_TOKEN_ID}; +use crate::constants::gss_api::{IAKERB_PROXY_TOKEN_ID, MIC_FILLER, MIC_TOKEN_ID, WRAP_FILLER, WRAP_TOKEN_ID}; +use crate::messages::IAKerbHeader; const MIC_TOKEN_INITIATOR_DEFAULT_FLAGS: u8 = 0x04; const MIC_TOKEN_ACCEPTOR_DEFAULT_FLAGS: u8 = 0x05; @@ -23,6 +25,8 @@ const WRAP_HEADER_LEN: usize = 16; pub enum GssApiMessageError { #[error("Invalid token id. Expected {0:?} but got {1:?}")] InvalidId([u8; 2], [u8; 2]), + #[error("Invalid mechanism OID. Expected {0} but got {1}")] + InvalidMechanismOid(String, String), #[error("IO error: {0:?}")] IoError(#[from] io::Error), #[error("Invalid MIC token filler {0:?}")] @@ -179,6 +183,102 @@ impl ser::Serialize for KrbMessage { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IAKrbProxyMessage { + pub header: IAKerbHeader, + pub krb_msg: T, +} + +impl IAKrbProxyMessage { + pub fn encode(&self, mut data: impl Write) -> Result<(), GssApiMessageError> { + let mut oid = Vec::new(); + + { + let mut s = picky_asn1_der::Serializer::new_to_byte_buf(&mut oid); + ObjectIdentifierAsn1(iakerb5()).serialize(&mut s)?; + } + + data.write_all(&oid)?; + data.write_all(&IAKERB_PROXY_TOKEN_ID)?; + data.write_all(&picky_asn1_der::to_vec(&self.header)?)?; + data.write_all(&picky_asn1_der::to_vec(&self.krb_msg)?)?; + + Ok(()) + } +} + +impl IAKrbProxyMessage { + /// Deserializes `ApplicationTag0>`. + pub fn decode_application_iakrb_proxy_message( + mut data: &[u8], + ) -> Result>, GssApiMessageError> { + if data.is_empty() || Tag::from(data[0]) != Tag::application_constructed(0) { + return Err(GssApiMessageError::Asn1Error(Asn1DerError::InvalidData)); + } + + // We cannot implement the deserialization using the `Deserialize` trait + // because the iakerb token id is not an ASN1 field, but plain two-byte value. + // We cannot read this id using our ASN1 deserializer. + + // This is a workaround we use to read the `ApplicationTag0` tag and length bytes. + // At the same time it will also read the first field of the `IAKrbProxyMessage`. + #[derive(Deserialize)] + struct Container { + iakerb_oid: ObjectIdentifierAsn1, + } + + let max_len = data.len(); + let mut reader = &mut data; + let Container { iakerb_oid } = + Container::deserialize(&mut picky_asn1_der::Deserializer::new_from_reader(&mut reader, max_len))?; + + if iakerb_oid.0 != iakerb5() { + return Err(GssApiMessageError::InvalidMechanismOid( + iakerb5().into(), + iakerb_oid.0.into(), + )); + } + + let mut token_id = [0, 0]; + reader.read_exact(&mut token_id)?; + + if token_id != IAKERB_PROXY_TOKEN_ID { + return Err(GssApiMessageError::InvalidId(IAKERB_PROXY_TOKEN_ID, token_id)); + } + + let max_len = data.len(); + let mut reader = &mut data; + let mut der = picky_asn1_der::Deserializer::new_from_reader(&mut reader, max_len); + + let header = IAKerbHeader::deserialize(&mut der)?; + let krb_msg: T = T::deserialize(&mut der)?; + + Ok(ApplicationTag0(IAKrbProxyMessage { header, krb_msg })) + } +} + +impl Serialize for IAKrbProxyMessage { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + use serde::ser::Error; + + // We encode `IAKrbProxyMessage` fields using `IAKrbProxyMessage::encode` method. + // We use the `Container` type to prepend the sequence tag and length to the encoded fields. + #[derive(Serialize)] + struct Container { + buff: Asn1RawDer, + } + + let mut buff = Vec::new(); + self.encode(&mut buff) + .map_err(|e| S::Error::custom(format!("cannot serialize IAKrbProxyMessage inner value: {e:?}")))?; + + Container { buff: Asn1RawDer(buff) }.serialize(serializer) + } +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct GssApiNegInit { pub oid: ObjectIdentifierAsn1, @@ -451,17 +551,22 @@ impl WrapToken { #[cfg(test)] mod tests { - use picky_asn1::restricted_string::IA5String; + use picky_asn1::bit_string::BitString; + use picky_asn1::date::Date; + use picky_asn1::restricted_string::{IA5String, Ia5String}; use picky_asn1::wrapper::{ - Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3, + Asn1SequenceOf, BitStringAsn1, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, + ExplicitContextTag3, ExplicitContextTag4, ExplicitContextTag5, ExplicitContextTag7, ExplicitContextTag8, GeneralStringAsn1, IntegerAsn1, ObjectIdentifierAsn1, OctetStringAsn1, Optional, }; use picky_asn1_x509::oids; use crate::constants::types::TGT_REP_MSG_TYPE; - use crate::data_types::{EncryptedData, KerberosStringAsn1, PrincipalName, Ticket, TicketInner}; - use crate::gss_api::{ApplicationTag0, MicToken, WrapToken}; - use crate::messages::TgtRep; + use crate::data_types::{ + EncryptedData, KerberosStringAsn1, KerberosTime, PaData, PrincipalName, Ticket, TicketInner, + }; + use crate::gss_api::{ApplicationTag0, IAKrbProxyMessage, MicToken, WrapToken}; + use crate::messages::{AsReq, IAKerbHeader, KdcReq, KdcReqBody, TgtRep}; use super::KrbMessage; @@ -657,4 +762,88 @@ mod tests { assert_eq!(krb_message, expected); assert_eq!(krb_message_raw, expected_raw); } + + #[test] + fn iakerb_proxy_message() { + let expected_raw = vec![ + 96, 129, 225, 6, 6, 43, 6, 1, 5, 2, 5, 5, 1, 48, 29, 161, 13, 12, 11, 69, 88, 65, 77, 80, 76, 69, 46, 67, + 79, 77, 162, 12, 4, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 106, 129, 181, 48, 129, 178, 161, 3, 2, 1, 5, 162, + 3, 2, 1, 10, 163, 26, 48, 24, 48, 10, 161, 4, 2, 2, 0, 150, 162, 2, 4, 0, 48, 10, 161, 4, 2, 2, 0, 149, + 162, 2, 4, 0, 164, 129, 137, 48, 129, 134, 160, 7, 3, 5, 0, 0, 0, 0, 16, 161, 19, 48, 17, 160, 3, 2, 1, 1, + 161, 10, 48, 8, 27, 6, 109, 121, 117, 115, 101, 114, 162, 13, 27, 11, 69, 88, 65, 77, 80, 76, 69, 46, 67, + 79, 77, 163, 32, 48, 30, 160, 3, 2, 1, 2, 161, 23, 48, 21, 27, 6, 107, 114, 98, 116, 103, 116, 27, 11, 69, + 88, 65, 77, 80, 76, 69, 46, 67, 79, 77, 165, 17, 24, 15, 50, 48, 50, 49, 49, 50, 50, 57, 49, 48, 51, 54, + 48, 54, 90, 167, 6, 2, 4, 29, 32, 235, 11, 168, 26, 48, 24, 2, 1, 18, 2, 1, 17, 2, 1, 20, 2, 1, 19, 2, 1, + 16, 2, 1, 23, 2, 1, 25, 2, 1, 26, + ]; + + let expected = ApplicationTag0(IAKrbProxyMessage { + header: IAKerbHeader { + target_realm: ExplicitContextTag1::from("EXAMPLE.COM".to_string()), + cookie: Optional::from(Some(ExplicitContextTag2::from(OctetStringAsn1::from(vec![ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + ])))), + flags: Optional::from(None), + }, + krb_msg: AsReq::from(KdcReq { + pvno: ExplicitContextTag1::from(IntegerAsn1(vec![5])), + msg_type: ExplicitContextTag2::from(IntegerAsn1(vec![10])), + padata: Optional::from(Some(ExplicitContextTag3::from(Asn1SequenceOf::from(vec![ + PaData { + padata_type: ExplicitContextTag1::from(IntegerAsn1(vec![0, 150])), + padata_data: ExplicitContextTag2::from(OctetStringAsn1(Vec::new())), + }, + PaData { + padata_type: ExplicitContextTag1::from(IntegerAsn1(vec![0, 149])), + padata_data: ExplicitContextTag2::from(OctetStringAsn1(Vec::new())), + }, + ])))), + req_body: ExplicitContextTag4::from(KdcReqBody { + kdc_options: ExplicitContextTag0::from(BitStringAsn1::from(BitString::with_bytes(vec![ + 0, 0, 0, 16, + ]))), + cname: Optional::from(Some(ExplicitContextTag1::from(PrincipalName { + name_type: ExplicitContextTag0::from(IntegerAsn1(vec![1])), + name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![GeneralStringAsn1::from( + Ia5String::from_string("myuser".to_owned()).unwrap(), + )])), + }))), + realm: ExplicitContextTag2::from(GeneralStringAsn1::from( + Ia5String::from_string("EXAMPLE.COM".to_owned()).unwrap(), + )), + sname: Optional::from(Some(ExplicitContextTag3::from(PrincipalName { + name_type: ExplicitContextTag0::from(IntegerAsn1(vec![2])), + name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![ + KerberosStringAsn1::from(Ia5String::from_string("krbtgt".to_owned()).unwrap()), + KerberosStringAsn1::from(Ia5String::from_string("EXAMPLE.COM".to_owned()).unwrap()), + ])), + }))), + from: Optional::from(None), + till: ExplicitContextTag5::from(KerberosTime::from(Date::new(2021, 12, 29, 10, 36, 6).unwrap())), + rtime: Optional::from(None), + nonce: ExplicitContextTag7::from(IntegerAsn1(vec![29, 32, 235, 11])), + etype: ExplicitContextTag8::from(Asn1SequenceOf::from(vec![ + IntegerAsn1(vec![18]), + IntegerAsn1(vec![17]), + IntegerAsn1(vec![20]), + IntegerAsn1(vec![19]), + IntegerAsn1(vec![16]), + IntegerAsn1(vec![23]), + IntegerAsn1(vec![25]), + IntegerAsn1(vec![26]), + ])), + addresses: Optional::from(None), + enc_authorization_data: Optional::from(None), + additional_tickets: Optional::from(None), + }), + }), + }); + + let iakerb_proxy_message = + IAKrbProxyMessage::::decode_application_iakrb_proxy_message(expected_raw.as_slice()).unwrap(); + let iakerb_proxy_message_raw = picky_asn1_der::to_vec(&expected).unwrap(); + + assert_eq!(iakerb_proxy_message, expected); + assert_eq!(iakerb_proxy_message_raw, expected_raw); + } } diff --git a/picky-krb/src/messages.rs b/picky-krb/src/messages.rs index 70d97f7c..1ca6844a 100644 --- a/picky-krb/src/messages.rs +++ b/picky-krb/src/messages.rs @@ -3,7 +3,7 @@ use std::io::{self, Read}; use picky_asn1::tag::{TagClass, TagPeeker}; use picky_asn1::wrapper::{ - Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3, + Asn1SequenceOf, BitStringAsn1, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3, ExplicitContextTag4, ExplicitContextTag5, ExplicitContextTag6, ExplicitContextTag7, ExplicitContextTag8, ExplicitContextTag9, ExplicitContextTag10, ExplicitContextTag11, ExplicitContextTag12, IntegerAsn1, OctetStringAsn1, Optional, @@ -445,6 +445,36 @@ impl ser::Serialize for KrbPrivMessage { } } +/// Opaque data, if sent by the server, MUST be copied by the client verbatim into the next IAKRB_PROXY message. +/// +/// [draft-ietf-kitten-iakerb-03 3](https://datatracker.ietf.org/doc/html/draft-ietf-kitten-iakerb-03#section-3) +pub type IAKerbCookie = Option>; + +/// [draft-ietf-kitten-iakerb-03 3](https://datatracker.ietf.org/doc/html/draft-ietf-kitten-iakerb-03#section-3) +/// +/// ```not_rust +/// IAKERB-HEADER ::= SEQUENCE { +/// -- Note that the tag numbers start at 1, not 0, which would +/// -- be more conventional for Kerberos. +/// target-realm [1] UTF8String, +/// -- The name of the target realm. +/// cookie [2] OCTET STRING OPTIONAL, +/// -- Opaque data, if sent by the server, +/// -- MUST be copied by the client verbatim into +/// -- the next IAKRB_PROXY message. +/// ... +/// } +/// ``` +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct IAKerbHeader { + pub target_realm: ExplicitContextTag1, + #[serde(default)] + pub cookie: Optional, + // This field is not specified in the RFC but present in real messages. + #[serde(default)] + pub flags: Optional>>, +} + #[cfg(test)] mod tests { use crate::constants::error_codes::{KDC_ERR_C_PRINCIPAL_UNKNOWN, KRB_AP_ERR_INAPP_CKSUM}; @@ -453,8 +483,8 @@ mod tests { Ticket, TicketInner, }; use crate::messages::{ - ApMessage, ApRep, ApRepInner, ApReq, ApReqInner, AsRep, AsReq, KdcProxyMessage, KdcRep, KdcReq, KdcReqBody, - KrbError, KrbErrorInner, KrbPriv, KrbPrivInner, KrbPrivMessage, TgsReq, + ApMessage, ApRep, ApRepInner, ApReq, ApReqInner, AsRep, AsReq, IAKerbHeader, KdcProxyMessage, KdcRep, KdcReq, + KdcReqBody, KrbError, KrbErrorInner, KrbPriv, KrbPrivInner, KrbPrivMessage, TgsReq, }; use picky_asn1::bit_string::BitString; @@ -1178,4 +1208,26 @@ mod tests { assert_eq!(expected, tgs_req); assert_eq!(expected_raw, tgs_req_raw); } + + #[test] + fn iakerb_header() { + let expected_raw = vec![ + 48, 29, 161, 13, 12, 11, 69, 88, 65, 77, 80, 76, 69, 46, 67, 79, 77, 162, 12, 4, 10, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, + ]; + + let expected = IAKerbHeader { + target_realm: ExplicitContextTag1::from("EXAMPLE.COM".to_string()), + cookie: Optional::from(Some(ExplicitContextTag2::from(OctetStringAsn1::from(vec![ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + ])))), + flags: Optional::from(None), + }; + + let iakerb: IAKerbHeader = picky_asn1_der::from_bytes(&expected_raw).unwrap(); + let iakerb_raw = picky_asn1_der::to_vec(&expected).unwrap(); + + assert_eq!(expected, iakerb); + assert_eq!(expected_raw, iakerb_raw); + } } From a856b0543d732bc2a919b8d97a1c74f45adb3771 Mon Sep 17 00:00:00 2001 From: Rostyslav Romanets Date: Mon, 28 Sep 2026 13:56:58 +0200 Subject: [PATCH 2/2] refactor(picky-krb): apply code review suggestions from Copilot --- picky-asn1-der/src/lib.rs | 1 + picky-krb/src/gss_api.rs | 162 +++++++++++++++++++++++++++++++++----- picky-krb/src/messages.rs | 64 +++++++++++---- 3 files changed, 191 insertions(+), 36 deletions(-) diff --git a/picky-asn1-der/src/lib.rs b/picky-asn1-der/src/lib.rs index 7b7dae72..9dc4fe94 100644 --- a/picky-asn1-der/src/lib.rs +++ b/picky-asn1-der/src/lib.rs @@ -79,6 +79,7 @@ mod raw_der; mod ser; pub use crate::de::{Deserializer, from_bytes, from_reader, from_reader_with_max_len}; +pub use crate::misc::Length; pub use crate::raw_der::Asn1RawDer; pub use crate::ser::{Serializer, to_byte_buf, to_bytes, to_vec, to_writer}; diff --git a/picky-krb/src/gss_api.rs b/picky-krb/src/gss_api.rs index 8e231bcc..ab68c86a 100644 --- a/picky-krb/src/gss_api.rs +++ b/picky-krb/src/gss_api.rs @@ -7,7 +7,7 @@ use picky_asn1::wrapper::{ Asn1SequenceOf, BitStringAsn1, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3, ObjectIdentifierAsn1, OctetStringAsn1, Optional, }; -use picky_asn1_der::{Asn1DerError, Asn1RawDer}; +use picky_asn1_der::{Asn1DerError, Asn1RawDer, Length}; use picky_asn1_x509::oids::iakerb5; use serde::de::{self, DeserializeOwned}; use serde::{Deserialize, Serialize, Serializer, ser}; @@ -124,12 +124,17 @@ impl KrbMessage { impl KrbMessage { /// Deserializes `ApplicationTag0>`. pub fn decode_application_krb_message( - mut data: &[u8], + data: &[u8], ) -> Result>, GssApiMessageError> { if data.is_empty() || Tag::from(data[0]) != Tag::application_constructed(0) { return Err(GssApiMessageError::Asn1Error(Asn1DerError::InvalidData)); } + let total_len = der_tlv_total_len(data)?; + let mut data = data + .get(..total_len) + .ok_or(GssApiMessageError::Asn1Error(Asn1DerError::TruncatedData))?; + // We cannot implement the deserialization using the `Deserialize` trait // because the krb5 token id is not an ASN1 field, but plain two-byte value. // We cannot read this id using our ASN1 deserializer. @@ -153,6 +158,10 @@ impl KrbMessage { let mut reader = &mut data; let krb_msg: T = T::deserialize(&mut picky_asn1_der::Deserializer::new_from_reader(&mut reader, max_len))?; + if !data.is_empty() { + return Err(GssApiMessageError::Asn1Error(Asn1DerError::InvalidData)); + } + Ok(ApplicationTag0(KrbMessage { krb5_oid, krb5_token_id, @@ -184,12 +193,12 @@ impl ser::Serialize for KrbMessage { } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct IAKrbProxyMessage { +pub struct IAKerbProxyMessage { pub header: IAKerbHeader, pub krb_msg: T, } -impl IAKrbProxyMessage { +impl IAKerbProxyMessage { pub fn encode(&self, mut data: impl Write) -> Result<(), GssApiMessageError> { let mut oid = Vec::new(); @@ -207,21 +216,26 @@ impl IAKrbProxyMessage { } } -impl IAKrbProxyMessage { - /// Deserializes `ApplicationTag0>`. - pub fn decode_application_iakrb_proxy_message( - mut data: &[u8], - ) -> Result>, GssApiMessageError> { +impl IAKerbProxyMessage { + /// Deserializes `ApplicationTag0>`. + pub fn decode_application_iakerb_proxy_message( + data: &[u8], + ) -> Result>, GssApiMessageError> { if data.is_empty() || Tag::from(data[0]) != Tag::application_constructed(0) { return Err(GssApiMessageError::Asn1Error(Asn1DerError::InvalidData)); } + let total_len = der_tlv_total_len(data)?; + let mut data = data + .get(..total_len) + .ok_or(GssApiMessageError::Asn1Error(Asn1DerError::TruncatedData))?; + // We cannot implement the deserialization using the `Deserialize` trait // because the iakerb token id is not an ASN1 field, but plain two-byte value. // We cannot read this id using our ASN1 deserializer. // This is a workaround we use to read the `ApplicationTag0` tag and length bytes. - // At the same time it will also read the first field of the `IAKrbProxyMessage`. + // At the same time it will also read the first field of the `IAKerbProxyMessage`. #[derive(Deserialize)] struct Container { iakerb_oid: ObjectIdentifierAsn1, @@ -248,23 +262,32 @@ impl IAKrbProxyMessage { let max_len = data.len(); let mut reader = &mut data; - let mut der = picky_asn1_der::Deserializer::new_from_reader(&mut reader, max_len); - let header = IAKerbHeader::deserialize(&mut der)?; - let krb_msg: T = T::deserialize(&mut der)?; + let (header, krb_msg) = { + let mut der = picky_asn1_der::Deserializer::new_from_reader(&mut reader, max_len); + + let header = IAKerbHeader::deserialize(&mut der)?; + let krb_msg: T = T::deserialize(&mut der)?; + + (header, krb_msg) + }; + + if !data.is_empty() { + return Err(GssApiMessageError::Asn1Error(Asn1DerError::InvalidData)); + } - Ok(ApplicationTag0(IAKrbProxyMessage { header, krb_msg })) + Ok(ApplicationTag0(IAKerbProxyMessage { header, krb_msg })) } } -impl Serialize for IAKrbProxyMessage { +impl Serialize for IAKerbProxyMessage { fn serialize(&self, serializer: S) -> Result where S: Serializer, { use serde::ser::Error; - // We encode `IAKrbProxyMessage` fields using `IAKrbProxyMessage::encode` method. + // We encode `IAKerbProxyMessage` fields using `IAKerbProxyMessage::encode` method. // We use the `Container` type to prepend the sequence tag and length to the encoded fields. #[derive(Serialize)] struct Container { @@ -273,7 +296,7 @@ impl Serialize for IAKrbProxyMessage { let mut buff = Vec::new(); self.encode(&mut buff) - .map_err(|e| S::Error::custom(format!("cannot serialize IAKrbProxyMessage inner value: {e:?}")))?; + .map_err(|e| S::Error::custom(format!("cannot serialize IAKerbProxyMessage inner value: {e:?}")))?; Container { buff: Asn1RawDer(buff) }.serialize(serializer) } @@ -549,6 +572,22 @@ impl WrapToken { } } +/// Reads the DER TLV header and returns the total length (header + content) of the TLV. +fn der_tlv_total_len(data: &[u8]) -> Result { + if data.len() < 2 { + return Err(GssApiMessageError::Asn1Error(Asn1DerError::TruncatedData)); + } + + let content_len = Length::deserialized(&data[1..]).map_err(GssApiMessageError::Asn1Error)?; + let header_len = 1 + Length::encoded_len(content_len); + + let total_len = header_len + .checked_add(content_len) + .ok_or(GssApiMessageError::Asn1Error(Asn1DerError::UnsupportedValue))?; + + Ok(total_len) +} + #[cfg(test)] mod tests { use picky_asn1::bit_string::BitString; @@ -565,7 +604,7 @@ mod tests { use crate::data_types::{ EncryptedData, KerberosStringAsn1, KerberosTime, PaData, PrincipalName, Ticket, TicketInner, }; - use crate::gss_api::{ApplicationTag0, IAKrbProxyMessage, MicToken, WrapToken}; + use crate::gss_api::{ApplicationTag0, IAKerbProxyMessage, MicToken, WrapToken}; use crate::messages::{AsReq, IAKerbHeader, KdcReq, KdcReqBody, TgtRep}; use super::KrbMessage; @@ -777,7 +816,7 @@ mod tests { 16, 2, 1, 23, 2, 1, 25, 2, 1, 26, ]; - let expected = ApplicationTag0(IAKrbProxyMessage { + let expected = ApplicationTag0(IAKerbProxyMessage { header: IAKerbHeader { target_realm: ExplicitContextTag1::from("EXAMPLE.COM".to_string()), cookie: Optional::from(Some(ExplicitContextTag2::from(OctetStringAsn1::from(vec![ @@ -840,10 +879,93 @@ mod tests { }); let iakerb_proxy_message = - IAKrbProxyMessage::::decode_application_iakrb_proxy_message(expected_raw.as_slice()).unwrap(); + IAKerbProxyMessage::::decode_application_iakerb_proxy_message(expected_raw.as_slice()).unwrap(); let iakerb_proxy_message_raw = picky_asn1_der::to_vec(&expected).unwrap(); assert_eq!(iakerb_proxy_message, expected); assert_eq!(iakerb_proxy_message_raw, expected_raw); } + + #[test] + fn iakerb_header_deserialize_skips_unknown_extension_fields() { + let iakerb_proxy_message_raw = vec![ + 96, 129, 236, 6, 6, 43, 6, 1, 5, 2, 5, 5, 1, 48, 40, 161, 13, 12, 11, 69, 88, 65, 77, 80, 76, 69, 46, 67, + 79, 77, 162, 12, 4, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 164, 9, 12, 7, 117, 110, 107, 110, 111, 119, 110, + 106, 129, 181, 48, 129, 178, 161, 3, 2, 1, 5, 162, 3, 2, 1, 10, 163, 26, 48, 24, 48, 10, 161, 4, 2, 2, 0, + 150, 162, 2, 4, 0, 48, 10, 161, 4, 2, 2, 0, 149, 162, 2, 4, 0, 164, 129, 137, 48, 129, 134, 160, 7, 3, 5, + 0, 0, 0, 0, 16, 161, 19, 48, 17, 160, 3, 2, 1, 1, 161, 10, 48, 8, 27, 6, 109, 121, 117, 115, 101, 114, 162, + 13, 27, 11, 69, 88, 65, 77, 80, 76, 69, 46, 67, 79, 77, 163, 32, 48, 30, 160, 3, 2, 1, 2, 161, 23, 48, 21, + 27, 6, 107, 114, 98, 116, 103, 116, 27, 11, 69, 88, 65, 77, 80, 76, 69, 46, 67, 79, 77, 165, 17, 24, 15, + 50, 48, 50, 49, 49, 50, 50, 57, 49, 48, 51, 54, 48, 54, 90, 167, 6, 2, 4, 29, 32, 235, 11, 168, 26, 48, 24, + 2, 1, 18, 2, 1, 17, 2, 1, 20, 2, 1, 19, 2, 1, 16, 2, 1, 23, 2, 1, 25, 2, 1, 26, + ]; + + let expected = ApplicationTag0(IAKerbProxyMessage { + header: IAKerbHeader { + target_realm: ExplicitContextTag1::from("EXAMPLE.COM".to_string()), + cookie: Optional::from(Some(ExplicitContextTag2::from(OctetStringAsn1::from(vec![ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + ])))), + flags: Optional::from(None), + }, + krb_msg: AsReq::from(KdcReq { + pvno: ExplicitContextTag1::from(IntegerAsn1(vec![5])), + msg_type: ExplicitContextTag2::from(IntegerAsn1(vec![10])), + padata: Optional::from(Some(ExplicitContextTag3::from(Asn1SequenceOf::from(vec![ + PaData { + padata_type: ExplicitContextTag1::from(IntegerAsn1(vec![0, 150])), + padata_data: ExplicitContextTag2::from(OctetStringAsn1(Vec::new())), + }, + PaData { + padata_type: ExplicitContextTag1::from(IntegerAsn1(vec![0, 149])), + padata_data: ExplicitContextTag2::from(OctetStringAsn1(Vec::new())), + }, + ])))), + req_body: ExplicitContextTag4::from(KdcReqBody { + kdc_options: ExplicitContextTag0::from(BitStringAsn1::from(BitString::with_bytes(vec![ + 0, 0, 0, 16, + ]))), + cname: Optional::from(Some(ExplicitContextTag1::from(PrincipalName { + name_type: ExplicitContextTag0::from(IntegerAsn1(vec![1])), + name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![GeneralStringAsn1::from( + Ia5String::from_string("myuser".to_owned()).unwrap(), + )])), + }))), + realm: ExplicitContextTag2::from(GeneralStringAsn1::from( + Ia5String::from_string("EXAMPLE.COM".to_owned()).unwrap(), + )), + sname: Optional::from(Some(ExplicitContextTag3::from(PrincipalName { + name_type: ExplicitContextTag0::from(IntegerAsn1(vec![2])), + name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![ + KerberosStringAsn1::from(Ia5String::from_string("krbtgt".to_owned()).unwrap()), + KerberosStringAsn1::from(Ia5String::from_string("EXAMPLE.COM".to_owned()).unwrap()), + ])), + }))), + from: Optional::from(None), + till: ExplicitContextTag5::from(KerberosTime::from(Date::new(2021, 12, 29, 10, 36, 6).unwrap())), + rtime: Optional::from(None), + nonce: ExplicitContextTag7::from(IntegerAsn1(vec![29, 32, 235, 11])), + etype: ExplicitContextTag8::from(Asn1SequenceOf::from(vec![ + IntegerAsn1(vec![18]), + IntegerAsn1(vec![17]), + IntegerAsn1(vec![20]), + IntegerAsn1(vec![19]), + IntegerAsn1(vec![16]), + IntegerAsn1(vec![23]), + IntegerAsn1(vec![25]), + IntegerAsn1(vec![26]), + ])), + addresses: Optional::from(None), + enc_authorization_data: Optional::from(None), + additional_tickets: Optional::from(None), + }), + }), + }); + + let iakerb_proxy_message = + IAKerbProxyMessage::::decode_application_iakerb_proxy_message(iakerb_proxy_message_raw.as_slice()) + .unwrap(); + + assert_eq!(iakerb_proxy_message, expected); + } } diff --git a/picky-krb/src/messages.rs b/picky-krb/src/messages.rs index 1ca6844a..4c02f724 100644 --- a/picky-krb/src/messages.rs +++ b/picky-krb/src/messages.rs @@ -1,6 +1,15 @@ use std::fmt; use std::io::{self, Read}; +use crate::constants::krb_priv::KRB_PRIV_VERSION; +use crate::constants::types::{ + AP_REP_MSG_TYPE, AP_REQ_MSG_TYPE, AS_REP_MSG_TYPE, AS_REQ_MSG_TYPE, ENC_AS_REP_PART_TYPE, ENC_TGS_REP_PART_TYPE, + KRB_ERROR_MSG_TYPE, KRB_PRIV, TGS_REP_MSG_TYPE, TGS_REQ_MSG_TYPE, +}; +use crate::data_types::{ + ApOptions, EncryptedData, EncryptionKey, HostAddresses, KerberosFlags, KerberosStringAsn1, KerberosTime, LastReq, + Microseconds, PaData, PrincipalName, Realm, Ticket, +}; use picky_asn1::tag::{TagClass, TagPeeker}; use picky_asn1::wrapper::{ Asn1SequenceOf, BitStringAsn1, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3, @@ -10,19 +19,10 @@ use picky_asn1::wrapper::{ }; use picky_asn1_der::application_tag::ApplicationTag; use picky_asn1_der::{Asn1DerError, Asn1RawDer}; -use serde::ser::Error; +use serde::de::Error as _; +use serde::ser::Error as _; use serde::{Deserialize, Serialize, de, ser}; -use crate::constants::krb_priv::KRB_PRIV_VERSION; -use crate::constants::types::{ - AP_REP_MSG_TYPE, AP_REQ_MSG_TYPE, AS_REP_MSG_TYPE, AS_REQ_MSG_TYPE, ENC_AS_REP_PART_TYPE, ENC_TGS_REP_PART_TYPE, - KRB_ERROR_MSG_TYPE, KRB_PRIV, TGS_REP_MSG_TYPE, TGS_REQ_MSG_TYPE, -}; -use crate::data_types::{ - ApOptions, EncryptedData, EncryptionKey, HostAddresses, KerberosFlags, KerberosStringAsn1, KerberosTime, LastReq, - Microseconds, PaData, PrincipalName, Realm, Ticket, -}; - /// [2.2.2 KDC_PROXY_MESSAGE](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-kkdcp/5778aff5-b182-4b97-a970-29c7f911eef2) /// /// ```not_rust @@ -445,7 +445,7 @@ impl ser::Serialize for KrbPrivMessage { } } -/// Opaque data, if sent by the server, MUST be copied by the client verbatim into the next IAKRB_PROXY message. +/// Opaque data, if sent by the server, MUST be copied by the client verbatim into the next IAKERB_PROXY message. /// /// [draft-ietf-kitten-iakerb-03 3](https://datatracker.ietf.org/doc/html/draft-ietf-kitten-iakerb-03#section-3) pub type IAKerbCookie = Option>; @@ -461,20 +461,52 @@ pub type IAKerbCookie = Option>; /// cookie [2] OCTET STRING OPTIONAL, /// -- Opaque data, if sent by the server, /// -- MUST be copied by the client verbatim into -/// -- the next IAKRB_PROXY message. +/// -- the next IAKERB_PROXY message. /// ... /// } /// ``` -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] pub struct IAKerbHeader { pub target_realm: ExplicitContextTag1, - #[serde(default)] pub cookie: Optional, // This field is not specified in the RFC but present in real messages. - #[serde(default)] pub flags: Optional>>, } +// IAKERB-HEADER is extensible, so we need a custom deserialization implementation +// to consume the entire header while parsing only the known fields. +// The derived implementation would leave the unknown fields of the header unconsumed, +// which would cause the subsequent deserialization to fail. +impl<'de> de::Deserialize<'de> for IAKerbHeader { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + #[derive(Deserialize)] + struct IAKerbHeaderHelper { + target_realm: ExplicitContextTag1, + #[serde(default)] + cookie: Optional, + #[serde(default)] + flags: Optional>>, + } + + let Asn1RawDer(raw) = Asn1RawDer::deserialize(deserializer)?; + let IAKerbHeaderHelper { + target_realm, + cookie, + flags, + } = picky_asn1_der::from_bytes(&raw) + .map_err(|err| D::Error::custom(format!("Cannot deserialize IAKerbHeader: {err:?}")))?; + + Ok(IAKerbHeader { + target_realm, + cookie, + flags, + }) + } +} + #[cfg(test)] mod tests { use crate::constants::error_codes::{KDC_ERR_C_PRINCIPAL_UNKNOWN, KRB_AP_ERR_INAPP_CKSUM};