diff -pruN 0.19.0-1/.github/workflows/test.yml 0.20.0-1/.github/workflows/test.yml
--- 0.19.0-1/.github/workflows/test.yml	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/.github/workflows/test.yml	2025-08-22 13:07:29.000000000 +0000
@@ -17,17 +17,17 @@ jobs:
   test:
     runs-on: ubuntu-latest
     strategy:
+      fail-fast: false
       matrix:
         ruby:
-          - "2.6"
-          - "2.7"
           - "3.0"
           - "3.1"
           - "3.2"
-          - "jruby-9.3"
+          - "3.3"
+          - "3.4"
           - "jruby-9.4"
-          - "truffleruby-22"
+          - "truffleruby"
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v4
     - name: Run tests with Ruby ${{ matrix.ruby }}
-      run: docker-compose run ci-${{ matrix.ruby }}
+      run: docker compose run ci-${{ matrix.ruby }}
diff -pruN 0.19.0-1/.rubocop_todo.yml 0.20.0-1/.rubocop_todo.yml
--- 0.19.0-1/.rubocop_todo.yml	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/.rubocop_todo.yml	2025-08-22 13:07:29.000000000 +0000
@@ -1,26 +1,11 @@
 # This configuration was generated by
 # `rubocop --auto-gen-config`
-# on 2023-03-29 17:13:45 UTC using RuboCop version 1.48.1.
+# on 2025-05-31 20:03:27 UTC using RuboCop version 1.75.8.
 # The point is for the user to remove these configuration records
 # one by one as the offenses are removed from the code base.
 # Note that changes in the inspected code, or installation of new
 # versions of RuboCop, may require this file to be generated again.
 
-# Offense count: 1
-# This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include.
-# Include: **/*.gemspec
-Gemspec/OrderedDependencies:
-  Exclude:
-    - 'net-ldap.gemspec'
-
-# Offense count: 1
-# Configuration parameters: Severity, Include.
-# Include: **/*.gemspec
-Gemspec/RequiredRubyVersion:
-  Exclude:
-    - 'net-ldap.gemspec'
-
 # Offense count: 3
 # This cop supports safe autocorrection (--autocorrect).
 # Configuration parameters: EnforcedStyle, IndentationWidth.
@@ -61,7 +46,7 @@ Layout/EmptyLineAfterMagicComment:
 
 # Offense count: 6
 # This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, AllowAdjacentOneLineDefs, NumberOfEmptyLines.
+# Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines.
 Layout/EmptyLineBetweenDefs:
   Exclude:
     - 'lib/net/ldap/dataset.rb'
@@ -104,7 +89,7 @@ Layout/EndAlignment:
   Exclude:
     - 'testserver/ldapserver.rb'
 
-# Offense count: 2
+# Offense count: 6
 # This cop supports safe autocorrection (--autocorrect).
 # Configuration parameters: IndentationWidth.
 # SupportedStyles: special_inside_parentheses, consistent, align_brackets
@@ -148,9 +133,9 @@ Layout/IndentationWidth:
     - 'lib/net/ldap/password.rb'
     - 'lib/net/snmp.rb'
 
-# Offense count: 15
+# Offense count: 14
 # This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment.
+# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment, AllowRBSInlineAnnotation, AllowSteepAnnotation.
 Layout/LeadingCommentSpace:
   Exclude:
     - 'lib/net/ber/core_ext/array.rb'
@@ -168,7 +153,7 @@ Layout/MultilineMethodCallBraceLayout:
   Exclude:
     - 'lib/net/ldap/filter.rb'
 
-# Offense count: 7
+# Offense count: 8
 # This cop supports safe autocorrection (--autocorrect).
 # Configuration parameters: EnforcedStyle.
 # SupportedStyles: space, no_space
@@ -186,8 +171,9 @@ Layout/SpaceAroundKeyword:
 
 # Offense count: 7
 # This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator.
+# Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator, EnforcedStyleForRationalLiterals.
 # SupportedStylesForExponentOperator: space, no_space
+# SupportedStylesForRationalLiterals: space, no_space
 Layout/SpaceAroundOperators:
   Exclude:
     - 'lib/net/ber/ber_parser.rb'
@@ -214,8 +200,8 @@ Layout/SpaceInsideParens:
     - 'lib/net/snmp.rb'
 
 # Offense count: 1
-# This cop supports unsafe autocorrection (--autocorrect-all).
-# Configuration parameters: AllowComments.
+# This cop supports safe autocorrection (--autocorrect).
+# Configuration parameters: AutoCorrect, AllowComments.
 Lint/EmptyConditionalBody:
   Exclude:
     - 'lib/net/ldap/filter.rb'
@@ -227,6 +213,7 @@ Lint/EmptyWhen:
     - 'lib/net/ldap/pdu.rb'
 
 # Offense count: 30
+# This cop supports safe autocorrection (--autocorrect).
 Lint/ImplicitStringConcatenation:
   Exclude:
     - 'test/test_filter.rb'
@@ -241,9 +228,9 @@ Lint/RescueException:
   Exclude:
     - 'lib/net/ldap/pdu.rb'
 
