Skip to content

Instantly share code, notes, and snippets.

@t2ym
Last active June 15, 2021 07:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save t2ym/9b80d6c41a99f3dee6136751b463e90d to your computer and use it in GitHub Desktop.
Save t2ym/9b80d6c41a99f3dee6136751b463e90d to your computer and use it in GitHub Desktop.
Git patch to nghttp2-v1.43.0 to add const SSL *nghttp2::asio_http2::server::request::ssl() to associate client certificates with requests
diff --git a/src/asio_server.cc b/src/asio_server.cc
index 74c92276..5a79061e 100644
--- a/src/asio_server.cc
+++ b/src/asio_server.cc
@@ -154,7 +154,7 @@ void server::start_accept(boost::asio::ssl::context &tls_context,
return;
}
- new_connection->start();
+ new_connection->start(new_connection->socket().native_handle());
});
}
diff --git a/src/asio_server_connection.h b/src/asio_server_connection.h
index daf9a664..c9570c04 100644
--- a/src/asio_server_connection.h
+++ b/src/asio_server_connection.h
@@ -85,12 +85,15 @@ public:
stopped_(false) {}
/// Start the first asynchronous operation for the connection.
- void start() {
+ void start(SSL *ssl = nullptr) {
boost::system::error_code ec;
handler_ = std::make_shared<http2_handler>(
GET_IO_SERVICE(socket_), socket_.lowest_layer().remote_endpoint(ec),
[this]() { do_write(); }, mux_);
+ if (ssl) {
+ handler_->ssl(ssl);
+ }
if (handler_->start() != 0) {
stop();
return;
diff --git a/src/asio_server_http2_handler.cc b/src/asio_server_http2_handler.cc
index c1fc195f..78186bd7 100644
--- a/src/asio_server_http2_handler.cc
+++ b/src/asio_server_http2_handler.cc
@@ -241,6 +241,7 @@ http2_handler::http2_handler(boost::asio::io_service &io_service,
mux_(mux),
io_service_(io_service),
remote_ep_(ep),
+ ssl_(nullptr),
session_(nullptr),
buf_(nullptr),
buflen_(0),
@@ -484,6 +485,14 @@ const boost::asio::ip::tcp::endpoint &http2_handler::remote_endpoint() {
return remote_ep_;
}
+const SSL *http2_handler::ssl() const {
+ return ssl_;
+}
+
+void http2_handler::ssl(SSL *ssl) {
+ ssl_ = ssl;
+}
+
callback_guard::callback_guard(http2_handler &h) : handler(h) {
handler.enter_callback();
}
diff --git a/src/asio_server_http2_handler.h b/src/asio_server_http2_handler.h
index 12064499..7bf5cf43 100644
--- a/src/asio_server_http2_handler.h
+++ b/src/asio_server_http2_handler.h
@@ -92,6 +92,9 @@ public:
const boost::asio::ip::tcp::endpoint &remote_endpoint();
+ const SSL *ssl() const;
+ void ssl(SSL *ssl);
+
const std::string &http_date();
template <size_t N>
@@ -156,6 +159,7 @@ private:
serve_mux &mux_;
boost::asio::io_service &io_service_;
boost::asio::ip::tcp::endpoint remote_ep_;
+ SSL *ssl_;
nghttp2_session *session_;
const uint8_t *buf_;
std::size_t buflen_;
diff --git a/src/asio_server_request.cc b/src/asio_server_request.cc
index 36669a52..8083a516 100644
--- a/src/asio_server_request.cc
+++ b/src/asio_server_request.cc
@@ -54,6 +54,10 @@ const boost::asio::ip::tcp::endpoint &request::remote_endpoint() const {
return impl_->remote_endpoint();
}
+const SSL *request::ssl() const {
+ return impl_->ssl();
+}
+
} // namespace server
} // namespace asio_http2
} // namespace nghttp2
diff --git a/src/asio_server_request_impl.cc b/src/asio_server_request_impl.cc
index 8442ad05..5cc6ab78 100644
--- a/src/asio_server_request_impl.cc
+++ b/src/asio_server_request_impl.cc
@@ -62,6 +62,14 @@ void request_impl::remote_endpoint(boost::asio::ip::tcp::endpoint ep) {
remote_ep_ = std::move(ep);
}
+const SSL *request_impl::ssl() const {
+ return ssl_;
+}
+
+void request_impl::ssl(const SSL *ssl) {
+ ssl_ = ssl;
+}
+
size_t request_impl::header_buffer_size() const { return header_buffer_size_; }
void request_impl::update_header_buffer_size(size_t len) {
diff --git a/src/asio_server_request_impl.h b/src/asio_server_request_impl.h
index 05de98a8..3f29299d 100644
--- a/src/asio_server_request_impl.h
+++ b/src/asio_server_request_impl.h
@@ -58,6 +58,9 @@ public:
const boost::asio::ip::tcp::endpoint &remote_endpoint() const;
void remote_endpoint(boost::asio::ip::tcp::endpoint ep);
+ const SSL *ssl() const;
+ void ssl(const SSL *ssl);
+
size_t header_buffer_size() const;
void update_header_buffer_size(size_t len);
@@ -68,6 +71,7 @@ private:
uri_ref uri_;
data_cb on_data_cb_;
boost::asio::ip::tcp::endpoint remote_ep_;
+ const SSL *ssl_;
size_t header_buffer_size_;
};
diff --git a/src/asio_server_stream.cc b/src/asio_server_stream.cc
index f763c1e0..05ba88e5 100644
--- a/src/asio_server_stream.cc
+++ b/src/asio_server_stream.cc
@@ -35,6 +35,7 @@ namespace server {
stream::stream(http2_handler *h, int32_t stream_id)
: handler_(h), stream_id_(stream_id) {
request_.impl().stream(this);
+ request_.impl().ssl(h->ssl());
response_.impl().stream(this);
}
diff --git a/src/includes/nghttp2/asio_http2_server.h b/src/includes/nghttp2/asio_http2_server.h
index d4ec489a..ae6cb65d 100644
--- a/src/includes/nghttp2/asio_http2_server.h
+++ b/src/includes/nghttp2/asio_http2_server.h
@@ -62,6 +62,8 @@ public:
// Returns the remote endpoint of the request
const boost::asio::ip::tcp::endpoint &remote_endpoint() const;
+ const SSL *ssl() const;
+
private:
std::unique_ptr<request_impl> impl_;
};
@t2ym
Copy link
Author

t2ym commented May 23, 2021

Extract specified properties from JSON by jq

ldapsearch -H ldaps://winserver.example.local -x -W -D "bind.user@example.local" -y passwdfile -b "dc=example,dc=local" \
'(&(objectCategory=person)(objectClass=user)(memberOf:1.2.840.113556.1.4.1941:=CN=WebAppX_Users,OU=SectionA,OU=DevDepartment,DC=example,DC=local))' |\
 awk -f ldif2json | jq -M '[.[] | {dn: .dn, mail: .mail, userCertificate: .userCertificate }] '
[
  {
    "dn": "CN=SecA User1,OU=SectionA,OU=DevDepartment,DC=example,DC=local",
    "mail": "seca.user1@example.local",
    "userCertificate": "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"
  },
  {
    "dn": "CN=SecA User2,OU=SectionA,OU=DevDepartment,DC=example,DC=local",
    "mail": "seca.user2@example.local",
    "userCertificate": "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"
  },
  {
    "dn": "CN=SecB User1,OU=SectionB,OU=DevDepartment,DC=example,DC=local",
    "mail": "secb.user1@example.local",
    "userCertificate": null
  },
  {
    "dn": "CN=SecB User2,OU=SectionB,OU=DevDepartment,DC=example,DC=local",
    "mail": "secb.user2@example.local",
    "userCertificate": null
  }
]

@t2ym
Copy link
Author

t2ym commented May 23, 2021

List SHA1 fingerprints for JSON userCertificate props

jq -nc '$ARGS.positional' --args `for i in \`ldapsearch -H ldaps://winserver.example.local -x -W -D "bind.user@example.local" -y passwdfile -b "dc=example,dc=local" '(&(objectCategory=person)(objectClass=user)(memberOf:1.2.840.113556.1.4.1941:=CN=WebAppX_Users,OU=SectionA,OU=DevDepartment,DC=example,DC=local))' |awk -f ldif2json  | jq '.[].userCertificate' | sed -e 's/^"\(.*\)"$/\1/' | sed -e 's/^null//' | egrep -e .\`; do { echo -n ${i} | base64 -d | sha1sum | cut -c1-40 | awk '{print toupper($0)}'; } done` | jq .
[
  "C94F88A3E4B9CC73D919F74620F821D28782B8AF",
  "020C1FCEA19A398DD4E544A3263E0A6EC6357CFD"
]

Powershell to get SHA1 hash list of userCertificate for group members who have the attribute

Get-ADGroupMember WebAppX_Users  -recursive |
Get-ADUser -Properties userCertificate |
Select userCertificate |
ForEach-Object { if ($_.userCertificate) {
Get-FileHash -Algorithm SHA1 -InputStream (new-object System.IO.MemoryStream(,$_.userCertificate[0]))} } |
ForEach-Object { $_.Hash } | ConvertTo-Json
[
    "020C1FCEA19A398DD4E544A3263E0A6EC6357CFD",
    "C94F88A3E4B9CC73D919F74620F821D28782B8AF"
]

@t2ym
Copy link
Author

t2ym commented May 24, 2021

Get Fingerprints for Roles mapped from AD group and subgroups

  • Pseudo-role "*" to show the root group DN
  • Other meta-infomration such as timestamp may be useful
$group = "CN=WebAppX_Users,OU=SectionA,OU=DevDepartment,DC=example,DC=local"; $outfile = "roles.json"; $jsonBase = @{"*"=$group}; Get-ADGroupMember $group |
Select distinguishedName |
ForEach-Object { $jsonBase.Add(($_.distinguishedName -replace "CN=([^,]*),.*", "`$1"), 
@(,(Get-ADGroupMember $_.distinguishedName -recursive |
Get-ADUser -Properties userCertificate |
Select userCertificate | 
ForEach-Object { if ($_.userCertificate) {
Get-FileHash -Algorithm SHA1 -InputStream (new-object System.IO.MemoryStream(,$_.userCertificate[0]))} } |
ForEach-Object { $_.Hash }) |
ForEach-Object { $_ })); }; $jsonBase | ConvertTo-Json | Out-File $outfile; Get-Content $outfile;
{
    "Admin":  [
                  "C94F88A3E4B9CC73D919F74620F821D28782B8AF"
              ],
    "*":  "CN=WebAppX_Users,OU=SectionA,OU=DevDepartment,DC=example,DC=local",
    "User":  [
                 "C94F88A3E4B9CC73D919F74620F821D28782B8AF",
                 "020C1FCEA19A398DD4E544A3263E0A6EC6357CFD",
                 "E92C3F99D1ADF24B4895C4A7C96214056CEDF23F",
                 "DEC457FBC9C1828B7654ED36343E5AD85008E99C",
                 "BD639B17FAFA564DF2F7E8729781A38E9B4A6024"
             ]
}

On UN*X-like systems

  • LDAP configuration at ~/.ldaprc
TLS_CACERT {PATH_TO_TRUSTED_CA_FILE_IN_CONCATENATED_PEM}
get_roles() { local GROUP_DN="$1";local BIND_USER="$2";local BIND="$3";local SERVERURI="$4";local PASSWDFILE="$5";local LDIF2JSON="$6"; local OUTFILE="$7";\
  get_fingerprints () { local SUBGROUP_DN=$1; jq -nc '$ARGS.positional' \
    --args `for i in \`ldapsearch -H ${SERVERURI} -x -W -D ${BIND_USER} -y passwdfile -b ${BIND} \
    "(&(objectCategory=person)(objectClass=user)(memberOf:1.2.840.113556.1.4.1941:=${SUBGROUP_DN}))" |\
    awk -f ${LDIF2JSON} | jq '.[].userCertificate' | sed -e 's/^"\(.*\)"$/\1/' | sed -e 's/^null//' | egrep -e .\`; \
    do { echo -n ${i} | base64 -d | sha1sum | cut -c1-40 | awk '{print toupper($0)}'; } done` | jq .;
  }; \
  echo \{ `ldapsearch -H ${SERVERURI} -x -W -D "${BIND_USER}" -y ${PASSWDFILE} -b "${BIND}" "(distinguishedName=${GROUP_DN})" | awk -f ${LDIF2JSON} | jq .[].member | jq -r .[] |\
  while read line; do { echo \"\`echo -n \"${line}\" | sed -e 's/^"CN=\([^,]*\),.*$/\\1/'\`\": \`get_fingerprints ${line}\`, ; } done` \} \{\"*\": \"${GROUP_DN}\" \} |\
  sed -e 's/, *}/}/' | jq -s add >${OUTFILE}; \
  cat ${OUTFILE}; unset get_fingerprints; unset get_roles;\
}; \
get_roles "CN=WebAppX_Users,OU=SectionA,OU=DevDepartment,DC=example,DC=local" "bind.user@example.local" "dc=example,dc=local" "ldaps://winserver.example.local" "./passwdfile" "./ldif2json" "./roles.json"
{
  "User": [
    "C94F88A3E4B9CC73D919F74620F821D28782B8AF",
    "020C1FCEA19A398DD4E544A3263E0A6EC6357CFD",
    "DEC457FBC9C1828B7654ED36343E5AD85008E99C",
    "BD639B17FAFA564DF2F7E8729781A38E9B4A6024",
    "E92C3F99D1ADF24B4895C4A7C96214056CEDF23F"
  ],
  "Admin": [
    "C94F88A3E4B9CC73D919F74620F821D28782B8AF"
  ],
  "*": "CN=WebAppX_Users,OU=SectionA,OU=DevDepartment,DC=example,DC=local"
}

Group and User configurations in the above scripts

  • Subgroups mapped to roles can be located in any LDAP paths including outside of the container OU to avoid role name conflicts with other web applications
function Get-ADTree {
  param( $group )
  
  if ($group.GetType().FullName -eq "System.String") {
    Get-ADTree (Get-ADGroup $group);
  }
  elseif ($group.ObjectClass -eq "group") {
    @{$group.distinguishedName=(Get-ADGroupMember $group | ForEach-Object { Get-ADTree $_ })}
  }
  else {
    $group.distinguishedName
  }
}
Get-ADTree "CN=WebAppX_Users,OU=SectionA,OU=DevDepartment,DC=example,DC=local" | ConvertTo-Json -Depth 5
  • Output reformatted with jq .
  • To show a single element array, ConvertTo-Json -Depth 5 -AsArray should be used in PowerShell 7.1
{
  "CN=WebAppX_Users,OU=SectionA,OU=DevDepartment,DC=example,DC=local": [
    {
      "CN=Admin,OU=SectionA,OU=DevDepartment,DC=example,DC=local": "CN=SecA User1,OU=SectionA,OU=DevDepartment,DC=example,DC=local"
    },
    {
      "CN=User,OU=SectionA,OU=DevDepartment,DC=example,DC=local": [
        {
          "CN=SecA_Users,OU=SectionA,OU=DevDepartment,DC=example,DC=local": [
            "CN=SecA User1,OU=SectionA,OU=DevDepartment,DC=example,DC=local",
            "CN=SecA User2,OU=SectionA,OU=DevDepartment,DC=example,DC=local",
            "CN=SecA User3,OU=SectionA,OU=DevDepartment,DC=example,DC=local"
          ]
        },
        {
          "CN=SecB_Users,OU=SectionB,OU=DevDepartment,DC=example,DC=local": [
            "CN=SecB User1,OU=SectionB,OU=DevDepartment,DC=example,DC=local",
            "CN=SecB User2,OU=SectionB,OU=DevDepartment,DC=example,DC=local"
          ]
        }
      ]
    }
  ]
}

@t2ym
Copy link
Author

t2ym commented May 25, 2021

Map groups to roles generated from AD groups and subgroups

  • If only groups of which a user is a direct member is known, the user's roles can be determined by this map from groups to roles
  • Searching only for groups should be faster than exhaustive search for userCertificate attributes
  • userCertificate attributes are not populated until users sign in to their computers
    • Web apps for all users should be able to authenticate and authorize such users at their first sign-in operations, where intranet web services must be of their focus
      • Retrieval of their group membership attributes from Active Directory should be done just on their first access to web apps without delay
    • For intranet web apps that are expected to be registered on requests, all the expected userCertificate attributes should be existent before their access attempts. Fingerprints can be pre-fetched for such applications
  • Option # 1: Direct groups can be fetched from Active Directory on each user access to the target web app
  • Option # 2: Find a way to fetch all the fingerprints of target users of relevant groups in a reasonable query load on Active Directory
  • Option # 3: Hybrid solution with fingerprints mapped to roles for normal users and roles from Active Directory groups on demand as a fallback to achieve complete service levels
    1. Authenticate on verify callback via trusted certificate issuer(s)
    2. Authorize on request middleware via fingerprints assigned to role(s)
    3. Authorize as a fallback on request middleware via Active Directory group membership mapped to role(s)
    • Prototyping LDAP accessor class: ldap_member_of.cc
      • Basic synchronous search for memberOf attributes
      • Primitive fallback mechanism for synchronous search
      • Basic asynchronous search for memberOf attributes
        • Note: In .net core, polling is used for async LDAP operations as callback interface is unavailable in OpenLDAP

Underlying native libraries don't support callback-based function, so we will instead use polling and use a Stopwatch to track the timeout manually.

  • dummy item for indentation
    • dummy item for indentation
      • Fallback mechanism for asynchronous search
  • Caching of roles derived from group membership in Active Directory is essential for performance
  • On Web Apps, it may NOT be trivial to map a user's subject in user certificate to Active Directory distinguishedName
    • If emailAddress is included in the subject, it should be trivial
    • Is this simple conversion feasible? subject: CN=${Name},OU... == distinguishedName
  • Format-Json came from https://stackoverflow.com/questions/56322993/proper-formating-of-json-using-powershell/56324939
function Get-ADGroupRoles {
  param( $group, $roles, $result, $root )

  if ($group.GetType().FullName -eq "System.String") {
    [void]($result = @{});
    [void]($roles = @());
    [void]($root = Get-ADGroup $group);
    [void](Get-ADGroupRoles $root $roles $result $root);
    $result
  }
  else {
    if ($root.distinguishedName -ne $group.distinguishedName) {
      if ($roles.Count -eq 0) {
        [void]($private:roles = @(($group.distinguishedName -replace "CN=([^,]*),.*", "`$1")));
        [void]($result.add($group.distinguishedName,$private:roles.psobject.copy()));
      }
      else {
        [void]($private:existingRoles = $result[$group.distinguishedName]);
        if ($private:existingRoles) {
          [void]($roles | ForEach-Object { $private:existingRoles += $_; });
          [void]($result[$group.distinguishedName] = $private:existingRoles);
        }
        else {
          [void]($result.add($group.distinguishedName, $roles.psobject.copy()));
        }
        [void]($private:roles = $roles.psobject.copy());
      }
    }
    Get-ADGroupMember $group |
    Where-Object { $_.objectClass -eq "group" } |
    ForEach-Object { Get-ADGroupRoles $_ $private:roles $result $root }
  }
}
Get-ADGroupRoles WebAppX_Users  | ConvertTo-Json -Depth 5 | Format-Json
{
    "CN=Interim_Group3,OU=SectionA,OU=DevDepartment,DC=example,DC=local": [
        "Admin",
        "User"
    ],
    "CN=Admin,OU=SectionA,OU=DevDepartment,DC=example,DC=local": [
        "Admin"
    ],
    "CN=Interim_Group2,OU=SectionA,OU=DevDepartment,DC=example,DC=local": [
        "User"
    ],
    "CN=User,OU=SectionA,OU=DevDepartment,DC=example,DC=local": [
        "User"
    ],
    "CN=Interim_Group1,OU=SectionA,OU=DevDepartment,DC=example,DC=local": [
        "User"
    ],
    "CN=SecB_Users,OU=SectionB,OU=DevDepartment,DC=example,DC=local": [
        "User"
    ],
    "CN=SecA_Users,OU=SectionA,OU=DevDepartment,DC=example,DC=local": [
        "User"
    ]
}

Group and User configurations in the above script

function Get-ADTree {
  param( $group )
  
  if ($group.GetType().FullName -eq "System.String") {
    Get-ADTree (Get-ADGroup $group);
  }
  elseif ($group.ObjectClass -eq "group") {
    @{$group.distinguishedName=(Get-ADGroupMember $group | ForEach-Object { Get-ADTree $_ })}
  }
  else {
    $group.distinguishedName
  }
}
Get-ADTree WebAppX_Users  | ConvertTo-Json -Depth 7 | Format-Json
{
    "CN=WebAppX_Users,OU=SectionA,OU=DevDepartment,DC=example,DC=local": [
        {
            "CN=Admin,OU=SectionA,OU=DevDepartment,DC=example,DC=local": [
                "CN=SecA User1,OU=SectionA,OU=DevDepartment,DC=example,DC=local",
                {
                    "CN=Interim_Group3,OU=SectionA,OU=DevDepartment,DC=example,DC=local": [
                        "CN=SecA User4,OU=SectionA,OU=DevDepartment,DC=example,DC=local",
                        "CN=SecA User5,OU=SectionA,OU=DevDepartment,DC=example,DC=local"
                    ]
                }
            ]
        },
        {
            "CN=User,OU=SectionA,OU=DevDepartment,DC=example,DC=local": [
                {
                    "CN=SecA_Users,OU=SectionA,OU=DevDepartment,DC=example,DC=local": [
                        "CN=SecA User1,OU=SectionA,OU=DevDepartment,DC=example,DC=local",
                        "CN=SecA User2,OU=SectionA,OU=DevDepartment,DC=example,DC=local",
                        "CN=SecA User3,OU=SectionA,OU=DevDepartment,DC=example,DC=local",
                        "CN=SecA User4,OU=SectionA,OU=DevDepartment,DC=example,DC=local",
                        "CN=SecA User5,OU=SectionA,OU=DevDepartment,DC=example,DC=local"
                    ]
                },
                {
                    "CN=SecB_Users,OU=SectionB,OU=DevDepartment,DC=example,DC=local": [
                        "CN=SecB User1,OU=SectionB,OU=DevDepartment,DC=example,DC=local",
                        "CN=SecB User2,OU=SectionB,OU=DevDepartment,DC=example,DC=local"
                    ]
                },
                {
                    "CN=Interim_Group1,OU=SectionA,OU=DevDepartment,DC=example,DC=local": {
                        "CN=Interim_Group2,OU=SectionA,OU=DevDepartment,DC=example,DC=local": {
                            "CN=Interim_Group3,OU=SectionA,OU=DevDepartment,DC=example,DC=local": [
                                "CN=SecA User4,OU=SectionA,OU=DevDepartment,DC=example,DC=local",
                                "CN=SecA User5,OU=SectionA,OU=DevDepartment,DC=example,DC=local"
                            ]
                        }
                    }
                }
            ]
        }
    ]
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment