2 * Copyright (c) 2019 Kungliga Tekniska Högskolan
3 * (Royal Institute of Technology, Stockholm, Sweden).
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in the
15 * documentation and/or other materials provided with the distribution.
17 * 3. Neither the name of the Institute nor the names of its contributors
18 * may be used to endorse or promote products derived from this software
19 * without specific prior written permission.
21 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
22 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
25 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
27 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
30 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
35 * This plugin authorizes requested certificate SANs and EKUs by calling a
36 * service over IPC (Unix domain sockets on Linux/BSD/Illumos).
38 * The IPC protocol is request/response, with requests and responses sent as
42 * where the <length> is 4 bytes, unsigned binary in network byte order, and
43 * <string> is an array of <length> bytes and does NOT include a NUL
46 * Requests are of the form:
48 * check <princ> <exttype>=<extvalue> ...
50 * where <princ> is a URL-escaped principal name, <exttype> is one of:
59 * and <extvalue> is a URL-escaped string representation of the SAN or OID.
61 * OIDs are in the form 1.2.3.4.5.6.
63 * Only characters other than alphanumeric, '@', '.', '-', '_', and '/' are
66 * Responses are any of:
74 * C->S: check jane@TEST.H5L.SE san_dnsname=jane.foo.test.h5l.se eku=1.3.6.1.5.5.7.3.1
77 * Only digitalSignature and nonRepudiation key usages are allowed. Requested
78 * key usages are not sent to the CSR authorizer IPC server.
83 #include <sys/types.h>
97 #include <common_plugin.h>
98 #include <csr_authorizer_plugin.h>
101 * string_encode_sz() and string_encode() encode principal names and such to be
102 * safe for use in our IPC text messages. They function very much like URL
103 * encoders, but '~' also gets encoded, and '.' and '@' do not.
105 * An unescaper is not needed here.
108 string_encode_sz(const char *in
)
110 size_t sz
= strlen(in
);
123 if (isalnum((unsigned char)c
))
132 string_encode(const char *in
)
134 size_t len
= strlen(in
);
135 size_t sz
= string_encode_sz(in
);
139 if ((s
= malloc(sz
+ 1)) == NULL
)
143 for (i
= k
= 0; i
< len
; i
++) {
144 unsigned char c
= ((const unsigned char *)in
)[i
];
159 s
[k
++] = "0123456789abcdef"[(c
&0xff)>>4];
160 s
[k
++] = "0123456789abcdef"[(c
&0x0f)];
168 cmd_append(struct rk_strpool
**cmd
, const char *s0
, ...)
174 if ((*cmd
= rk_strpoolprintf(*cmd
, "%s", s0
)) == NULL
)
178 while ((arg
= va_arg(ap
, const char *))) {
181 if ((s
= string_encode(arg
)) == NULL
) {
182 rk_strpoolfree(*cmd
);
187 *cmd
= rk_strpoolprintf(*cmd
, "%s", s
);
200 /* Like strpbrk(), but from the end of the string */
202 strrpbrk(char *s
, const char *accept
)
208 p
= strpbrk(p
, accept
);
218 * For /get-tgts we need to support partial authorization of requests. The
219 * hx509_request APIs support that.
221 * Here we just step through the IPC server's response and mark the
222 * corresponding request elements authorized so that /get-tgts can issue or not
223 * issue TGTs according to which requested principals are authorized and which
227 mark_piecemeal_authorized(krb5_context context
,
229 heim_octet_string
*rep
)
233 char *s
, *p
, *rep2
, *tok
, *next
= NULL
;
238 /* We have a data, but we want a C string */
239 if ((rep2
= strndup(rep
->data
, rep
->length
)) == NULL
)
240 return krb5_enomem(context
);
242 /* The first token should be "denied"; skip it */
243 if ((s
= strchr(rep2
, ' ')) == NULL
) {
249 while ((tok
= strtok_r(s
, ",", &next
))) {
250 hx509_san_type san_type
, san_type2
;
253 s
= NULL
; /* for strtok_r() */
255 if (strncmp(tok
, "eku=", sizeof("eku=") -1) == 0) {
257 * Very simplistic handling of partial authz for EKUs:
259 * - denial of an EKU -> deny the whole request
260 * - else below mark all EKUs approved
262 if (strstr(tok
, ":denied")) {
263 krb5_set_error_message(context
, EACCES
, "CSR denied because "
264 "EKU denied: %s", tok
);
272 * For SANs we check that the nth SAN in the response matches the nth
273 * SAN in the hx509_request.
276 if (strncmp(tok
, "san_pkinit=", sizeof("san_pkinit=") - 1) == 0) {
277 tok
+= sizeof("san_pkinit=") - 1;
278 san_type
= HX509_SAN_TYPE_PKINIT
;
279 } else if (strncmp(tok
, "san_dnsname=", sizeof("san_dnsname=") -1) == 0) {
280 tok
+= sizeof("san_dnsname=") - 1;
281 san_type
= HX509_SAN_TYPE_DNSNAME
;
282 } else if (strncmp(tok
, "san_email=", sizeof("san_email=") -1) == 0) {
283 tok
+= sizeof("san_email=") - 1;
284 san_type
= HX509_SAN_TYPE_EMAIL
;
285 } else if (strncmp(tok
, "san_xmpp=", sizeof("san_xmpp=") -1) == 0) {
286 tok
+= sizeof("san_xmpp=") - 1;
287 san_type
= HX509_SAN_TYPE_XMPP
;
288 } else if (strncmp(tok
, "san_ms_upn=", sizeof("san_ms_upn=") -1) == 0) {
289 tok
+= sizeof("san_ms_upn=") - 1;
290 san_type
= HX509_SAN_TYPE_MS_UPN
;
292 krb5_set_error_message(context
, EACCES
, "CSR denied because could "
293 "not parse token in response: %s", tok
);
299 * This token has to end in ":granted" or ":denied". Using our
300 * `strrpbrk()' means we can deal with principals names that have ':'
303 if ((p
= strrpbrk(tok
, ":")) == NULL
) {
309 /* Now we get the nth SAN from the authorization */
310 ret
= hx509_request_get_san(csr
, san_idx
, &san_type2
, &s2
);
311 if (ret
== HX509_NO_ITEM
) {
317 /* And we check that it matches the SAN in this token */
319 if (san_type
!= san_type2
||
320 strcmp(tok
, s2
) != 0) {
322 * We expect the tokens in the reply to be in the same order as
323 * in the request. If not, we must take a slow path where we
324 * have to sort requests and responses then iterate them in
333 if (strcmp(p
, "granted") == 0) {
334 ret
= hx509_request_authorize_san(csr
, san_idx
);
337 ret
= hx509_request_reject_san(csr
, san_idx
);
347 * FIXME? Implement the slow path?
349 * Basically, we'd get all the SANs from the request into an array of
350 * {SAN, index} and sort that array, then all the SANs from the
351 * response into an array and sort it, then step a cursor through both,
352 * using the index from the first to mark SANs in the request
353 * authorized or rejected.
355 krb5_set_error_message(context
, EACCES
, "CSR denied because "
356 "authorizer service did not include all "
357 "piecemeal grants/denials in order");
361 /* Mark all the EKUs authorized */
362 for (eku_idx
= 0; ret
== 0; eku_idx
++)
363 ret
= hx509_request_authorize_eku(csr
, eku_idx
);
364 if (ret
== HX509_NO_ITEM
)
366 if (ret
== 0 && partial
) {
367 krb5_set_error_message(context
, EACCES
, "CSR partially authorized");
375 static krb5_error_code
mark_authorized(hx509_request
);
378 call_svc(krb5_context context
,
382 int piecemeal_check_ok
)
384 heim_octet_string req
, resp
;
387 req
.data
= (void *)(uintptr_t)cmd
;
388 req
.length
= strlen(cmd
);
391 ret
= heim_ipc_call(ipc
, &req
, &resp
, NULL
);
393 /* Check for all granted case */
395 resp
.length
== sizeof("granted") - 1 &&
396 strncasecmp(resp
.data
, "granted", sizeof("granted") - 1) == 0) {
398 return mark_authorized(csr
); /* Full approval */
401 /* Check for "denied ..." piecemeal authorization case */
402 if ((ret
== 0 || ret
== EACCES
|| ret
== KRB5_PLUGIN_NO_HANDLE
) &&
403 piecemeal_check_ok
&&
404 resp
.length
> sizeof("denied") - 1 &&
405 strncasecmp(resp
.data
, "denied", sizeof("denied") - 1) == 0) {
406 /* Piecemeal authorization */
407 ret
= mark_piecemeal_authorized(context
, csr
, &resp
);
409 /* mark_piecemeal_authorized() should return EACCES; just in case: */
416 /* All other failure cases */
418 if (resp
.data
== NULL
|| resp
.length
== 0) {
419 krb5_set_error_message(context
, ret
, "CSR authorizer IPC service "
425 if (resp
.length
== sizeof("ignore") - 1 &&
426 strncasecmp(resp
.data
, "ignore", sizeof("ignore") - 1) == 0) {
428 * In this case the server is saying "I can't handle this request, try
429 * some other authorizer plugin".
432 return KRB5_PLUGIN_NO_HANDLE
;
435 if (resp
.length
== sizeof("denied") - 1 &&
436 strncasecmp(resp
.data
, "denied", sizeof("denied") - 1) == 0) {
437 krb5_set_error_message(context
, ret
, "CSR authorizer rejected %s",
443 if (resp
.length
> INT_MAX
)
444 krb5_set_error_message(context
, ret
, "CSR authorizer rejected %s", cmd
);
446 krb5_set_error_message(context
, ret
, "CSR authorizer rejected %s: %.*s",
447 cmd
, resp
.length
, resp
.data
);
460 static krb5_error_code
461 mark_authorized(hx509_request csr
)
467 for (i
= 0; ret
== 0; i
++) {
468 ret
= hx509_request_get_eku(csr
, i
, &s
);
470 hx509_request_authorize_eku(csr
, i
);
473 if (ret
== HX509_NO_ITEM
)
476 for (i
= 0; ret
== 0; i
++) {
477 hx509_san_type san_type
;
478 ret
= hx509_request_get_san(csr
, i
, &san_type
, &s
);
480 hx509_request_authorize_san(csr
, i
);
483 return ret
== HX509_NO_ITEM
? 0 : ret
;
486 static KRB5_LIB_CALL krb5_error_code
488 krb5_context context
,
491 krb5_const_principal client
,
492 krb5_boolean
*result
)
494 struct rk_strpool
*cmd
= NULL
;
496 hx509_context hx509ctx
= NULL
;
504 int piecemeal_check_ok
= 1;
506 if ((svc
= krb5_config_get_string_default(context
, NULL
,
507 "ANY:org.h5l.csr_authorizer",
509 "ipc_csr_authorizer", "service",
511 return KRB5_PLUGIN_NO_HANDLE
;
513 if ((ret
= heim_ipc_init_context(svc
, &ipc
))) {
515 * If the IPC authorizer is optional, then fallback on whatever is
518 if (krb5_config_get_bool_default(context
, NULL
, FALSE
,
520 "ipc_csr_authorizer", "optional",
522 return KRB5_PLUGIN_NO_HANDLE
;
523 krb5_set_error_message(context
, ret
, "Could not set up IPC client "
524 "end-point for service %s", svc
);
528 if ((ret
= hx509_context_init(&hx509ctx
)))
531 if ((ret
= krb5_unparse_name(context
, client
, &princ
)))
534 if ((ret
= cmd_append(&cmd
, "check ", princ
, NULL
)))
538 for (i
= 0; ret
== 0; i
++) {
539 hx509_san_type san_type
;
542 ret
= hx509_request_get_san(csr
, i
, &san_type
, &s
);
547 * We cannot do a piecemeal check if any of the SANs could make the
548 * response ambiguous.
550 p
= strcspn(s
, ",= ");
552 piecemeal_check_ok
= 0;
553 if (piecemeal_check_ok
&& strstr(s
, ":granted") != NULL
)
554 piecemeal_check_ok
= 0;
557 case HX509_SAN_TYPE_EMAIL
:
558 if ((ret
= cmd_append(&cmd
, " san_email=", s
, NULL
)))
562 case HX509_SAN_TYPE_DNSNAME
:
563 if ((ret
= cmd_append(&cmd
, " san_dnsname=", s
, NULL
)))
567 case HX509_SAN_TYPE_XMPP
:
568 if ((ret
= cmd_append(&cmd
, " san_xmpp=", s
, NULL
)))
572 case HX509_SAN_TYPE_PKINIT
:
573 if ((ret
= cmd_append(&cmd
, " san_pkinit=", s
, NULL
)))
577 case HX509_SAN_TYPE_MS_UPN
:
578 if ((ret
= cmd_append(&cmd
, " san_ms_upn=", s
, NULL
)))
583 if ((ret
= hx509_request_reject_san(csr
, i
)))
589 if (ret
== HX509_NO_ITEM
)
594 for (i
= 0; ret
== 0; i
++) {
595 ret
= hx509_request_get_eku(csr
, i
, &s
);
598 if ((ret
= cmd_append(&cmd
, " eku=", s
, NULL
)))
603 if (ret
== HX509_NO_ITEM
)
608 ku
= int2KeyUsage(0);
609 ku
.digitalSignature
= 1;
610 ku
.nonRepudiation
= 1;
611 hx509_request_authorize_ku(csr
, ku
);
614 s
= rk_strpoolcollect(cmd
);
618 if ((ret
= call_svc(context
, ipc
, csr
, s
, piecemeal_check_ok
)))
620 } /* else there was nothing to check -> permit */
627 ret
= krb5_enomem(context
);
631 heim_ipc_free_context(ipc
);
632 hx509_context_free(&hx509ctx
);
640 static KRB5_LIB_CALL krb5_error_code
641 ipc_csr_authorizer_init(krb5_context context
, void **c
)
647 static KRB5_LIB_CALL
void
648 ipc_csr_authorizer_fini(void *c
)
652 static krb5plugin_csr_authorizer_ftable plug_desc
=
653 { 1, ipc_csr_authorizer_init
, ipc_csr_authorizer_fini
, authorize
};
655 static krb5plugin_csr_authorizer_ftable
*plugs
[] = { &plug_desc
};
658 ipc_csr_authorizer_get_instance(const char *libname
)
660 if (strcmp(libname
, "krb5") == 0)
661 return krb5_get_instance(libname
);
662 if (strcmp(libname
, "kdc") == 0)
663 return kdc_get_instance(libname
);
664 if (strcmp(libname
, "hx509") == 0)
665 return hx509_get_instance(libname
);
669 krb5_plugin_load_ft kdc_csr_authorizer_plugin_load
;
671 krb5_error_code KRB5_CALLCONV
672 kdc_csr_authorizer_plugin_load(heim_pcontext context
,
673 krb5_get_instance_func_t
*get_instance
,
675 krb5_plugin_common_ftable_cp
**plugins
)
677 *get_instance
= ipc_csr_authorizer_get_instance
;
678 *num_plugins
= sizeof(plugs
) / sizeof(plugs
[0]);
679 *plugins
= (krb5_plugin_common_ftable_cp
*)plugs
;