Skip to content

Commit a13b693

Browse files
committed
feat: Implement RFC 9345 Delegated Credentials
Adds support for Delegated Credentials (DC) on both server and client. Includes DC class, parsing, extension handling, settings integration, and validation.
1 parent 6805bcb commit a13b693

File tree

12 files changed

+817
-195
lines changed

12 files changed

+817
-195
lines changed

scripts/tls.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from tlslite.utils.cryptomath import getRandomBytes
4141
from tlslite.constants import KeyUpdateMessageType
4242
from tlslite.utils.compression import compression_algo_impls
43+
from tlslite.utils.pem import dePem
4344

4445
try:
4546
from tack.structures.Tack import Tack
@@ -109,7 +110,7 @@ def printUsage(s=None):
109110
[-c CERT] [-k KEY] [-t TACK] [-v VERIFIERDB] [-d DIR] [-l LABEL] [-L LENGTH]
110111
[--reqcert] [--param DHFILE] [--psk PSK] [--psk-ident IDENTITY]
111112
[--psk-sha384] [--ssl3] [--max-ver VER] [--tickets COUNT] [--cipherlist]
112-
[--request-pha] [--require-pha] [--echo] [--groups GROUPS]
113+
[--request-pha] [--require-pha] [--echo] [--groups GROUPS] [--dc-key KEY]
113114
HOST:PORT
114115
115116
client
@@ -138,6 +139,8 @@ def printUsage(s=None):
138139
post-handshake authentication
139140
--echo - function as an echo server
140141
--groups - specify what key exchange groups should be supported
142+
--dc-key KEY - the private key of the delegated credential
143+
--dc-pub KEY - the public key of the delegated credential
141144
GROUPS - comma-separated list of enabled key exchange groups
142145
CERT, KEY - the file with key and certificates that will be used by client or
143146
server. The server can accept multiple pairs of `-c` and `-k` options
@@ -200,6 +203,8 @@ def handleArgs(argv, argString, flagsList=[]):
200203
require_pha = False
201204
echo = False
202205
groups = None
206+
dc_key = None
207+
dc_pub = None
203208

204209
for opt, arg in opts:
205210
if opt == "-k":
@@ -231,6 +236,20 @@ def handleArgs(argv, argString, flagsList=[]):
231236
else:
232237
v_host_cert = X509CertChain()
233238
v_host_cert.parsePemList(s)
239+
elif opt == "--dc-key":
240+
s = open(arg, "rb").read()
241+
if sys.version_info[0] >= 3:
242+
s = str(s, 'utf-8')
243+
if not cert_chain:
244+
raise ValueError("Certificate is missing (must be listed "
245+
"before the delegated credentials)")
246+
dc_key = parsePEMKey(s, private=True,
247+
implementations=["python"])
248+
elif opt == "--dc-pub":
249+
s = open(arg, "rb").read()
250+
if sys.version_info[0] >= 3:
251+
s = str(s, 'utf-8')
252+
dc_pub = dePem(s, "PUBLIC KEY")
234253
elif opt == "-u":
235254
username = arg
236255
elif opt == "-p":
@@ -351,6 +370,10 @@ def handleArgs(argv, argString, flagsList=[]):
351370
retList.append(echo)
352371
if "groups=" in flagsList:
353372
retList.append(groups)
373+
if "dc_key=" in flagsList:
374+
retList.append(dc_key)
375+
if "dc_pub=" in flagsList:
376+
retList.append(dc_pub)
354377
return retList
355378

356379

@@ -556,12 +579,12 @@ def serverCmd(argv):
556579
directory, reqCert,
557580
expLabel, expLength, dhparam, psk, psk_ident, psk_hash, ssl3,
558581
max_ver, tickets, cipherlist, request_pha, require_pha, echo,
559-
groups) = \
582+
groups, dc_key, dc_pub) = \
560583
handleArgs(argv, "kctbvdlL",
561584
["reqcert", "param=", "psk=",
562585
"psk-ident=", "psk-sha384", "ssl3", "max-ver=",
563586
"tickets=", "cipherlist=", "request-pha", "require-pha",
564-
"echo", "groups="])
587+
"echo", "groups=", "dc_key=", "dc_pub="])
565588