-# Offense count: 9
+# Offense count: 10
 # This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
+# Configuration parameters: AutoCorrect, IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
 Lint/UnusedBlockArgument:
   Exclude:
     - 'lib/net/ldap.rb'
@@ -251,7 +238,8 @@ Lint/UnusedBlockArgument:
 
 # Offense count: 7
 # This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods.
+# Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions.
+# NotImplementedExceptions: NotImplementedError
 Lint/UnusedMethodArgument:
   Exclude:
     - 'lib/net/ldap/entry.rb'
@@ -262,19 +250,21 @@ Lint/UnusedMethodArgument:
 
 # Offense count: 1
 # This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods.
+# Configuration parameters: AutoCorrect, ContextCreatingMethods, MethodCreatingMethods.
 Lint/UselessAccessModifier:
   Exclude:
     - 'lib/net/ldap/connection.rb'
 
 # Offense count: 5
+# This cop supports safe autocorrection (--autocorrect).
+# Configuration parameters: AutoCorrect.
 Lint/UselessAssignment:
   Exclude:
     - 'test/integration/test_add.rb'
     - 'test/test_ldap_connection.rb'
     - 'test/test_search.rb'
 
-# Offense count: 38
+# Offense count: 42
 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
 Metrics/AbcSize:
   Max: 124
@@ -285,22 +275,22 @@ Metrics/AbcSize:
 Metrics/BlockLength:
   Max: 119
 
-# Offense count: 11
-# Configuration parameters: CountBlocks.
+# Offense count: 6
+# Configuration parameters: CountBlocks, CountModifierForms.
 Metrics/BlockNesting:
   Max: 4
 
-# Offense count: 11
+# Offense count: 12
 # Configuration parameters: CountComments, CountAsOne.
 Metrics/ClassLength:
-  Max: 443
+  Max: 451
 
-# Offense count: 20
+# Offense count: 21
 # Configuration parameters: AllowedMethods, AllowedPatterns.
 Metrics/CyclomaticComplexity:
   Max: 45
 
-# Offense count: 74
+# Offense count: 79
 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
 Metrics/MethodLength:
   Max: 130
@@ -333,7 +323,7 @@ Naming/ClassAndModuleCamelCase:
   Exclude:
     - 'lib/net/ldap/auth_adapter/gss_spnego.rb'
 
-# Offense count: 87
+# Offense count: 88
 Naming/ConstantName:
   Exclude:
     - 'lib/net/ldap.rb'
@@ -350,6 +340,7 @@ Naming/ConstantName:
 # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
 Naming/FileName:
   Exclude:
+    - 'Rakefile.rb'
     - 'lib/net-ldap.rb'
 
 # Offense count: 11
@@ -380,7 +371,7 @@ Style/AccessorGrouping:
     - 'lib/net/ldap.rb'
     - 'lib/net/ldap/pdu.rb'
 
-# Offense count: 10
+# Offense count: 11
 # This cop supports safe autocorrection (--autocorrect).
 # Configuration parameters: EnforcedStyle.
 # SupportedStyles: prefer_alias, prefer_alias_method
@@ -434,8 +425,10 @@ Style/CharacterLiteral:
 
 # Offense count: 23
 # This cop supports unsafe autocorrection (--autocorrect-all).
-# Configuration parameters: EnforcedStyle.
+# Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules.
 # SupportedStyles: nested, compact
+# SupportedStylesForClasses: ~, nested, compact
+# SupportedStylesForModules: ~, nested, compact
 Style/ClassAndModuleChildren:
   Enabled: false
 
@@ -493,7 +486,7 @@ Style/Documentation:
 
 # Offense count: 1
 # This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: EnforcedStyle.
+# Configuration parameters: AutoCorrect, EnforcedStyle.
 # SupportedStyles: compact, expanded
 Style/EmptyMethod:
   Exclude:
@@ -525,7 +518,7 @@ Style/ExplicitBlockArgument:
     - 'lib/net/ldap.rb'
     - 'lib/net/ldap/dataset.rb'
 
-# Offense count: 54
+# Offense count: 57
 # This cop supports unsafe autocorrection (--autocorrect-all).
 # Configuration parameters: EnforcedStyle.
 # SupportedStyles: always, always_true, never
@@ -545,11 +538,11 @@ Style/GuardClause:
   Exclude:
     - 'lib/net/ldap/filter.rb'
 
-# Offense count: 159
+# Offense count: 164
 # This cop supports safe autocorrection (--autocorrect).
 # Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
 # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
-# SupportedShorthandSyntax: always, never, either, consistent
+# SupportedShorthandSyntax: always, never, either, consistent, either_consistent
 Style/HashSyntax:
   Exclude:
     - 'lib/net/ber.rb'
@@ -573,7 +566,7 @@ Style/IfInsideElse:
   Exclude:
     - 'lib/net/ldap/instrumentation.rb'
 
-# Offense count: 25
+# Offense count: 28
 # This cop supports safe autocorrection (--autocorrect).
 Style/IfUnlessModifier:
   Exclude:
@@ -618,7 +611,14 @@ Style/MultilineWhenThen:
   Exclude:
     - 'lib/net/ldap/dn.rb'
 
-# Offense count: 25
+# Offense count: 1
+# This cop supports safe autocorrection (--autocorrect).
+# Configuration parameters: AllowMethodComparison, ComparisonsThreshold.
+Style/MultipleComparison:
+  Exclude:
+    - 'lib/net/ldap/dataset.rb'
+
+# Offense count: 26
 # This cop supports unsafe autocorrection (--autocorrect-all).
 # Configuration parameters: EnforcedStyle.
 # SupportedStyles: literals, strict
@@ -650,7 +650,7 @@ Style/NegatedWhile:
 
 # Offense count: 3
 # This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: EnforcedStyle, MinBodyLength.
+# Configuration parameters: EnforcedStyle, MinBodyLength, AllowConsecutiveConditionals.
 # SupportedStyles: skip_modifier_ifs, always
 Style/Next:
   Exclude:
@@ -678,7 +678,7 @@ Style/Not:
   Exclude:
     - 'lib/net/ldap/filter.rb'
 
-# Offense count: 11
+# Offense count: 13
 # This cop supports safe autocorrection (--autocorrect).
 # Configuration parameters: Strict, AllowedNumbers, AllowedPatterns.
 Style/NumericLiterals:
@@ -704,15 +704,12 @@ Style/OptionalBooleanParameter:
   Exclude:
     - 'lib/net/ldap/entry.rb'
 
-# Offense count: 6
+# Offense count: 1
 # This cop supports safe autocorrection (--autocorrect).
 # Configuration parameters: AllowSafeAssignment, AllowInMultilineConditions.
 Style/ParenthesesAroundCondition:
   Exclude:
-    - 'lib/net/ldap.rb'
-    - 'lib/net/ldap/auth_adapter/gss_spnego.rb'
     - 'lib/net/ldap/auth_adapter/sasl.rb'
-    - 'lib/net/ldap/auth_adapter/simple.rb'
 
 # Offense count: 13
 # This cop supports safe autocorrection (--autocorrect).
@@ -737,7 +734,7 @@ Style/PerlBackrefs:
     - 'testserver/ldapserver.rb'
 
 # Offense count: 10
-# This cop supports safe autocorrection (--autocorrect).
+# This cop supports unsafe autocorrection (--autocorrect-all).
 # Configuration parameters: EnforcedStyle, AllowedCompactTypes.
 # SupportedStyles: compact, exploded
 Style/RaiseArgs:
@@ -874,7 +871,7 @@ Style/StringConcatenation:
     - 'test/test_ldif.rb'
     - 'test/test_snmp.rb'
 
-# Offense count: 683
+# Offense count: 728
 # This cop supports safe autocorrection (--autocorrect).
 # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
 # SupportedStyles: single_quotes, double_quotes
@@ -907,7 +904,7 @@ Style/TernaryParentheses:
 # Offense count: 38
 # This cop supports safe autocorrection (--autocorrect).
 # Configuration parameters: EnforcedStyleForMultiline.
-# SupportedStylesForMultiline: comma, consistent_comma, no_comma
+# SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma
 Style/TrailingCommaInHashLiteral:
   Enabled: false
 
@@ -955,9 +952,9 @@ Style/ZeroLengthPredicate:
     - 'lib/net/ldap/filter.rb'
     - 'testserver/ldapserver.rb'
 
-# Offense count: 24
+# Offense count: 27
 # This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
+# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
 # URISchemes: http, https
 Layout/LineLength:
   Max: 360
diff -pruN 0.19.0-1/Gemfile 0.20.0-1/Gemfile
--- 0.19.0-1/Gemfile	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/Gemfile	2025-08-22 13:07:29.000000000 +0000
@@ -1,2 +1,8 @@
 source 'https://rubygems.org'
 gemspec
+
+gem "debug", platform: :mri
+gem "flexmock", "~> 1.3"
+gem "rake", "~> 12.3.3"
+gem "rubocop", "~> 1.48"
+gem "test-unit"
diff -pruN 0.19.0-1/History.rdoc 0.20.0-1/History.rdoc
--- 0.19.0-1/History.rdoc	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/History.rdoc	2025-08-22 13:07:29.000000000 +0000
@@ -1,3 +1,14 @@
+=== Net::LDAP 0.20.0
+* Update test.yml by @HarlemSquirrel in #433
+* Add `ostruct` as a dependency to the gemspec by @Ivanov-Anton in #432
+* Require Ruby >= 3.0 by @HarlemSquirrel in #435
+* Link to usage examples by @sebbASF in #428
+* Add controls for modify and add operations by @zeroSteiner in #426
+* Add support for ldapwhoami (RFC4532) (now with tests) by @zeroSteiner in #425
+* Update for ruby 3.4 by @HarlemSquirrel in #439
+* Add ruby 3.4 to CI by @hakeem0114 in #438
+* Add support for UTF-8 encoded passwords by @frankwalentowski in #430
+
 === Net::LDAP 0.19.0
 * Net::LDAP::DN - Retain trailing spaces in RDN values in DNs #412
 * Add in ability for users to specify LDAP controls when conducting searches #411
diff -pruN 0.19.0-1/README.rdoc 0.20.0-1/README.rdoc
--- 0.19.0-1/README.rdoc	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/README.rdoc	2025-08-22 13:07:29.000000000 +0000
@@ -23,7 +23,7 @@ the most recent LDAP RFCs (4510–4519,
 
 == Synopsis
 
-See {Net::LDAP on rubydoc.info}[https://www.rubydoc.info/github/ruby-ldap/ruby-net-ldap] for documentation and usage samples.
+See {Net::LDAP on rubydoc.info}[https://www.rubydoc.info/github/ruby-ldap/ruby-net-ldap/Net/LDAP] for documentation and usage samples.
 
 == Requirements
 
diff -pruN 0.19.0-1/ci-run.sh 0.20.0-1/ci-run.sh
--- 0.19.0-1/ci-run.sh	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/ci-run.sh	2025-08-22 13:07:29.000000000 +0000
@@ -3,5 +3,6 @@
 set -e
 
 gem install bundler
+ruby -v | grep jruby && apt update && apt install -y gcc
 bundle check || bundle install
 bundle exec rake ci
diff -pruN 0.19.0-1/debian/changelog 0.20.0-1/debian/changelog
--- 0.19.0-1/debian/changelog	2024-12-18 21:34:08.000000000 +0000
+++ 0.20.0-1/debian/changelog	2025-10-26 15:52:50.000000000 +0000
@@ -1,3 +1,11 @@
+ruby-net-ldap (0.20.0-1) unstable; urgency=medium
+
+  * Team upload.
+  * New upstream release.
+  * Update Standards-Version to 4.7.2, no changes needed.
+
+ -- Simon Quigley <tsimonq2@debian.org>  Sun, 26 Oct 2025 10:52:50 -0500
+
 ruby-net-ldap (0.19.0-1) unstable; urgency=medium
 
   * Team upload.
diff -pruN 0.19.0-1/debian/control 0.20.0-1/debian/control
--- 0.19.0-1/debian/control	2024-12-18 21:33:53.000000000 +0000
+++ 0.20.0-1/debian/control	2025-10-26 15:51:28.000000000 +0000
@@ -7,7 +7,7 @@ Build-Depends: debhelper-compat (= 13),
                gem2deb (>= 2.1~),
                ruby-flexmock,
                ruby-test-unit
-Standards-Version: 4.7.0
+Standards-Version: 4.7.2
 Vcs-Git: https://salsa.debian.org/ruby-team/ruby-net-ldap.git
 Vcs-Browser: https://salsa.debian.org/ruby-team/ruby-net-ldap
 Homepage: https://rubyldap.com/
diff -pruN 0.19.0-1/debian/gbp.conf 0.20.0-1/debian/gbp.conf
--- 0.19.0-1/debian/gbp.conf	1970-01-01 00:00:00.000000000 +0000
+++ 0.20.0-1/debian/gbp.conf	2025-10-26 15:50:58.000000000 +0000
@@ -0,0 +1,4 @@
+[DEFAULT]
+debian-branch = debian/latest
+upstream-branch = upstream/latest
+pristine-tar = True
diff -pruN 0.19.0-1/debian/salsa-ci.yml 0.20.0-1/debian/salsa-ci.yml
--- 0.19.0-1/debian/salsa-ci.yml	2024-12-18 21:30:21.000000000 +0000
+++ 0.20.0-1/debian/salsa-ci.yml	2025-10-26 15:50:58.000000000 +0000
@@ -1,4 +1,3 @@
 ---
 include:
-  - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml
-  - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml
+  - https://salsa.debian.org/ruby-team/meta/raw/master/salsa-ci.yml
diff -pruN 0.19.0-1/docker-compose.yml 0.20.0-1/docker-compose.yml
--- 0.19.0-1/docker-compose.yml	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/docker-compose.yml	2025-08-22 13:07:29.000000000 +0000
@@ -1,5 +1,3 @@
-version: "3.8"
-
 networks:
   integration_test_network:
 
@@ -24,9 +22,9 @@ services:
     volumes:
       - ./test/fixtures/ldif:/ldif:ro
 
-  ci-2.6:
-    image: ruby:2.7
-    entrypoint: /code/ci-run.sh
+  ci-3.0:
+    image: ruby:3.0
+    command: /code/ci-run.sh
     environment:
       INTEGRATION: openldap
       INTEGRATION_HOST: ldap.example.org
@@ -38,9 +36,9 @@ services:
       - .:/code
     working_dir: /code
 
-  ci-2.7:
-    image: ruby:2.7
-    entrypoint: /code/ci-run.sh
+  ci-3.1:
+    image: ruby:3.1
+    command: /code/ci-run.sh
     environment:
       INTEGRATION: openldap
       INTEGRATION_HOST: ldap.example.org
@@ -52,9 +50,9 @@ services:
       - .:/code
     working_dir: /code
 
-  ci-3.0:
-    image: ruby:3.0
-    entrypoint: /code/ci-run.sh
+  ci-3.2:
+    image: ruby:3.2
+    command: /code/ci-run.sh
     environment:
       INTEGRATION: openldap
       INTEGRATION_HOST: ldap.example.org
@@ -66,9 +64,9 @@ services:
       - .:/code
     working_dir: /code
 
-  ci-3.1:
-    image: ruby:3.1
-    entrypoint: /code/ci-run.sh
+  ci-3.3:
+    image: ruby:3.3
+    command: /code/ci-run.sh
     environment:
       INTEGRATION: openldap
       INTEGRATION_HOST: ldap.example.org
@@ -80,8 +78,8 @@ services:
       - .:/code
     working_dir: /code
 
-  ci-3.2:
-    image: ruby:3.2
+  ci-3.4:
+    image: ruby:3.4
     entrypoint: /code/ci-run.sh
     environment:
       INTEGRATION: openldap
@@ -95,9 +93,9 @@ services:
     working_dir: /code
 
   # https://github.com/flavorjones/truffleruby/pkgs/container/truffleruby
-  ci-truffleruby-22:
-    image: ghcr.io/flavorjones/truffleruby:22.3.1
-    entrypoint: /code/ci-run.sh
+  ci-truffleruby:
+    image: ghcr.io/flavorjones/truffleruby:stable
+    command: /code/ci-run.sh
     environment:
       INTEGRATION: openldap
       INTEGRATION_HOST: ldap.example.org
@@ -111,7 +109,7 @@ services:
 
   ci-jruby-9.3:
     image: jruby:9.3
-    entrypoint: /code/ci-run.sh
+    command: /code/ci-run.sh
     environment:
       INTEGRATION: openldap
       INTEGRATION_HOST: ldap.example.org
@@ -125,7 +123,7 @@ services:
 
   ci-jruby-9.4:
     image: jruby:9.4
-    entrypoint: /code/ci-run.sh
+    command: /code/ci-run.sh
     environment:
       INTEGRATION: openldap
       INTEGRATION_HOST: ldap.example.org
diff -pruN 0.19.0-1/lib/net/ldap/auth_adapter/gss_spnego.rb 0.20.0-1/lib/net/ldap/auth_adapter/gss_spnego.rb
--- 0.19.0-1/lib/net/ldap/auth_adapter/gss_spnego.rb	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/lib/net/ldap/auth_adapter/gss_spnego.rb	2025-08-22 13:07:29.000000000 +0000
@@ -20,7 +20,7 @@ module Net
           require 'ntlm'
 
           user, psw = [auth[:username] || auth[:dn], auth[:password]]
-          raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)
+          raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless user && psw
 
           nego = proc do |challenge|
             t2_msg = NTLM::Message.parse(challenge)
diff -pruN 0.19.0-1/lib/net/ldap/auth_adapter/sasl.rb 0.20.0-1/lib/net/ldap/auth_adapter/sasl.rb
--- 0.19.0-1/lib/net/ldap/auth_adapter/sasl.rb	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/lib/net/ldap/auth_adapter/sasl.rb	2025-08-22 13:07:29.000000000 +0000
@@ -30,7 +30,7 @@ module Net
         def bind(auth)
           mech, cred, chall = auth[:mechanism], auth[:initial_credential],
             auth[:challenge_response]
-          raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (mech && cred && chall)
+          raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless mech && cred && chall
 
           message_id = @connection.next_msgid
 
diff -pruN 0.19.0-1/lib/net/ldap/auth_adapter/simple.rb 0.20.0-1/lib/net/ldap/auth_adapter/simple.rb
--- 0.19.0-1/lib/net/ldap/auth_adapter/simple.rb	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/lib/net/ldap/auth_adapter/simple.rb	2025-08-22 13:07:29.000000000 +0000
@@ -11,7 +11,7 @@ module Net
                         ["", ""]
                       end
 
-          raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)
+          raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless user && psw
 
           message_id = @connection.next_msgid
           request    = [
diff -pruN 0.19.0-1/lib/net/ldap/connection.rb 0.20.0-1/lib/net/ldap/connection.rb
--- 0.19.0-1/lib/net/ldap/connection.rb	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/lib/net/ldap/connection.rb	2025-08-22 13:07:29.000000000 +0000
@@ -569,7 +569,12 @@ class Net::LDAP::Connection #:nodoc:
       ops.to_ber_sequence,
     ].to_ber_appsequence(Net::LDAP::PDU::ModifyRequest)
 
-    write(request, nil, message_id)
+    controls = args.fetch(:controls, nil)
+    unless controls.nil?
+      controls = controls.to_ber_contextspecific(0)
+    end
+
+    write(request, controls, message_id)
     pdu = queued_read(message_id)
 
     if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyResponse
@@ -641,7 +646,12 @@ class Net::LDAP::Connection #:nodoc:
     message_id = next_msgid
     request    = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(Net::LDAP::PDU::AddRequest)
 
-    write(request, nil, message_id)
+    controls = args.fetch(:controls, nil)
+    unless controls.nil?
+      controls = controls.to_ber_contextspecific(0)
+    end
+
+    write(request, controls, message_id)
     pdu = queued_read(message_id)
 
     if !pdu || pdu.app_tag != Net::LDAP::PDU::AddResponse
@@ -690,6 +700,22 @@ class Net::LDAP::Connection #:nodoc:
       raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
     end
 
+    pdu
+  end
+
+  def ldapwhoami
+    ext_seq = [Net::LDAP::WhoamiOid.to_ber_contextspecific(0)]
+    request = ext_seq.to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)
+
+    message_id = next_msgid
+
+    write(request, nil, message_id)
+    pdu = queued_read(message_id)
+
+    if !pdu || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse
+      raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
+    end
+
     pdu
   end
 
diff -pruN 0.19.0-1/lib/net/ldap/password.rb 0.20.0-1/lib/net/ldap/password.rb
--- 0.19.0-1/lib/net/ldap/password.rb	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/lib/net/ldap/password.rb	2025-08-22 13:07:29.000000000 +0000
@@ -28,10 +28,14 @@ class Net::LDAP::Password
          '{SHA}' + Base64.strict_encode64(Digest::SHA1.digest(str))
       when :ssha
          salt = SecureRandom.random_bytes(16)
-         '{SSHA}' + Base64.strict_encode64(Digest::SHA1.digest(str + salt) + salt)
+         digest = Digest::SHA1.new
+         digest << str << salt
+         '{SSHA}' + Base64.strict_encode64(digest.digest + salt)
       when :ssha256
         salt = SecureRandom.random_bytes(16)
-        '{SSHA256}' + Base64.strict_encode64(Digest::SHA256.digest(str + salt) + salt)
+        digest = Digest::SHA256.new
+        digest << str << salt
+        '{SSHA256}' + Base64.strict_encode64(digest.digest + salt)
       else
          raise Net::LDAP::HashTypeUnsupportedError, "Unsupported password-hash type (#{type})"
       end
diff -pruN 0.19.0-1/lib/net/ldap/pdu.rb 0.20.0-1/lib/net/ldap/pdu.rb
--- 0.19.0-1/lib/net/ldap/pdu.rb	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/lib/net/ldap/pdu.rb	2025-08-22 13:07:29.000000000 +0000
@@ -194,13 +194,13 @@ class Net::LDAP::PDU
   #           requestValue     [1] OCTET STRING OPTIONAL }
 
   def parse_extended_response(sequence)
