Skip to content
Merged
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: 2 additions & 0 deletions src/krb5/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
Creds,
InitCredsContext,
Krb5Prompt,
TicketFlags,
TicketTimes,
get_init_creds_keytab,
get_init_creds_password,
Expand Down Expand Up @@ -100,6 +101,7 @@
"Principal",
"PrincipalParseFlags",
"PrincipalUnparseFlags",
"TicketFlags",
"TicketTimes",
"build_principal",
"cc_default",
Expand Down
40 changes: 37 additions & 3 deletions src/krb5/_creds.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright: (c) 2021 Jordan Borean (@jborean93) <jborean93@gmail.com>
# MIT License (see LICENSE or https://opensource.org/licenses/MIT)

import enum
import typing

from krb5._ccache import CCache
Expand All @@ -16,6 +17,36 @@ class TicketTimes(typing.NamedTuple):
endtime: int
renew_till: int

class TicketFlags(enum.IntFlag):
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Would be great to add a docstring about this enum that links to the RFC where they are defined.

"""Kerberos ticket flags.

See <https://www.rfc-editor.org/rfc/rfc4120.html#page-68> for the definition
of the flags.

The enc_pa_rep flag is defined by
<https://www.rfc-editor.org/rfc/rfc6806.html#section-11>.

The anonymous flag is defined by
<https://www.rfc-editor.org/rfc/rfc6112.html#section-3>.
"""

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.

Expand All @@ -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."""
Expand Down
68 changes: 56 additions & 12 deletions src/krb5/_creds.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Can you share an actual scenario where this is required? When you say will contain only known flags do you mean that unsetting the bit will clear out any values that aren't defined here or something else?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Can you share an actual scenario where this is required?

This would be if the KDC sets some flag which is not known to the pykrb5 library and the application wants to see that flag.

When you say will contain only known flags do you mean that unsetting the bit will clear out any values that aren't defined here

Yes.

import enum

class MyFlags(enum.IntFlag):
    A = 1
    B = 2

print(int(MyFlags(15)))  # Will print 15
print(int(MyFlags(15) & ~MyFlags.B))  # On python >= 3.11, will print 1, before 3.11 will print 13
print(int(~MyFlags.B))  # On python >= 3.11, will print 1, before then will print -3

That means without this on Python >= 3.11, a & ~TicketFlags.initial will remove not only the initial flag, but also all flags which are not defined in the TicketFlags enum.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

That's definitely surprising behaviour but if that's out of our control here :) Thanks for sharing the details.



cdef class Creds:
# cdef Context ctx
# cdef krb5_creds raw
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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, &times, NULL, NULL, NULL)
pykrb5_creds_get(&self.raw, NULL, NULL, NULL, &times, 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
Expand All @@ -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
Expand Down
48 changes: 46 additions & 2 deletions tests/test_creds.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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""
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand All @@ -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)
Expand All @@ -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")
Expand Down