On Mon, Sep 19, 2022 at 09:54:56PM +1000, Jonathan Matthew wrote: > This adds client certificate authentication to ypldap(8). libtls makes the > actual certificate part of this straightforward (I would still like it > reviewed, though), but there are some LDAP complications. > > Depending on your LDAP server and how you connect to it (LDAPS on port 636 > or LDAP+TLS on port 389), a client presenting a certificate might > automatically be bound as the subject of the certificate, or it might not. > If it's not, the client can do an LDAP bind operation using the SASL > EXTERNAL mechanism to bind as the cert subject, and it can optionally specify > an identity, which means the bind will fail if the cert subject doesn't match > that identity. If the client didn't present a certificate, the bind will > also fail (one would hope). > > For reference, with Active Directory, SASL EXTERNAL bind is required when > using LDAP+TLS, but not when using LDAPS, and the client identity can be > specified in the form of "dn:" followed by the expected cert subject DN. > OpenLDAP doesn't seem to do automatic bind at all, so SASL EXTERNAL would > always be required there, and it doesn't appear to support specifying the > expected identity with the bind. > > The diff adds 'certfile' and 'keyfile' config directives for specifying > the certificate to use, and a 'bindext' directive for enabling SASL EXTERNAL > bind, optionally including the identity string. SASL EXTERNAL bind doesn't > get enabled implicitly when you configure a client cert, because ypldap > can't tell if it's required or supported by the server. It's also not an > error to enable SASL EXTERNAL bind without a client cert, since you could be > connecting through stunnel or something. > > To configure this in ypldap.conf, you'd do something like this: > > directory "ldap.example.com" tls { > bindext "dn:CN=ypldap,OU,Accounts,DC=example,DC=com" > certfile "/etc/ssl/ypldap-cert.pem" > keyfile "/etc/ssl/private/ypldap-key.pem" > > ... > } > > ok?
The libtls parts of this are of course fine. I do not have a way to test this, but your explanations and the code make sense. ok tb Two minor comments below. > > Index: aldap.c > =================================================================== > RCS file: /cvs/src/usr.sbin/ypldap/aldap.c,v > retrieving revision 1.48 > diff -u -p -r1.48 aldap.c > --- aldap.c 31 Mar 2022 09:06:55 -0000 1.48 > +++ aldap.c 19 Sep 2022 11:47:13 -0000 > @@ -220,6 +220,40 @@ fail: > } > > int > +aldap_bind_sasl_external(struct aldap *ldap, char *bindid) > +{ > + struct ber_element *root = NULL, *elm; > + > + if ((root = ober_add_sequence(NULL)) == NULL) > + goto fail; > + > + elm = ober_printf_elements(root, "d{tds{ts", ++ldap->msgid, > + BER_CLASS_APP, LDAP_REQ_BIND, VERSION, "", > + BER_CLASS_CONTEXT, LDAP_AUTH_SASL, LDAP_SASL_MECH_EXTERNAL); I think a NULL check for elm would be appropriate: while it's unlikely, the subsequent ober_add_*() calls might hide an error in ober_printf_elements(). > + if (bindid == NULL) > + elm = ober_add_null(elm); > + else > + elm = ober_add_string(elm, bindid); > + > + if (elm == NULL) > + goto fail; > + > + LDAP_DEBUG("aldap_bind_sasl_external", root); > + > + if (aldap_send(ldap, root) == -1) { > + root = NULL; > + goto fail; > + } > + return (ldap->msgid); > +fail: > + if (root != NULL) Since ber.c r1.32, this NULL check is no longer necessary. Perhaps all such checks could be garbage collected in another diff. > + ober_free_elements(root); > + > + ldap->err = ALDAP_ERR_OPERATION_FAILED; > + return (-1); > +} > + > +int > aldap_unbind(struct aldap *ldap) > { > struct ber_element *root = NULL, *elm; > Index: aldap.h > =================================================================== > RCS file: /cvs/src/usr.sbin/ypldap/aldap.h,v > retrieving revision 1.14 > diff -u -p -r1.14 aldap.h > --- aldap.h 11 May 2019 17:46:02 -0000 1.14 > +++ aldap.h 19 Sep 2022 11:47:13 -0000 > @@ -32,6 +32,8 @@ > #define LDAP_PAGED_OID "1.2.840.113556.1.4.319" > #define LDAP_STARTTLS_OID "1.3.6.1.4.1.1466.20037" > > +#define LDAP_SASL_MECH_EXTERNAL "EXTERNAL" > + > struct aldap { > #define ALDAP_ERR_SUCCESS 0 > #define ALDAP_ERR_PARSER_ERROR 1 > @@ -137,6 +139,7 @@ enum deref_aliases { > > enum authentication_choice { > LDAP_AUTH_SIMPLE = 0, > + LDAP_AUTH_SASL = 3, > }; > > enum scope { > @@ -222,6 +225,7 @@ void aldap_freemsg(struct > aldap_messa > int aldap_req_starttls(struct aldap *); > > int aldap_bind(struct aldap *, char *, char *); > +int aldap_bind_sasl_external(struct aldap *, char *); > int aldap_unbind(struct aldap *); > int aldap_search(struct aldap *, char *, enum scope, char *, char **, int, > int, int, struct aldap_page_control *); > int aldap_get_errno(struct aldap *, const char **); > Index: ldapclient.c > =================================================================== > RCS file: /cvs/src/usr.sbin/ypldap/ldapclient.c,v > retrieving revision 1.45 > diff -u -p -r1.45 ldapclient.c > --- ldapclient.c 22 Aug 2022 10:10:59 -0000 1.45 > +++ ldapclient.c 19 Sep 2022 11:47:13 -0000 > @@ -635,7 +635,11 @@ client_try_idm(struct env *env, struct i > int rc; > > where = "binding"; > - if (aldap_bind(al, idm->idm_binddn, idm->idm_bindcred) == -1) > + if (idm->idm_bindext != 0) > + rc = aldap_bind_sasl_external(al, idm->idm_bindextid); > + else > + rc = aldap_bind(al, idm->idm_binddn, idm->idm_bindcred); > + if (rc == -1) > goto bad; > > where = "parsing"; > Index: parse.y > =================================================================== > RCS file: /cvs/src/usr.sbin/ypldap/parse.y,v > retrieving revision 1.35 > diff -u -p -r1.35 parse.y > --- parse.y 19 Aug 2022 03:50:32 -0000 1.35 > +++ parse.y 19 Sep 2022 11:47:13 -0000 > @@ -108,7 +108,7 @@ typedef struct { > %token USER GROUP TO EXPIRE HOME SHELL GECOS UID GID INTERVAL > %token PASSWD NAME FIXED LIST GROUPNAME GROUPPASSWD GROUPGID MAP > %token INCLUDE DIRECTORY CLASS PORT ERROR GROUPMEMBERS LDAPS TLS CAFILE > -%token BIND LOCAL PORTMAP > +%token BIND LOCAL PORTMAP BINDEXT CERTFILE KEYFILE > %token <v.string> STRING > %token <v.number> NUMBER > %type <v.number> opcode attribute > @@ -213,6 +213,11 @@ attribute : NAME > { $$ = 0; } > ; > > diropt : BINDDN STRING { > + if (idm->idm_bindext != 0) { > + yyerror("can't specify multiple bind types"); > + free($2); > + YYERROR; > + } > idm->idm_flags |= F_NEEDAUTH; > if (strlcpy(idm->idm_binddn, $2, > sizeof(idm->idm_binddn)) >= > @@ -224,6 +229,11 @@ diropt : BINDDN STRING > { > free($2); > } > | BINDCRED STRING { > + if (idm->idm_bindext != 0) { > + yyerror("can't specify multiple bind types"); > + free($2); > + YYERROR; > + } > idm->idm_flags |= F_NEEDAUTH; > if (strlcpy(idm->idm_bindcred, $2, > sizeof(idm->idm_bindcred)) >= > @@ -234,6 +244,64 @@ diropt : BINDDN STRING > { > } > free($2); > } > + | BINDEXT STRING { > + if (idm->idm_flags & F_NEEDAUTH) { > + yyerror("can't specify multiple bind types"); > + free($2); > + YYERROR; > + } > + idm->idm_flags |= F_NEEDAUTH; > + idm->idm_bindext = 1; > + if (strlcpy(idm->idm_bindextid, $2, > + sizeof(idm->idm_bindextid)) >= > + sizeof(idm->idm_bindextid)) { > + yyerror("directory bindext truncated"); > + free($2); > + YYERROR; > + } > + free($2); > + } > + | BINDEXT { > + if (idm->idm_flags & F_NEEDAUTH) { > + yyerror("can't specify multiple bind types"); > + YYERROR; > + } > + idm->idm_flags |= F_NEEDAUTH; > + idm->idm_bindext = 1; > + idm->idm_bindextid[0] = '\0'; > + } > + | CERTFILE STRING { > + if (idm->idm_tls_config == NULL) { > + yyerror("can't set cert file without tls" > + " enabled"); > + free($2); > + YYERROR; > + } > + if (tls_config_set_cert_file(idm->idm_tls_config, $2) > + == -1) { > + yyerror("tls set cert file failed: %s", > + tls_config_error( > + idm->idm_tls_config)); > + free($2); > + YYERROR; > + } > + } > + | KEYFILE STRING { > + if (idm->idm_tls_config == NULL) { > + yyerror("can't set key file without tls" > + " enabled"); > + free($2); > + YYERROR; > + } > + if (tls_config_set_key_file(idm->idm_tls_config, $2) > + == -1) { > + yyerror("tls set key file failed: %s", > + tls_config_error( > + idm->idm_tls_config)); > + free($2); > + YYERROR; > + } > + } > | BASEDN STRING { > if (strlcpy(idm->idm_basedn, $2, > sizeof(idm->idm_basedn)) >= > @@ -460,7 +528,9 @@ lookup(char *s) > { "bind", BIND }, > { "bindcred", BINDCRED }, > { "binddn", BINDDN }, > + { "bindext", BINDEXT }, > { "cafile", CAFILE }, > + { "certfile", CERTFILE }, > { "change", CHANGE }, > { "class", CLASS }, > { "directory", DIRECTORY }, > @@ -479,6 +549,7 @@ lookup(char *s) > { "home", HOME }, > { "include", INCLUDE }, > { "interval", INTERVAL }, > + { "keyfile", KEYFILE }, > { "ldaps", LDAPS }, > { "list", LIST }, > { "local", LOCAL }, > Index: ypldap.conf.5 > =================================================================== > RCS file: /cvs/src/usr.sbin/ypldap/ypldap.conf.5,v > retrieving revision 1.27 > diff -u -p -r1.27 ypldap.conf.5 > --- ypldap.conf.5 22 Aug 2022 07:07:45 -0000 1.27 > +++ ypldap.conf.5 19 Sep 2022 11:47:13 -0000 > @@ -144,6 +144,9 @@ or > attribute to the LDAP attribute name supplied. > .It Ic basedn Ar string > Use the supplied search base as starting point for the directory search. > +.It Ic certfile Ar string > +Use the specified client certificate when connecting to the directory. > +The file must contain a PEM encoded certificate. > .It Ic groupdn Ar string > Use the supplied search base as starting point for the directory search for > groups. > @@ -152,12 +155,23 @@ If not supplied, the basedn value will b > Use the supplied credentials for simple authentication against the directory. > .It Ic binddn Ar string > Use the supplied Distinguished Name to bind to the directory. > +.It Ic bindext Oo Ar string Oc > +Bind to the directory using SASL EXTERNAL, optionally using a supplied > identity > +string. > +When using a TLS client certificate, this allows the client to bind as the > +subject of the certificate. > +If an identity string is supplied, usually in the form of a distinguished > name > +prefixed with "dn:", the directory will only allow the bind to succeed if it > +matches the subject of the certificate. > .It Ic fixed attribute Ar attribute string > Do not retrieve the specified attribute from LDAP but > instead set it unconditionally to the supplied value for > every entry. > .It Ic group filter Ar string > Use the supplied LDAP filter to retrieve group entries. > +.It Ic keyfile Ar string > +Use the specified private key when connecting to the directory. > +The file must contain a PEM encoded key. > .It Xo > .Ic list Ar name Ic maps to Ar string > .Xc > Index: ypldap.h > =================================================================== > RCS file: /cvs/src/usr.sbin/ypldap/ypldap.h,v > retrieving revision 1.22 > diff -u -p -r1.22 ypldap.h > --- ypldap.h 19 Aug 2022 03:50:32 -0000 1.22 > +++ ypldap.h 19 Sep 2022 11:47:13 -0000 > @@ -101,7 +101,9 @@ struct idm { > u_int32_t idm_list; > struct ypldap_addr_list idm_addr; > in_port_t idm_port; > + int idm_bindext; > char idm_binddn[LINE_WIDTH]; > + char idm_bindextid[LINE_WIDTH]; > char idm_bindcred[LINE_WIDTH]; > char idm_basedn[LINE_WIDTH]; > char idm_groupdn[LINE_WIDTH]; >