-    sequence.length >= 3 or raise Net::LDAP::PDU::Error, "Invalid LDAP result length."
+    sequence.length.between?(3, 5) or raise Net::LDAP::PDU::Error, "Invalid LDAP result length."
     @ldap_result = {
       :resultCode => sequence[0],
       :matchedDN => sequence[1],
       :errorMessage => sequence[2],
     }
-    @extended_response = sequence[3]
+    @extended_response = sequence.length == 3 ? nil : sequence.last
   end
   private :parse_extended_response
 
diff -pruN 0.19.0-1/lib/net/ldap/version.rb 0.20.0-1/lib/net/ldap/version.rb
--- 0.19.0-1/lib/net/ldap/version.rb	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/lib/net/ldap/version.rb	2025-08-22 13:07:29.000000000 +0000
@@ -1,5 +1,5 @@
 module Net
   class LDAP
-    VERSION = "0.19.0"
+    VERSION = "0.20.0"
   end
 end
diff -pruN 0.19.0-1/lib/net/ldap.rb 0.20.0-1/lib/net/ldap.rb
--- 0.19.0-1/lib/net/ldap.rb	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/lib/net/ldap.rb	2025-08-22 13:07:29.000000000 +0000
@@ -311,7 +311,7 @@ class Net::LDAP
     0 => :array, # RFC-2251 Control and Filter-AND
     1 => :array, # SearchFilter-OR
     2 => :array, # SearchFilter-NOT
-    3 => :array, # Seach referral
+    3 => :array, # Search referral
     4 => :array, # unknown use in Microsoft Outlook
     5 => :array, # SearchFilter-GE
     6 => :array, # SearchFilter-LE
@@ -325,7 +325,7 @@ class Net::LDAP
 
   universal = {
     constructed: {
-      107 => :array, #ExtendedResponse (PasswdModifyResponseValue)
+      107 => :string, # ExtendedResponse
     },
   }
 
@@ -341,6 +341,7 @@ class Net::LDAP
 
   StartTlsOid = '1.3.6.1.4.1.1466.20037'
   PasswdModifyOid = '1.3.6.1.4.1.4203.1.11.1'
+  WhoamiOid = '1.3.6.1.4.1.4203.1.11.3'
 
   # https://tools.ietf.org/html/rfc4511#section-4.1.9
   # https://tools.ietf.org/html/rfc4511#appendix-A
@@ -1200,6 +1201,23 @@ class Net::LDAP
     end
   end
 