566589

567590
if (cert_chain and not privateKey) or (not cert_chain and privateKey):
@@ -585,6 +608,8 @@ def serverCmd(argv):
585608
print("Using Tacks...")
586609
if reqCert:
587610
print("Asking for client certificates...")
611+
if dc_key and dc_pub:
612+
print("Usage of delegated credential is available...")
588613

589614
#############
590615
sessionCache = SessionCache()
@@ -619,6 +644,8 @@ def serverCmd(argv):
619644
settings.dhGroups = dh_groups
620645
settings.eccCurves = ecc_groups
621646
settings.keyShares = []
647+
if dc_key and dc_pub:
648+
settings.delegated_credential = [SignatureScheme.ed25519]
622649

623650
class MySimpleEchoHandler(BaseRequestHandler):
624651
def handle(self):
@@ -700,7 +727,9 @@ def handshake(self, connection):
700727
nextProtos=[b"http/1.1"],
701728
alpn=[bytearray(b'http/1.1')],
702729
reqCert=reqCert,
703-
sni=sni)
730+
sni=sni,
731+
dc_key=dc_key,
732+
dc_pub=dc_pub)
704733
# As an example (does not work here):
705734
#nextProtos=[b"spdy/3", b"spdy/2", b"http/1.1"])
706735
try:
@@ -754,4 +783,3 @@ def handshake(self, connection):
754783
serverCmd(sys.argv[2:])
755784
else:
756785
printUsage("Unknown command: %s" % sys.argv[1])
757-

tests/serverX509DCKey.pem

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCT/Pf92sZZd5nP
3+
5WUs+ilhN2GxgSx1ylXyARar2MGWfYA/b0eMPsfVf+bLpQX/Da2drrq7L1HxTaId
4+
NUYsh/nvPLxROkA4AmECahFZSCkJZ6VdVgO2cMu7SNE9L7vZXIo22aL+COBqZ1xn
5+
vjMUyVXnrs02fMfcn6c/FIO0bd2YkRUiOP2iBAhCn6GmHZeJ0/OMHB7VJ0Ozpy63
6+
xsONxtIBgPEimQFlTCgmMmBjDzyquTIqVZ0ajQnHNKM4BaBcsu788sttPCh3i7EQ
7+
4sAI8Pw8tmPxs/nb2fMNgQEd+wjvIHTss82hzu9XUTDt9CAOWHyRCUi5D9a6dMS4
8+
oscRpQiPAgMBAAECggEAEy4gPiiSuJnFt6o1mMS7hDwXT1g8mO+mf/0gIRmwzX5q
9+
ls4nacfhQoyXLyGuS0ZMkDlLPmN9rVawgjSbab4d6KHojmaMWDYGuLdilD3EA9IJ
10+
HrW9OXIZFab0Z4e+Qwe5ai5+74na/C91TITcPf9yQNrpAfzeMnwGwyg3gbUTmWhR
11+
2AriFcTdWINmZKNKbOS0Fkw/Kn7zPAidjSanvthNdQPNi1k1fob0rj05Q57kEe4d
12+
lOC5j79mYV3XUWlyf6g8knmoBsQ1Y0gfFyro1R3VtAJSlU8KPJ41bhlE5kpiUZYb
13+
cWRCJYF3anRxTxo8getRLffE19en0i+z99tZIzgwoQKBgQDOgU5/2DK6ussAm9kv
14+
6IPOuwr9gEaYQAMmgG08ck2CCW7J/MGAGXURtZOMf5zSsMBvHXT/xvAWBuElilbF
15+
9/DNHz51Ts2GvyqPSzSFHxQUo3ljV6n/qIXP+7Xb3xwzjGVzXl2CfPeI0hxVZmz+
16+
W6jcvczqyOXxjH+WZFmgLnzJSQKBgQC3dTFQYSWJpXb9gVNSWLRbN6ZVgoEjMbOJ
17+
N8Pzj+3hcKYeQJUdH1dxbGdjFUrCrkHVWEaXZ7eH5B/fWd+uTBUgwnO0FExjpkAm
18+
EsakhP96vdN/jtax0S7S1Jcn6Ty6YdBAzXT9JsguxAUvVE4uSpdrdxpJF/4F6key
19+
WdoXE9JbFwKBgG52qvgmPVS3sPm9ZFuFRGSkl0dtg9XTgBvrXQOVnTJvO01fIF8W
20+
vxHfEHN6m/f0RqvplPlxgGI4Ad3j93DkpXIEQZPcuIJY5jpKn2iKbGJx4/ApJ62z
21+
hwjve6OG4H4OnwIsu1ae5IbS5gckyC7z9wtFmEULfD1Oy702Jt9RnrzJAoGAbtug
22+
SwQJHN4hwxpM8SutAJnmJzHPOycjaD2MaTeF9X6OwyUfdhOkUWPCLbuGC5IlMfg/
23+
3+nKm5EcOWkjoz1SXxNhu2Wwq16g0ODzrCK6Br+CeEgmMBlJhBj2piVoju/gWehN
24+
U1QGD0xgHbOB8rMcQNIdziFzXLuvS3TENsHBkU0CgYBg4mTavRdbMwDUN07aRpt8
25+
+EaJJnmxWRl0wD9yIvFVZa0wPRYd54/HOMj5dqHHklgn1eb+HwxdyEYw/kUMn+oC
26+
roPHTA4hYXDMqAHkXMC6sDmG5MVgP6ozWEYVbiK/5y6QsTozLciNpwA0STGHmtcC
27+
kQpskxTgLWPCkzB6RvWqzw==
28+
-----END PRIVATE KEY-----

tests/serverX509DCPub.pem

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk/z3/drGWXeZz+VlLPop
3+
YTdhsYEsdcpV8gEWq9jBln2AP29HjD7H1X/my6UF/w2tna66uy9R8U2iHTVGLIf5
4+
7zy8UTpAOAJhAmoRWUgpCWelXVYDtnDLu0jRPS+72VyKNtmi/gjgamdcZ74zFMlV
5+
567NNnzH3J+nPxSDtG3dmJEVIjj9ogQIQp+hph2XidPzjBwe1SdDs6cut8bDjcbS
6+
AYDxIpkBZUwoJjJgYw88qrkyKlWdGo0JxzSjOAWgXLLu/PLLbTwod4uxEOLACPD8
7+
PLZj8bP529nzDYEBHfsI7yB07LPNoc7vV1Ew7fQgDlh8kQlIuQ/WunTEuKLHEaUI
8+
jwIDAQAB
9+
-----END PUBLIC KEY-----

tests/tlstest.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import timeit
2222
import getopt
2323
from tempfile import mkstemp
24+
25+
from tlslite.x509 import DelegatedCredential
2426
try:
2527
from BaseHTTPServer import HTTPServer
2628
from SimpleHTTPServer import SimpleHTTPRequestHandler
@@ -45,6 +47,7 @@
4547
import ssl
4648
from tlslite import *
4749
from tlslite.constants import KeyUpdateMessageType, ECPointFormat, SignatureScheme
50+
from tlslite.utils.pem import dePem
4851

4952
try:
5053
from tack.structures.Tack import Tack
@@ -1910,6 +1913,21 @@ def heartbeat_response_check(message):
19101913

19111914
test_no += 1
19121915

