diff --git a/src/krb5/__init__.py b/src/krb5/__init__.py index fe12353..b84ac84 100644 --- a/src/krb5/__init__.py +++ b/src/krb5/__init__.py @@ -35,6 +35,7 @@ Creds, InitCredsContext, Krb5Prompt, + TicketFlags, TicketTimes, get_init_creds_keytab, get_init_creds_password, @@ -100,6 +101,7 @@ "Principal", "PrincipalParseFlags", "PrincipalUnparseFlags", + "TicketFlags", "TicketTimes", "build_principal", "cc_default", diff --git a/src/krb5/_creds.pyi b/src/krb5/_creds.pyi index 82bf12d..d0840c3 100644 --- a/src/krb5/_creds.pyi +++ b/src/krb5/_creds.pyi @@ -1,6 +1,7 @@ # Copyright: (c) 2021 Jordan Borean (@jborean93) # MIT License (see LICENSE or https://opensource.org/licenses/MIT) +import enum import typing from krb5._ccache import CCache @@ -16,6 +17,36 @@ class TicketTimes(typing.NamedTuple): endtime: int renew_till: int +class TicketFlags(enum.IntFlag): + """Kerberos ticket flags. + + See for the definition + of the flags. + + The enc_pa_rep flag is defined by + . + + The anonymous flag is defined by + . + """ + + reserved = 1 << 0 + forwardable = 1 << 1 + forwarded = 1 << 2 + proxiable = 1 << 3 + proxy = 1 << 4 + may_postdate = 1 << 5 + postdated = 1 << 6 + invalid = 1 << 7 + renewable = 1 << 8 + initial = 1 << 9 + pre_authent = 1 << 10 + hw_authent = 1 << 11 + transited_policy_checked = 1 << 12 + ok_as_delegate = 1 << 13 + enc_pa_rep = 1 << 15 + anonymous = 1 << 16 + class Creds: """Kerberos Credentials object. @@ -37,9 +68,12 @@ class Creds: @property def times(self) -> TicketTimes: """Lifetime info.""" - # @property - # def ticket_flags(self) -> int: - # """Flags in ticket.""" + @property + def ticket_flags_raw(self) -> int: + """Flags in ticket, as returned by libkrb5.""" + @property + def ticket_flags(self) -> TicketFlags: + """Flags in ticket, converted to a representation where the first flag is in the lowermost bit.""" # @property # def addresses(self) -> Address: # """Addrs in ticket.""" diff --git a/src/krb5/_creds.pyx b/src/krb5/_creds.pyx index f2fadcc..6532300 100644 --- a/src/krb5/_creds.pyx +++ b/src/krb5/_creds.pyx @@ -2,6 +2,7 @@ # MIT License (see LICENSE or https://opensource.org/licenses/MIT) import collections +import enum import typing from krb5._exceptions import Krb5Error @@ -25,6 +26,7 @@ cdef extern from "python_krb5.h": krb5_principal *server, krb5_keyblock **keyblock, pykrb5_ticket_times *times, + uint32_t *ticket_flags_raw, uint32_t *ticket_flags, // krb5_address ***addresses, krb5_data *ticket, @@ -41,9 +43,18 @@ cdef extern from "python_krb5.h": #endif if (times != NULL) *times = creds->times; #if defined(HEIMDAL_XFREE) + if (ticket_flags_raw != NULL) *ticket_flags_raw = creds->flags.i; if (ticket_flags != NULL) *ticket_flags = creds->flags.i; #else - if (ticket_flags != NULL) *ticket_flags = creds->ticket_flags; + if (ticket_flags_raw != NULL) *ticket_flags_raw = creds->ticket_flags; + if (ticket_flags != NULL) { + *ticket_flags = 0; + // Reverse the order of the bits to get the first bit in the + // lowermost bit instead of in the uppermost bit. + for (int i = 0; i < 32; i++) { + if (creds->ticket_flags & (1 << (31 - i))) *ticket_flags |= (1 << i); + } + } #endif // if (addresses != NULL) *addresses = creds->addresses; if (ticket != NULL) *ticket = creds->ticket; @@ -58,6 +69,7 @@ cdef extern from "python_krb5.h": krb5_principal *server, krb5_keyblock **keyblock, pykrb5_ticket_times *times, + uint32_t *ticket_flags_raw, uint32_t *ticket_flags, # krb5_address ***addresses, krb5_data *ticket, @@ -139,6 +151,31 @@ cdef extern from "python_krb5.h": ) nogil +class TicketFlags(enum.IntFlag): + # https://github.com/krb5/krb5-assignments/blob/master/ticket-flags + reserved = 1 << 0 + forwardable = 1 << 1 + forwarded = 1 << 2 + proxiable = 1 << 3 + proxy = 1 << 4 + may_postdate = 1 << 5 + postdated = 1 << 6 + invalid = 1 << 7 + renewable = 1 << 8 + initial = 1 << 9 + pre_authent = 1 << 10 + hw_authent = 1 << 11 + transited_policy_checked = 1 << 12 + ok_as_delegate = 1 << 13 + enc_pa_rep = 1 << 15 + anonymous = 1 << 16 + + # This is to prevent python >= 3.11 from clearing unknown flags when doing: + # flags = flags & ~TicketFlags.forwarded + # (Under python 3.11, ~TicketFlags.forwarded will contain only known flags.) + _all_flags = (1 << 32) - 1 + + cdef class Creds: # cdef Context ctx # cdef krb5_creds raw @@ -159,7 +196,7 @@ cdef class Creds: @property def client(Creds self) -> Principal: princ = Principal(self.ctx, 0, needs_free=0) - pykrb5_creds_get(&self.raw, &princ.raw, NULL, NULL, NULL, NULL, NULL, NULL) + pykrb5_creds_get(&self.raw, &princ.raw, NULL, NULL, NULL, NULL, NULL, NULL, NULL) # Create a copy of the principal to make sure the returned value # remains valid even if the Creds object is destroyed @@ -170,7 +207,7 @@ cdef class Creds: @property def server(Creds self) -> Principal: princ = Principal(self.ctx, 0, needs_free=0) - pykrb5_creds_get(&self.raw, NULL, &princ.raw, NULL, NULL, NULL, NULL, NULL) + pykrb5_creds_get(&self.raw, NULL, &princ.raw, NULL, NULL, NULL, NULL, NULL, NULL) # Create a copy of the principal to make sure the returned value # remains valid even if the Creds object is destroyed @@ -181,7 +218,7 @@ cdef class Creds: @property def keyblock(Creds self) -> KeyBlock: kb = KeyBlock(self.ctx, needs_free=0) - pykrb5_creds_get(&self.raw, NULL, NULL, &kb.raw, NULL, NULL, NULL, NULL) + pykrb5_creds_get(&self.raw, NULL, NULL, &kb.raw, NULL, NULL, NULL, NULL, NULL) # Create a copy of the keyblock to make sure the returned value # remains valid even if the Creds object is destroyed @@ -192,21 +229,28 @@ cdef class Creds: @property def times(Creds self) -> TicketTimes: cdef pykrb5_ticket_times times - pykrb5_creds_get(&self.raw, NULL, NULL, NULL, ×, NULL, NULL, NULL) + pykrb5_creds_get(&self.raw, NULL, NULL, NULL, ×, NULL, NULL, NULL, NULL) return TicketTimes(times.authtime, times.starttime, times.endtime, times.renew_till) - # @property - # def ticket_flags(Creds self) -> int: - # cdef uint32_t flags - # pykrb5_creds_get(&self.raw, NULL, NULL, NULL, NULL, &flags, NULL, NULL) + @property + def ticket_flags_raw(Creds self) -> int: + cdef uint32_t flags_raw + pykrb5_creds_get(&self.raw, NULL, NULL, NULL, NULL, &flags_raw, NULL, NULL, NULL) + + return flags_raw + + @property + def ticket_flags(Creds self) -> TicketFlags: + cdef uint32_t flags + pykrb5_creds_get(&self.raw, NULL, NULL, NULL, NULL, NULL, &flags, NULL, NULL) - # return flags + return TicketFlags(flags) @property def ticket(Creds self) -> bytes: cdef krb5_data ticket - pykrb5_creds_get(&self.raw, NULL, NULL, NULL, NULL, NULL, &ticket, NULL) + pykrb5_creds_get(&self.raw, NULL, NULL, NULL, NULL, NULL, NULL, &ticket, NULL) cdef size_t length cdef char *value @@ -220,7 +264,7 @@ cdef class Creds: @property def second_ticket(Creds self) -> bytes: cdef krb5_data second_ticket - pykrb5_creds_get(&self.raw, NULL, NULL, NULL, NULL, NULL, NULL, &second_ticket) + pykrb5_creds_get(&self.raw, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &second_ticket) cdef size_t length cdef char *value diff --git a/tests/test_creds.py b/tests/test_creds.py index 85bc82f..d9ea005 100644 --- a/tests/test_creds.py +++ b/tests/test_creds.py @@ -30,6 +30,26 @@ def prompt(self, msg: bytes, hidden: bool) -> bytes: return self._responses.pop(0) +def test_TicketFlags() -> None: + # proxy (1 << 4) and two unknown flags (1 << 24 and 1 << 31) set + flags = krb5.TicketFlags(0x81000010) + + assert krb5.TicketFlags.proxy in flags + assert krb5.TicketFlags.forwarded not in flags + + # Clear proxy and forwarded, leave unknown flags intact + flags = flags & ~(krb5.TicketFlags.proxy | krb5.TicketFlags.forwarded) + assert krb5.TicketFlags.proxy not in flags + assert krb5.TicketFlags.forwarded not in flags + assert flags == 0x81000000 + assert type(flags) == krb5.TicketFlags + + flags = flags | krb5.TicketFlags.postdated + assert krb5.TicketFlags.postdated in flags + assert flags == 0x81000040 + assert type(flags) == krb5.TicketFlags + + def test_get_init_creds_keytab(realm: k5test.K5Realm) -> None: ctx = krb5.init_context() princ = krb5.parse_name_flags(ctx, realm.host_princ.encode()) @@ -44,7 +64,7 @@ def test_get_init_creds_keytab(realm: k5test.K5Realm) -> None: assert creds.server.name == b"krbtgt/KRBTEST.COM@KRBTEST.COM" assert len(creds.keyblock.data) > 0 assert str(creds.times).startswith("TicketTimes(authtime=") - # creds.ticket_flags + assert krb5.TicketFlags.initial in creds.ticket_flags # creds.addresses assert len(creds.ticket) > 0 assert creds.second_ticket == b"" @@ -168,6 +188,18 @@ def test_renew_creds(realm: k5test.K5Realm) -> None: assert creds.client.name == realm.user_princ.encode() assert creds.server.name == b"krbtgt/KRBTEST.COM@KRBTEST.COM" + assert krb5.TicketFlags.initial in creds.ticket_flags + assert krb5.TicketFlags.renewable in creds.ticket_flags + + flags_raw = creds.ticket_flags_raw + flags_raw_reversed = 0 + for i in range(32): + if flags_raw & (1 << i): + flags_raw_reversed |= 1 << (31 - i) + if realm.provider.lower() == "heimdal": + assert creds.ticket_flags == flags_raw + else: + assert creds.ticket_flags == flags_raw_reversed cc = krb5.cc_new_unique(ctx, b"MEMORY") krb5.cc_initialize(ctx, cc, princ) @@ -176,10 +208,16 @@ def test_renew_creds(realm: k5test.K5Realm) -> None: new_creds = krb5.get_renewed_creds(ctx, creds.client, cc) assert new_creds.client.name == realm.user_princ.encode() assert new_creds.server.name == b"krbtgt/KRBTEST.COM@KRBTEST.COM" + if realm.provider.lower() == "heimdal": + # The MIT KDC seems to return renewed tickets with the 'initial' flag + # set. + assert krb5.TicketFlags.initial not in new_creds.ticket_flags new_creds = krb5.get_renewed_creds(ctx, creds.client, cc, b"krbtgt/KRBTEST.COM@KRBTEST.COM") assert new_creds.client.name == realm.user_princ.encode() assert new_creds.server.name == b"krbtgt/KRBTEST.COM@KRBTEST.COM" + if realm.provider.lower() == "heimdal": + assert krb5.TicketFlags.initial not in new_creds.ticket_flags @pytest.mark.requires_api("get_validated_creds") @@ -189,10 +227,12 @@ def test_validate_creds(realm: k5test.K5Realm) -> None: opt = krb5.get_init_creds_opt_alloc(ctx) # Get postdated ticket, ticket will be valid after 1s creds = krb5.get_init_creds_password(ctx, princ, opt, realm.password("user").encode(), start_time=1) - # Ticket flags for creds should have TKT_FLG_POSTDATED and TKT_FLG_INVALID set assert creds.client.name == realm.user_princ.encode() assert creds.server.name == b"krbtgt/KRBTEST.COM@KRBTEST.COM" + # Ticket flags for creds should have TKT_FLG_POSTDATED and TKT_FLG_INVALID set + assert krb5.TicketFlags.postdated in creds.ticket_flags + assert krb5.TicketFlags.invalid in creds.ticket_flags cc = krb5.cc_new_unique(ctx, b"MEMORY") krb5.cc_initialize(ctx, cc, princ) @@ -215,11 +255,15 @@ def test_validate_creds(realm: k5test.K5Realm) -> None: assert new_creds.client.name == realm.user_princ.encode() assert new_creds.server.name == b"krbtgt/KRBTEST.COM@KRBTEST.COM" # Ticket flags for new_creds should have TKT_FLG_POSTDATED set and TKT_FLG_INVALID cleared + assert krb5.TicketFlags.postdated in new_creds.ticket_flags + assert krb5.TicketFlags.invalid not in new_creds.ticket_flags new_creds = krb5.get_validated_creds(ctx, creds.client, cc, b"krbtgt/KRBTEST.COM@KRBTEST.COM") assert new_creds.client.name == realm.user_princ.encode() assert new_creds.server.name == b"krbtgt/KRBTEST.COM@KRBTEST.COM" # Ticket flags for new_creds should have TKT_FLG_POSTDATED set and TKT_FLG_INVALID cleared + assert krb5.TicketFlags.postdated in new_creds.ticket_flags + assert krb5.TicketFlags.invalid not in new_creds.ticket_flags @pytest.mark.requires_api("get_etype_info")