Skip to content

Commit 3e7b228

Browse files
fox0430arnetheduck
andauthored
Add client certificate authentication support to TLSStream (#631)
Add support for mTLS client certificate authentication in `newTLSClientAsyncStream`. Both RSA and EC key types are supported. * Add EC test --------- Co-authored-by: Jacek Sieka <jacek@status.im>
1 parent 211a22d commit 3e7b228

3 files changed

Lines changed: 229 additions & 2 deletions

File tree

chronos/streams/tlsstream.nim

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ type
9999
writer*: TLSStreamWriter
100100
mainLoop*: Future[void].Raising([])
101101
trustAnchors: TrustAnchorStore
102+
clientCertificate: TLSCertificate
103+
clientPrivateKey: TLSPrivateKey
102104

103105
SomeTLSStreamType* = TLSStreamReader|TLSStreamWriter|TLSAsyncStream
104106
SomeTrustAnchorType* = TrustAnchorStore | openArray[X509TrustAnchor]
@@ -466,7 +468,9 @@ proc newTLSClientAsyncStream*(
466468
minVersion = TLSVersion.TLS12,
467469
maxVersion = TLSVersion.TLS12,
468470
flags: set[TLSFlags] = {},
469-
trustAnchors: SomeTrustAnchorType = MozillaTrustAnchors
471+
trustAnchors: SomeTrustAnchorType = MozillaTrustAnchors,
472+
certificate: TLSCertificate = nil,
473+
privateKey: TLSPrivateKey = nil
470474
): TLSAsyncStream {.raises: [TLSStreamInitError].} =
471475
## Create new TLS asynchronous stream for outbound (client) connections
472476
## using reading stream ``rsource`` and writing stream ``wsource``.
@@ -489,6 +493,10 @@ proc newTLSClientAsyncStream*(
489493
## anchors other than the default Mozilla trust anchors. If you pass
490494
## a ``TrustAnchorStore`` you should reuse the same instance for
491495
## every call to avoid making a copy of the trust anchors per call.
496+
##
497+
## ``certificate`` and ``privateKey`` - if both are provided, the client
498+
## will use them for client certificate authentication when the server
499+
## requests it.
492500
when trustAnchors is TrustAnchorStore:
493501
doAssert(len(trustAnchors.anchors) > 0,
494502
"Empty trust anchor list is invalid")
@@ -532,6 +540,36 @@ proc newTLSClientAsyncStream*(
532540
sslEngineSetVersions(res.ccontext.eng, uint16(minVersion),
533541
uint16(maxVersion))
534542

543+
if not isNil(certificate) xor not isNil(privateKey):
544+
raise newException(TLSStreamInitError,
545+
"Both certificate and privateKey must be provided for client " &
546+
"certificate authentication")
547+
548+
if not isNil(certificate) and not isNil(privateKey):
549+
if len(certificate.certs) == 0:
550+
raise newException(TLSStreamInitError, "Incorrect client certificate")
551+
res.clientCertificate = certificate
552+
res.clientPrivateKey = privateKey
553+
let algo = getSignerAlgo(res.clientCertificate.certs[0])
554+
if algo == -1:
555+
raise newException(TLSStreamInitError,
556+
"Could not decode client certificate")
557+
case privateKey.kind
558+
of TLSKeyType.RSA:
559+
sslClientSetSingleRsa(res.ccontext,
560+
addr res.clientCertificate.certs[0],
561+
len(res.clientCertificate.certs),
562+
addr res.clientPrivateKey.rsakey,
563+
rsaPkcs1SignGetDefault())
564+
of TLSKeyType.EC:
565+
sslClientSetSingleEc(res.ccontext,
566+
addr res.clientCertificate.certs[0],
567+
len(res.clientCertificate.certs),
568+
addr res.clientPrivateKey.eckey,
569+
cuint(KEYTYPE_SIGN or KEYTYPE_KEYX),
570+
cuint(algo), ecGetDefault(),
571+
ecdsaSignAsn1GetDefault())
572+
535573
if TLSFlags.NoVerifyServerName in flags:
536574
let err = sslClientReset(res.ccontext, nil, 0)
537575
if err == 0:

tests/testasyncstream.c

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,41 @@ const br_x509_trust_anchor SelfSignedTAs[1] = {
6161
}
6262
}
6363
};
64+
65+
// EC (P-256) trust anchor for SelfSignedEcCert
66+
// Generated with: brssl ta ec.cert.pem
67+
68+
static const unsigned char TA1_DN[] = {
69+
0x30, 0x5F, 0x31, 0x0B, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13,
70+
0x02, 0x41, 0x55, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08,
71+
0x0C, 0x0A, 0x53, 0x6F, 0x6D, 0x65, 0x2D, 0x53, 0x74, 0x61, 0x74, 0x65,
72+
0x31, 0x21, 0x30, 0x1F, 0x06, 0x03, 0x55, 0x04, 0x0A, 0x0C, 0x18, 0x49,
73+
0x6E, 0x74, 0x65, 0x72, 0x6E, 0x65, 0x74, 0x20, 0x57, 0x69, 0x64, 0x67,
74+
0x69, 0x74, 0x73, 0x20, 0x50, 0x74, 0x79, 0x20, 0x4C, 0x74, 0x64, 0x31,
75+
0x18, 0x30, 0x16, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, 0x0F, 0x31, 0x32,
76+
0x37, 0x2E, 0x30, 0x2E, 0x30, 0x2E, 0x31, 0x3A, 0x34, 0x33, 0x38, 0x30,
77+
0x38
78+
};
79+
80+
static const unsigned char TA1_EC_Q[] = {
81+
0x04, 0x6A, 0x8A, 0xB6, 0xDB, 0xD1, 0xF1, 0x76, 0x07, 0xB8, 0x4B, 0xB5,
82+
0x75, 0x0D, 0x58, 0x58, 0x44, 0xE3, 0xD3, 0xAD, 0x01, 0xBF, 0x57, 0x42,
83+
0xB0, 0xCE, 0x6E, 0x80, 0x4D, 0xBA, 0x67, 0xF3, 0xB0, 0x4F, 0xE4, 0xC6,
84+
0x06, 0x90, 0xDF, 0xF4, 0xFB, 0x11, 0x00, 0x1A, 0x69, 0x73, 0x0E, 0x54,
85+
0xB4, 0x45, 0xB4, 0xE2, 0x9D, 0x2C, 0x13, 0x84, 0xB3, 0x1E, 0x75, 0xC1,
86+
0x92, 0x59, 0x10, 0x80, 0x94
87+
};
88+
89+
const br_x509_trust_anchor SelfSignedEcTAs[1] = {
90+
{
91+
{ (unsigned char *)TA1_DN, sizeof TA1_DN },
92+
BR_X509_TA_CA,
93+
{
94+
BR_KEYTYPE_EC,
95+
{ .ec = {
96+
BR_EC_secp256r1,
97+
(unsigned char *)TA1_EC_Q, sizeof TA1_EC_Q,
98+
} }
99+
}
100+
}
101+
};

tests/testasyncstream.nim

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# Apache License, version 2.0, (LICENSE-APACHEv2)
77
# MIT license (LICENSE-MIT)
88
import unittest2
9-
import bearssl/[x509]
9+
import bearssl/[ssl, x509]
1010
import stew/byteutils
1111
import ".."/chronos/unittest2/asynctests
1212
import ".."/chronos/streams/[tlsstream, chunkstream, boundstream]
@@ -16,6 +16,11 @@ import ".."/chronos/streams/[tlsstream, chunkstream, boundstream]
1616
# To create self-signed certificate and key you can use openssl
1717
# openssl req -new -x509 -sha256 -newkey rsa:2048 -nodes \
1818
# -keyout example-com.key.pem -days 3650 -out example-com.cert.pem
19+
#
20+
# To create EC (P-256) self-signed certificate and key:
21+
# openssl ecparam -genkey -name prime256v1 | \
22+
# openssl pkcs8 -topk8 -nocrypt -outform PEM > ec.key.pem
23+
# openssl req -new -x509 -sha256 -key ec.key.pem -days 3650 -out ec.cert.pem
1924

2025
const SelfSignedRsaKey = """
2126
-----BEGIN PRIVATE KEY-----
@@ -74,7 +79,35 @@ N8r5CwGcIX/XPC3lKazzbZ8baA==
7479
-----END CERTIFICATE-----
7580
"""
7681

82+
# This SSL EC certificate will expire 31 March 2036.
83+
const SelfSignedEcKey = """
84+
-----BEGIN PRIVATE KEY-----
85+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgC9VcD8axNnS+wzKg
86+
Ng62Lb3LmOiLcUHt7yOS7pY/isyhRANCAARqirbb0fF2B7hLtXUNWFhE49OtAb9X
87+
QrDOboBNumfzsE/kxgaQ3/T7EQAaaXMOVLRFtOKdLBOEsx51wZJZEICU
88+
-----END PRIVATE KEY-----
89+
"""
90+
91+
const SelfSignedEcCert = """
92+
-----BEGIN CERTIFICATE-----
93+
MIICEzCCAbmgAwIBAgIUHYNpklhNSMzLR3zBV4jDbwCX/NgwCgYIKoZIzj0EAwIw
94+
XzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu
95+
dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEYMBYGA1UEAwwPMTI3LjAuMC4xOjQzODA4
96+
MB4XDTI2MDQwMzA0NTc1MVoXDTM2MDMzMTA0NTc1MVowXzELMAkGA1UEBhMCQVUx
97+
EzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMg
98+
UHR5IEx0ZDEYMBYGA1UEAwwPMTI3LjAuMC4xOjQzODA4MFkwEwYHKoZIzj0CAQYI
99+
KoZIzj0DAQcDQgAEaoq229Hxdge4S7V1DVhYROPTrQG/V0Kwzm6ATbpn87BP5MYG
100+
kN/0+xEAGmlzDlS0RbTinSwThLMedcGSWRCAlKNTMFEwHQYDVR0OBBYEFOy8CFwI
101+
iU3eg8xtAMrHJFmX59bcMB8GA1UdIwQYMBaAFOy8CFwIiU3eg8xtAMrHJFmX59bc
102+
MA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhAPJKbWOf7MWgfCky
103+
vf85DkBfIBMo2PM8WrgJYYSRkMmqAiBOcPebl59ZX11s4OhNa9BETOuCIE6kVtNm
104+
GaLrNaHO6A==
105+
-----END CERTIFICATE-----
106+
"""
107+
77108
let SelfSignedTrustAnchors {.importc: "SelfSignedTAs".}: array[1, X509TrustAnchor]
109+
let SelfSignedEcTrustAnchors {.importc: "SelfSignedEcTAs".}:
110+
array[1, X509TrustAnchor]
78111
{.compile: "testasyncstream.c".}
79112

80113
proc createBigMessage(message: string, size: int): seq[byte] =
@@ -944,6 +977,124 @@ suite "AsyncStream/TLSStream":
944977
let res = waitFor checkTrustAnchors("Some message")
945978
check res == "Some message\r\n"
946979

980+
test "Client certificate authentication":
981+
proc checkClientCert(testMessage: string): Future[string] {.async.} =
982+
var key = TLSPrivateKey.init(SelfSignedRsaKey)
983+
var cert = TLSCertificate.init(SelfSignedRsaCert)
984+
985+
proc serveClient(server: StreamServer,
986+
transp: StreamTransport) {.async: (raises: []).} =
987+
try:
988+
var reader = newAsyncStreamReader(transp)
989+
var writer = newAsyncStreamWriter(transp)
990+
var sstream = newTLSServerAsyncStream(reader, writer, key, cert,
991+
flags = {TLSFlags.NoRenegotiation})
992+
# Configure client certificate authentication via BearSSL API.
993+
# serverX509 must be stored on TLSAsyncStream to ensure it
994+
# outlives the TLS session (BearSSL holds a pointer to it).
995+
x509MinimalInitFull(sstream.x509,
996+
unsafeAddr SelfSignedTrustAnchors[0],
997+
uint(len(SelfSignedTrustAnchors)))
998+
sslEngineSetDefaultRsavrfy(sstream.scontext.eng)
999+
sslEngineSetDefaultEcdsa(sstream.scontext.eng)
1000+
sslServerSetTrustAnchorNamesAlt(sstream.scontext,
1001+
unsafeAddr SelfSignedTrustAnchors[0],
1002+
uint(len(SelfSignedTrustAnchors)))
1003+
sslEngineSetX509(sstream.scontext.eng,
1004+
X509ClassPointerConst(addr sstream.x509.vtable))
1005+
discard sslServerReset(sstream.scontext)
1006+
await handshake(sstream)
1007+
await sstream.writer.write(testMessage & "\r\n")
1008+
await sstream.writer.finish()
1009+
await sstream.writer.closeWait()
1010+
await sstream.reader.closeWait()
1011+
await reader.closeWait()
1012+
await writer.closeWait()
1013+
await transp.closeWait()
1014+
server.stop()
1015+
server.close()
1016+
except CatchableError as exc:
1017+
raiseAssert exc.msg
1018+
1019+
var server = createStreamServer(initTAddress("127.0.0.1:0"),
1020+
serveClient, {ReuseAddr})
1021+
server.start()
1022+
var conn = await connect(server.localAddress())
1023+
var creader = newAsyncStreamReader(conn)
1024+
var cwriter = newAsyncStreamWriter(conn)
1025+
let flags = {NoVerifyHost, NoVerifyServerName}
1026+
var cstream = newTLSClientAsyncStream(creader, cwriter, "",
1027+
flags = flags, certificate = cert, privateKey = key)
1028+
let res = await cstream.reader.read()
1029+
await cstream.reader.closeWait()
1030+
await cstream.writer.closeWait()
1031+
await creader.closeWait()
1032+
await cwriter.closeWait()
1033+
await conn.closeWait()
1034+
await server.join()
1035+
return string.fromBytes(res)
1036+
let res = waitFor checkClientCert("Client cert test")
1037+
check res == "Client cert test\r\n"
1038+
1039+
test "Client certificate authentication (EC)":
1040+
proc checkClientCertEc(testMessage: string): Future[string] {.async.} =
1041+
var key = TLSPrivateKey.init(SelfSignedEcKey)
1042+
var cert = TLSCertificate.init(SelfSignedEcCert)
1043+
1044+
proc serveClient(server: StreamServer,
1045+
transp: StreamTransport) {.async: (raises: []).} =
1046+
try:
1047+
var reader = newAsyncStreamReader(transp)
1048+
var writer = newAsyncStreamWriter(transp)
1049+
var sstream = newTLSServerAsyncStream(reader, writer, key, cert,
1050+
flags = {TLSFlags.NoRenegotiation})
1051+
# Configure client certificate authentication via BearSSL API.
1052+
# serverX509 must be stored on TLSAsyncStream to ensure it
1053+
# outlives the TLS session (BearSSL holds a pointer to it).
1054+
x509MinimalInitFull(sstream.x509,
1055+
unsafeAddr SelfSignedEcTrustAnchors[0],
1056+
uint(len(SelfSignedEcTrustAnchors)))
1057+
sslEngineSetDefaultRsavrfy(sstream.scontext.eng)
1058+
sslEngineSetDefaultEcdsa(sstream.scontext.eng)
1059+
sslServerSetTrustAnchorNamesAlt(sstream.scontext,
1060+
unsafeAddr SelfSignedEcTrustAnchors[0],
1061+
uint(len(SelfSignedEcTrustAnchors)))
1062+
sslEngineSetX509(sstream.scontext.eng,
1063+
X509ClassPointerConst(addr sstream.x509.vtable))
1064+
discard sslServerReset(sstream.scontext)
1065+
await handshake(sstream)
1066+
await sstream.writer.write(testMessage & "\r\n")
1067+
await sstream.writer.finish()
1068+
await sstream.writer.closeWait()
1069+
await sstream.reader.closeWait()
1070+
await reader.closeWait()
1071+
await writer.closeWait()
1072+
await transp.closeWait()
1073+
server.stop()
1074+
server.close()
1075+
except CatchableError as exc:
1076+
raiseAssert exc.msg
1077+
1078+
var server = createStreamServer(initTAddress("127.0.0.1:0"),
1079+
serveClient, {ReuseAddr})
1080+
server.start()
1081+
var conn = await connect(server.localAddress())
1082+
var creader = newAsyncStreamReader(conn)
1083+
var cwriter = newAsyncStreamWriter(conn)
1084+
let flags = {NoVerifyHost, NoVerifyServerName}
1085+
var cstream = newTLSClientAsyncStream(creader, cwriter, "",
1086+
flags = flags, certificate = cert, privateKey = key)
1087+
let res = await cstream.reader.read()
1088+
await cstream.reader.closeWait()
1089+
await cstream.writer.closeWait()
1090+
await creader.closeWait()
1091+
await cwriter.closeWait()
1092+
await conn.closeWait()
1093+
await server.join()
1094+
return string.fromBytes(res)
1095+
let res = waitFor checkClientCertEc("EC client cert test")
1096+
check res == "EC client cert test\r\n"
1097+
9471098
suite "AsyncStream/BoundedStream":
9481099
teardown:
9491100
checkLeaks()

0 commit comments

Comments
 (0)