1916+
print("Test {0} - Delegated Credential test".format(test_no))
1917+
synchro.recv(1)
1918+
connection = connect()
1919+
settings = HandshakeSettings()
1920+
settings.maxVersion = (3, 4)
1921+
settings.dc_sig_algs = [SignatureScheme.rsa_pkcs1_sha256]
1922+
connection.handshakeClientCert(settings=settings)
1923+
assert connection.session.delegated_credential is not None
1924+
assert isinstance(connection.session.delegated_credential,
1925+
DelegatedCredential)
1926+
testConnClient(connection)
1927+
connection.close()
1928+
1929+
test_no += 1
1930+
19131931
print('Test {0} - good standard XMLRPC https client'.format(test_no))
19141932
address = address[0], address[1]+1
19151933
synchro.recv(1)
@@ -1967,6 +1985,8 @@ def heartbeat_response_check(message):
19671985
print("Non-critical error: socket error trying to reach internet "
19681986
"server: ", e)
19691987

1988+
1989+
19701990
synchro.close()
19711991

19721992
if not badFault:
@@ -2119,6 +2139,13 @@ def connect():
21192139
with open(os.path.join(dir, "serverEd448Key.pem")) as f:
21202140
x509Ed448Key = parsePEMKey(f.read(), private=True,
21212141
implementations=["python"])
2142+
with open(os.path.join(dir, "serverX509DCKey.pem")) as f:
2143+
s = f.read()
2144+
x509DelegatedCredPrivKey = parsePEMKey(s, private=True,
2145+
implementations=["python"])
2146+
with open(os.path.join(dir, "serverX509DCPub.pem")) as f:
2147+
s = f.read()
2148+
x509DelegatedCredPubKey = dePem(s, "PUBLIC KEY")
21222149

21232150
test_no = 0
21242151

@@ -3655,6 +3682,25 @@ def heartbeat_response_check(message):
36553682

36563683
test_no +=1
36573684

3685+
print("Test {0} - Delegated Credential test".format(test_no))
3686+
synchro.send(b'R')
3687+
connection = connect()
3688+
settings = HandshakeSettings()
3689+
settings.maxVersion = (3, 4)
3690+
settings.dc_sig_algs = [SignatureScheme.rsa_pkcs1_sha256]
3691+
connection.handshakeServer(certChain=x509Chain,
3692+
privateKey=x509Key,
3693+
dc_key=x509DelegatedCredPrivKey,
3694+
dc_pub=x509DelegatedCredPubKey,
3695+
settings=settings)
3696+
assert connection.session.delegated_credential is not None
3697+
assert isinstance(connection.session.delegated_credential,
3698+
DelegatedCredential)
3699+
testConnServer(connection)
3700+
connection.close()
3701+
3702+
test_no += 1
3703+
36583704
print("Tests {0}-{1} - XMLRPXC server".format(test_no, test_no + 2))
36593705

36603706
address = address[0], address[1]+1

tlslite/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ class ExtensionType(TLSEnum):
181181
post_handshake_auth = 49 # TLS 1.3
182182
signature_algorithms_cert = 50 # TLS 1.3
183183
key_share = 51 # TLS 1.3
184+
delegated_credential = 34 # TLS 1.3, RFC 9345
184185
supports_npn = 13172
185186
tack = 0xF300
186187
renegotiation_info = 0xff01 # RFC 5746

tlslite/extensions.py

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
PskKeyExchangeMode, CertificateType, GroupName, ECPointFormat, \
1515
HeartbeatMode, CertificateCompressionAlgorithm
1616
from .errors import TLSInternalError
17+
from .x509 import DelegatedCredential
1718

1819

1920
class TLSExtension(object):
@@ -1350,6 +1351,48 @@ def parse(self, p):
13501351
return self
13511352

13521353