+  # Return the authorization identity of the client that issues the
+  # ldapwhoami request.  The method does not support any arguments.
+  #
+  # Returns True or False to indicate whether the request was successfull.
+  # The result is available in the extended status information when calling
+  # #get_operation_result.
+  #
+  #  ldap.ldapwhoami
+  #  puts ldap.get_operation_result.extended_response
+  def ldapwhoami(args = {})
+    instrument "ldapwhoami.net_ldap", args do |payload|
+      @result = use_connection(args, &:ldapwhoami)
+      @result.success? ? @result.extended_response : nil
+    end
+  end
+  alias_method :whoami, :ldapwhoami
+
   # This method is experimental and subject to change. Return the rootDSE
   # record from the LDAP server as a Net::LDAP::Entry, or an empty Entry if
   # the server doesn't return the record.
@@ -1257,10 +1275,10 @@ class Net::LDAP
     rs = search(:ignore_server_caps => true, :base => "",
                 :scope => SearchScope_BaseObject,
                 :attributes => [:subschemaSubentry])
-    return Net::LDAP::Entry.new unless (rs and rs.first)
+    return Net::LDAP::Entry.new unless rs and rs.first
 
     subschema_name = rs.first.subschemasubentry
-    return Net::LDAP::Entry.new unless (subschema_name and subschema_name.first)
+    return Net::LDAP::Entry.new unless subschema_name and subschema_name.first
 
     rs = search(:ignore_server_caps => true, :base => subschema_name.first,
                 :scope => SearchScope_BaseObject,
diff -pruN 0.19.0-1/net-ldap.gemspec 0.20.0-1/net-ldap.gemspec
--- 0.19.0-1/net-ldap.gemspec	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/net-ldap.gemspec	2025-08-22 13:07:29.000000000 +0000
@@ -26,12 +26,9 @@ the most recent LDAP RFCs (4510-4519, pl
   s.homepage = %q{http://github.com/ruby-ldap/ruby-net-ldap}
   s.rdoc_options = ["--main", "README.rdoc"]
   s.require_paths = ["lib"]
-  s.required_ruby_version = ">= 2.0.0"
+  s.required_ruby_version = ">= 3.0.0"
   s.summary = %q{Net::LDAP for Ruby (also called net-ldap) implements client access for the Lightweight Directory Access Protocol (LDAP), an IETF standard protocol for accessing distributed directory services}
 
-  s.add_development_dependency("flexmock", "~> 1.3")
-  s.add_development_dependency("rake", "~> 12.3.3")
-  s.add_development_dependency("rubocop", "~> 1.48")
-  s.add_development_dependency("test-unit", "~> 3.3")
-  s.add_development_dependency("byebug", "~> 9.0.6") unless RUBY_PLATFORM == "java"
+  s.add_dependency("base64")
+  s.add_dependency("ostruct")
 end
diff -pruN 0.19.0-1/test/integration/test_password_modify.rb 0.20.0-1/test/integration/test_password_modify.rb
--- 0.19.0-1/test/integration/test_password_modify.rb	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/test/integration/test_password_modify.rb	2025-08-22 13:07:29.000000000 +0000
@@ -1,6 +1,13 @@
 require_relative '../test_helper'
 
 class TestPasswordModifyIntegration < LDAPIntegrationTestCase
+  # see: https://www.rfc-editor.org/rfc/rfc3062#section-2
+  PASSWORD_MODIFY_SYNTAX = Net::BER.compile_syntax(
+    application: {},
+    universal: {},
+    context_specific: { primitive: { 0 => :string } },
+  )
+
   def setup
     super
     @admin_account = { dn: 'cn=admin,dc=example,dc=org', password: 'admin', method: :simple }
@@ -49,7 +56,13 @@ class TestPasswordModifyIntegration < LD
                                  auth: @auth,
                                  old_password: 'admin')
 
-    generated_password = @ldap.get_operation_result.extended_response[0][0]
+    passwd_modify_response_value = @ldap.get_operation_result.extended_response
+    seq = Net::BER::BerIdentifiedArray.new
+    sio = StringIO.new(passwd_modify_response_value)
+    until (e = sio.read_ber(PASSWORD_MODIFY_SYNTAX)).nil?
+      seq << e
+    end
+    generated_password = seq[0][0]
 
     assert generated_password, 'Should have generated a password'
 
@@ -64,8 +77,13 @@ class TestPasswordModifyIntegration < LD
     assert @ldap.password_modify(dn: @dn,
                                  auth: @auth)
 
-    generated_password = @ldap.get_operation_result.extended_response[0][0]
-
+    passwd_modify_response_value = @ldap.get_operation_result.extended_response
+    seq = Net::BER::BerIdentifiedArray.new
+    sio = StringIO.new(passwd_modify_response_value)
+    until (e = sio.read_ber(PASSWORD_MODIFY_SYNTAX)).nil?
+      seq << e
+    end
+    generated_password = seq[0][0]
     assert generated_password, 'Should have generated a password'
 
     refute @ldap.bind(username: @dn, password: 'admin', method: :simple),
diff -pruN 0.19.0-1/test/test_ldap_connection.rb 0.20.0-1/test/test_ldap_connection.rb
--- 0.19.0-1/test/test_ldap_connection.rb	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/test/test_ldap_connection.rb	2025-08-22 13:07:29.000000000 +0000
@@ -502,6 +502,40 @@ class TestLDAPConnectionInstrumentation
     assert unread.empty?, "should not have any leftover unread messages"
   end
 
+  def test_add_with_controls
+    dacl_flag = 0x4 # DACL_SECURITY_INFORMATION
+    control_values = [dacl_flag].map(&:to_ber).to_ber_sequence.to_s.to_ber
+    controls = []
+    # LDAP_SERVER_SD_FLAGS constant definition, taken from https://ldapwiki.com/wiki/LDAP_SERVER_SD_FLAGS_OID
+    ldap_server_sd_flags = '1.2.840.113556.1.4.801'.freeze
+    controls << [ldap_server_sd_flags.to_ber, true.to_ber, control_values].to_ber_sequence
+
+    ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""])
+    ber.ber_identifier = Net::LDAP::PDU::AddResponse
+    @tcp_socket.should_receive(:read_ber).and_return([1, ber])
+
+    result = @connection.add(:dn => "uid=added-user1,ou=People,dc=rubyldap,dc=com", :controls => controls)
+    assert result.success?, "should be success"
+    assert_equal "", result.error_message
+  end
+
+  def test_modify_with_controls
+    dacl_flag = 0x4 # DACL_SECURITY_INFORMATION
+    control_values = [dacl_flag].map(&:to_ber).to_ber_sequence.to_s.to_ber
+    controls = []
+    # LDAP_SERVER_SD_FLAGS constant definition, taken from https://ldapwiki.com/wiki/LDAP_SERVER_SD_FLAGS_OID
+    ldap_server_sd_flags = '1.2.840.113556.1.4.801'.freeze
+    controls << [ldap_server_sd_flags.to_ber, true.to_ber, control_values].to_ber_sequence
+
+    ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""])
+    ber.ber_identifier = Net::LDAP::PDU::ModifyResponse
+    @tcp_socket.should_receive(:read_ber).and_return([1, ber])
+
+    result = @connection.modify(:dn => "1", :operations => [[:replace, "mail", "something@sothsdkf.com"]], :controls => controls)
+    assert result.success?, "should be success"
+    assert_equal "", result.error_message
+  end
+
   def test_search_with_controls
     # search data
     search_data_ber = Net::BER::BerIdentifiedArray.new([1, [
@@ -540,4 +574,15 @@ class TestLDAPConnectionInstrumentation
     # ensure no unread
     assert unread.empty?, "should not have any leftover unread messages"
   end
+
+  def test_ldapwhoami
+    ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, '', '', 0, 'dn:uid=zerosteiner,ou=users,dc=example,dc=org'])
+    ber.ber_identifier = Net::LDAP::PDU::ExtendedResponse
+    response = [1, ber]
+
+    @tcp_socket.should_receive(:read_ber).and_return(response)
+
+    result = @connection.ldapwhoami
+    assert result.extended_response == 'dn:uid=zerosteiner,ou=users,dc=example,dc=org'
+  end
 end
diff -pruN 0.19.0-1/test/test_password.rb 0.20.0-1/test/test_password.rb
--- 0.19.0-1/test/test_password.rb	2024-01-03 17:06:46.000000000 +0000
+++ 0.20.0-1/test/test_password.rb	2025-08-22 13:07:29.000000000 +0000
@@ -12,4 +12,11 @@ class TestPassword < Test::Unit::TestCas
     flexmock(SecureRandom).should_receive(:random_bytes).and_return('\xE5\x8A\x99\xF8\xCB\x15GW\xE8\xEA\xAD\x0F\xBF\x95\xB0\xDC')
     assert_equal("{SSHA256}Cc7MXboTyUP5PnPAeJeCrgMy8+7Gus0sw7kBJuTrmf1ceEU1XHg4QVx4OTlceEY4XHhDQlx4MTVHV1x4RThceEVBXHhBRFx4MEZceEJGXHg5NVx4QjBceERD", Net::LDAP::Password.generate(:ssha256, "cashflow"))
   end
+
+  def test_utf8_psw
+    flexmock(SecureRandom).should_receive(:random_bytes).and_return('\xE5\x8A\x99\xF8\xCB\x15GW\xE8\xEA\xAD\x0F\xBF\x95\xB0\xDC')
+    utf8_psw = "iHVh©NjrLR§h!cru"
+    assert_equal("{SSHA}shzNiWgSPr3DoDm+Re7QPCcu1g1ceEU1XHg4QVx4OTlceEY4XHhDQlx4MTVHV1x4RThceEVBXHhBRFx4MEZceEJGXHg5NVx4QjBceERD", Net::LDAP::Password.generate(:ssha, utf8_psw))
+    assert_equal("{SSHA256}/aS06GodUyRYx+z436t+WZsH2aQCSac9FY4ewaXzhSNceEU1XHg4QVx4OTlceEY4XHhDQlx4MTVHV1x4RThceEVBXHhBRFx4MEZceEJGXHg5NVx4QjBceERD", Net::LDAP::Password.generate(:ssha256, utf8_psw))
+  end
 end
