Password change using Ldap3

I am currently trying to write a web app gui to administrate a samba domain controller.

I am able to add, modify, and delete users.
Setting passwords does not seem to work for some reason.

I can set the password with ldap_modify and this ldif:

dn: CN=TestUser,CN=Users,DC=mydomain,DC=local
changetype: modify
delete: unicodePwd
-
add: unicodePwd
unicodePwd:: IgB0AGUAcwB0AFAAYQBzAHMAdwBvAHIAZAAxADIAMwA0ACIA

But when I try to accomplish the same in ldap3 it does not work and I am having difficulties determining what is different.

I am running ldap.modify as:

    ldap.modify(
        &query_base,
        mods
    ).await?.success()?;

where query_base is "CN=TestUser,CN=Users,DC=mydomain,DC=local"
It is run after successfully binding with:
CN=Administrator,CN=Users,DC=mydomain,DC=local

I am using the Mod<String> generic to deserialize the JSON from my api call.
A println!("{:?}", mods) gives:

[
Delete("unicodePwd", {}), 
Add("unicodePwd", {"IgB0AGUAcwB0AFAAYQBzAHMAdwBvAHIAZAAxADIAMwA0ACIA"})
]

I am hoping some can figure out how my rust ldap3 ldap.modify call is different from the ldap_modify ldif that I am performing from the command line.

Both my ldap_modify and the rust ldap3 ldap.modify are running with starttls set.

Do you have anything that can tell you what error, specifically, is being returned? If not, try changing

    ldap.modify(
        &query_base,
        mods
    ).await?.success()?;

to

    dbg!(
        ldap.modify(
            &query_base,
            mods
        ).await
    )?.success()?;

Your program will print the Result of the await to stderr, generally as either Ok(…) or Err(…). (The type does appear to implement Debug.)

1 Like

I should have posted this before, but I didn't want to confuse things:


"LdapResult { result: LdapResult { rc: 53, matched: \"\", text: \"00002035: setup_io: it's not allowed to set the NT hash password directly'\", refs: [], ctrls: [] } }"

It seems this error is pretty much generic for samba whenever you don't do the password change exactly the way it wants. It wants a delete for the unicodePwd, followed by an add for the unicodePwd with the new password.

If I wasn't using Administrator for the bind, but trying to do it directly with the user, it would want the delete to contain the old password.

One thing that may be a problem here is that the ldif uses :: rather than : on the value to denote that the value is in base64 (which is apparently required for password change). I don't know how to replicate this in the rust ldap3 crate or if it is even possible.

I also tried challenging the fact that "it must be base64 encoded", but I run into a problem with the json parser that is re-exposed by rocket since the password must be enclosed in double-quotes. It appears that escaping double-quotes in the json will not parse.

I tried adding the quotes in code and it turns out that even if the data guard would have allowed the quotes, it still wasn't accepted by the ldap server.

I also took a look at the source for ldap3. It doesn't seem that there is any provision in the code for base64 entries, but I don't know enough about the spec to know if that is an ldap thing or an actual protocol thing. The instructions I found for setting the password dictate that the password set won't work without base64 encoding a string in quotes, but looking a the ldap3 crate code it seems that the protocol is more simple than that. I am going to go read up on the protocol now to see what I can figure out.

I have only been doing Rust for a few days now, so this is probably expected. Every time I think I have a good understanding of the Rust language I end up trying something or reading something that I just cannot internally decode yet.

There might be something in the ldap3 crate which allows me to correctly specify a base64 value, but I am having difficulty following the code. There seems to be a Tag structure which attempts to handle and encode all sorts of types, but I am not sure how to use it from the interface. I also don't have a good understanding of controls yet. It is possible those are usable for specifying ldap attribute types, but I think they are more for protocol level control.

This: ldap3::asn1 - Rust is probably what I need. The documentation seems to discourage its use, but if I understand what I am reading (and I don't); this will be the only way to get what I need done.

There may be more password rules that I am forgetting to list.
Here is the code I am using to create the base 64 password string that I am passing in:
echo -n '"testPassword1234"' | iconv -f utf8 -t utf16le | base64 -w 0

It seems it also needs to be a utf-16 string. So if the base64 is not actually being sent, but rather being used as a method for ldap_modify to convery unicode strings, then the problem is that even though I can add double-quotes, I cannot encode utf-16 strings.

So then that means that rules for password reset by Adminstrator are:

  1. Must be a delete for unicodePwd, followed by an add for unicodePwd with the value of the new password
  2. Must be surrounded by double quotes
  3. The double-quoted string must utf-16 encoded
  4. The ldif requires base64, but specifies the tag differently (I don't know if it is being sent this way.)

I think I actually have all of the information together now. :slight_smile:

I may have to implement a separate change_password API just to do all of this. I think I can make the utf-16 string by mapping to a Vec<u16>, but I am not sure how to get the ldap3 crate to accept this. It may all be automagic, but I probably have to provide some sort of octet string converter.

At least I feel like I am getting closer to the answer. It would be nice to know if ldap_modify is actually sending base64 data. Maybe I can snoop it to find out.

Have you seen this GH issue? It should have everything you need to change passwords using straight ldap3 calls.

Thank you for pointing out to me! It looks like that will do exactly what I need. Next time I will try to check the GH issues also.

That did give me what I needed to get this to work. As I had expected, the base64 encoding wasn't actually necessary, but rather how utf-16 was being coerced into an ldif.

I feel like my solution is perhaps the most complicated possible way to do something simple.
It may be a lot to ask, but I would not mind help refining this by someone who has done a bit more rust than me. The following solution works, but feels clunky:

#[derive(Deserialize)]
struct LdapModRequest<'a> {
    op: &'a str,
    attr: &'a str,
    values: HashSet<&'a str>
}

trait IntoBytes: Sized {
    fn to_le_bytes(a: Self) -> Vec<u8>;
}

impl IntoBytes for u16 {
    fn to_le_bytes(a: Self) -> Vec<u8> {
        a.to_le_bytes().to_vec()
    }
}
....
    let mods = mods.iter().filter_map( |modifier| 
        {
            let attr = String::from(modifier.attr).into_bytes();
            let values: HashSet<Vec<u8>> = modifier.values.iter().map( 
                |v| format!("\"{}\"", *v).encode_utf16().flat_map( 
                    |b| b.to_le_bytes() 
                ).collect()
            ).collect();

            match modifier.op.to_lowercase().as_str() {
                "add" => Some(Mod::Add(attr, values)),
                "delete" => Some(Mod::Delete(attr, values)),
                "replace" => Some(Mod::Replace(attr, values)),
                "increment" => Some(Mod::Increment(attr, values.into_iter().next()? )),
                _ => None
            }
        }
    ).collect();
1 Like

I spoke too soon. This does return "Ok" from the server, but I still can't bind using this password and an ldap compare returns that the password is not the same as how I set it.

None of these are ldap3 problems though. Thanks for the help!

1 Like

With the above, I can change the password of an existing user and the new password will work for a bind. The user I created myself through ldap does not work. So the above is probably ok and this is a different problem.