1354+
class DelegatedCredentialCertExtension(TLSExtension):
1355+
"""
1356+
Extension of CertificateEntry consisting delegated credential.
1357+
1358+
See RFC9345.
1359+
1360+
Creates, parses and returns the Delegated Credential class.
1361+
"""
1362+
def __init__(self):
1363+
"""Create instance of DelegatedCredentialCertExtension."""
1364+
super(DelegatedCredentialCertExtension, self).__init__(
1365+
extType=ExtensionType.delegated_credential)
1366+
self.delegated_credential = None
1367+
1368+
def create(self, delegated_credential):
1369+
"""Set values of the extension."""
1370+
self.delegated_credential = delegated_credential
1371+
return self
1372+
1373+
def parse(self, p):
1374+
"""Deserialise the data from on the wire representation."""
1375+
self.delegated_credential = DelegatedCredential().parse(p)
1376+
return self
1377+
1378+
@property
1379+
def extData(self):
1380+
"""Serialise the object."""
1381+
writer = Writer()
1382+
writer.addFour(
1383+
self.delegated_credential.cred.valid_time)
1384+
writer.addOne(
1385+
self.delegated_credential.cred.dc_cert_verify_algorithm[0])
1386+
writer.addOne(
1387+
self.delegated_credential.cred.dc_cert_verify_algorithm[1])
1388+
writer.add_var_bytes(
1389+
self.delegated_credential.cred.subject_public_key_info, 3)
1390+
writer.addOne(self.delegated_credential.algorithm[0])
1391+
writer.addOne(self.delegated_credential.algorithm[1])
1392+
writer.add_var_bytes(self.delegated_credential.signature, 2)
1393+
return writer.bytes
1394+
1395+
13531396
class SupportedGroupsExtension(VarListExtension):
13541397
"""
13551398
Client side list of supported groups of (EC)DHE key exchage.
@@ -1370,7 +1413,7 @@ def __init__(self):
13701413

13711414
class ECPointFormatsExtension(VarListExtension):
13721415
"""
1373-
Client side list of supported ECC point formats.
1416+
List of supported ECC point formats.
13741417
13751418
See RFC4492.
13761419
@@ -1412,6 +1455,27 @@ def _list_to_repr(self):
14121455
return "[{0}]".format(", ".join(values))
14131456

14141457

1458+
class DelegatedCredentialExtension(_SigListExt):
1459+
"""
1460+
Client side list of supported signature algorithms
1461+
for use of delegated credentials.
1462+
SignatureSchemeList.
1463+
1464+
See RFC8446 / RFC9345.
1465+
1466+
:vartype sigalgs: list of tuples
1467+
:ivar sigalgs: list of signature schemes supported by peer
1468+
"""
1469+
1470+
def __init__(self):
1471+
"""Create instance of class"""
1472+
super(DelegatedCredentialExtension, self).__init__(
1473+
1, 2, 2,
1474+
'sigalgs',
1475+
ExtensionType.delegated_credential,
1476+
SignatureScheme)
1477+
1478+
14151479
class SignatureAlgorithmsExtension(_SigListExt):
14161480
"""
14171481
Client side list of supported signature algorithms.
@@ -2190,7 +2254,8 @@ def __init__(self):
21902254
ExtensionType.cookie: CookieExtension,
21912255
ExtensionType.record_size_limit: RecordSizeLimitExtension,
21922256
ExtensionType.session_ticket: SessionTicketExtension,
2193-
ExtensionType.compress_certificate: CompressedCertificateExtension
2257+
ExtensionType.compress_certificate: CompressedCertificateExtension,
2258+
ExtensionType.delegated_credential: DelegatedCredentialExtension
21942259
}
21952260

21962261
TLSExtension._serverExtensions = {
@@ -2202,7 +2267,9 @@ def __init__(self):
22022267
}
22032268

22042269
TLSExtension._certificateExtensions = {
2205-
ExtensionType.status_request: CertificateStatusExtension
2270+
ExtensionType.status_request: CertificateStatusExtension,
2271+
ExtensionType.delegated_credential: DelegatedCredentialCertExtension
2272+
22062273
}
22072274

22082275
TLSExtension._hrrExtensions = {

0 commit comments

Comments
 (0)