[pecl-networking-ssh2] master: Add SHA-256 and raw host key inspection
| From: | Raj Siva-Rajah via Casper Langemeijer | Date: | Wed, 14 Oct 2026 19:55:46 +0000 |
| Subject: | [pecl-networking-ssh2] master: Add SHA-256 and raw host key inspection | ||
| Groups: | php.pecl.cvs | ||
| Request: | Send a blank email to pecl-cvs+get-28697@lists.php.net to get a copy of this message | ||
Author: Raj Siva-Rajah (binaryfire)
Committer: Casper Langemeijer (langemeijer)
Date: 2026-10-14T21:55:43+02:00
Commit: https://github.com/php/pecl-networking-ssh2/commit/7a01397f8844a43d8b849db036bac156343a2a72
Raw diff: https://github.com/php/pecl-networking-ssh2/commit/7a01397f8844a43d8b849db036bac156343a2a72.diff
Add SHA-256 and raw host key inspection
Raise the libssh2 build floor to 1.9.0 so every successful build exposes the complete host-key API,
and fail configuration clearly when headers and the linked library do not match.\n\nAdd SHA-256
fingerprint selection, raw negotiated host-key retrieval, and constants for the supported host-key
types. Preserve the existing MD5 default and SHA-1 behavior.\n\nCover the new surface with an
algorithm-independent PHPT that verifies the raw key and SHA-256 fingerprint describe the same
negotiated server key.
Changed paths:
A tests/ssh2_hostkey.phpt
M config.m4
M package.xml
M php_ssh2.h
M ssh2.c
Diff:
diff --git a/config.m4 b/config.m4
index 18b8caa..87b43d8 100644
--- a/config.m4
+++ b/config.m4
@@ -25,12 +25,27 @@ if test "$PHP_SSH2" != "no"; then
PHP_ADD_INCLUDE($SSH2_DIR/include)
+ saved_CPPFLAGS=$CPPFLAGS
+ CPPFLAGS="-I$SSH2_DIR/include $CPPFLAGS"
+ AC_MSG_CHECKING([for libssh2 version >= 1.9.0])
+ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
+ #include <libssh2.h>
+ #if LIBSSH2_VERSION_NUM < 0x010900
+ #error libssh2 1.9.0 or newer is required
+ #endif
+ ]], [[]])], [
+ AC_MSG_RESULT([yes])
+ ], [
+ AC_MSG_ERROR([libssh2 version >= 1.9.0 not found])
+ ])
+ CPPFLAGS=$saved_CPPFLAGS
+
PHP_CHECK_LIBRARY(ssh2,libssh2_session_hostkey,
[
PHP_ADD_LIBRARY_WITH_PATH(ssh2, $SSH2_DIR/lib, SSH2_SHARED_LIBADD)
AC_DEFINE(HAVE_SSH2LIB,1,[Have libssh2])
],[
- AC_MSG_ERROR([libssh2 version >= 1.2 not found])
+ AC_MSG_ERROR([libssh2 library not found or too old; version 1.9.0 or newer is required])
],[
-L$SSH2_DIR/lib -lm
])
@@ -39,7 +54,7 @@ if test "$PHP_SSH2" != "no"; then
[
AC_DEFINE(PHP_SSH2_AGENT_AUTH, 1, [Have libssh2 with ssh-agent support])
],[
- AC_MSG_WARN([libssh2 <= 1.2.3, ssh-agent subsystem support not enabled])
+ AC_MSG_ERROR([libssh2_agent_init not found; ensure the linked libssh2 matches the 1.9.0 or
newer headers])
],[
-L$SSH2_DIR/lib -lm
])
@@ -48,7 +63,7 @@ if test "$PHP_SSH2" != "no"; then
[
AC_DEFINE(PHP_SSH2_SESSION_TIMEOUT, 1, [Have libssh2 with session timeout support])
],[
- AC_MSG_WARN([libssh2 < 1.2.9, session timeout support not enabled])
+ AC_MSG_ERROR([libssh2_session_set_timeout not found; ensure the linked libssh2 matches the
1.9.0 or newer headers])
],[
-L$SSH2_DIR/lib -lm
])
@@ -57,7 +72,7 @@ if test "$PHP_SSH2" != "no"; then
[
AC_DEFINE(PHP_SSH2_KEEPALIVE, 1, [Have libssh2 with keepalive support])
],[
- AC_MSG_WARN([libssh2 keepalive support not available])
+ AC_MSG_ERROR([libssh2_keepalive_config not found; ensure the linked libssh2 matches the 1.9.0
or newer headers])
],[
-L$SSH2_DIR/lib -lm
])
@@ -66,7 +81,7 @@ if test "$PHP_SSH2" != "no"; then
[
AC_DEFINE(PHP_SSH2_CHANNEL_SIGNAL, 1, [Have libssh2 with channel signal support])
],[
- AC_MSG_WARN([libssh2 < 1.9.0, channel signal support not available])
+ AC_MSG_ERROR([libssh2_channel_signal_ex not found; ensure the linked libssh2 matches the 1.9.0
or newer headers])
],[
-L$SSH2_DIR/lib -lm
])
diff --git a/package.xml b/package.xml
index 07c0427..97a9209 100644
--- a/package.xml
+++ b/package.xml
@@ -75,6 +75,7 @@
<file role="test" name="ssh2_auth_pubkey_file.phpt"/>
<file role="test" name="ssh2_connect.phpt"/>
<file role="test" name="ssh2_exec.phpt"/>
+ <file role="test" name="ssh2_hostkey.phpt"/>
<file role="test" name="ssh2_keepalive.phpt"/>
<file role="test" name="ssh2_poll.phpt"/>
<file role="test" name="ssh2_send_eof.phpt"/>
diff --git a/php_ssh2.h b/php_ssh2.h
index 7c9cbcb..34e2185 100644
--- a/php_ssh2.h
+++ b/php_ssh2.h
@@ -24,6 +24,10 @@
#include "ext/standard/url.h"
#include "main/php_network.h"
+#if LIBSSH2_VERSION_NUM < 0x010900
+#error "The ssh2 extension requires libssh2 1.9.0 or newer"
+#endif
+
#define PHP_SSH2_VERSION "1.5.0"
#define PHP_SSH2_DEFAULT_PORT 22
@@ -32,6 +36,7 @@
#define PHP_SSH2_FINGERPRINT_SHA1 0x0001
#define PHP_SSH2_FINGERPRINT_HEX 0x0000
#define PHP_SSH2_FINGERPRINT_RAW 0x0002
+#define PHP_SSH2_FINGERPRINT_SHA256 0x0004
#define PHP_SSH2_TERM_UNIT_CHARS 0x0000
#define PHP_SSH2_TERM_UNIT_PIXELS 0x0001
diff --git a/ssh2.c b/ssh2.c
index b685b21..97c34e6 100644
--- a/ssh2.c
+++ b/ssh2.c
@@ -40,6 +40,10 @@
#define MD5_DIGEST_LENGTH 16
#endif
+#ifndef SHA256_DIGEST_LENGTH
+#define SHA256_DIGEST_LENGTH 32
+#endif
+
/* True global resources - no need for thread safety here */
int le_ssh2_session;
int le_ssh2_listener;
@@ -592,18 +596,28 @@ PHP_FUNCTION(ssh2_fingerprint)
zval *zsession;
const char *fingerprint;
zend_long flags = 0;
- int i, fingerprint_len;
+ size_t i, fingerprint_len;
+ int hash_type;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "r|l", &zsession, &flags) == FAILURE)
{
return;
}
- fingerprint_len = (flags & PHP_SSH2_FINGERPRINT_SHA1) ? SHA_DIGEST_LENGTH : MD5_DIGEST_LENGTH;
+ if (flags & PHP_SSH2_FINGERPRINT_SHA256) {
+ fingerprint_len = SHA256_DIGEST_LENGTH;
+ hash_type = LIBSSH2_HOSTKEY_HASH_SHA256;
+ } else if (flags & PHP_SSH2_FINGERPRINT_SHA1) {
+ fingerprint_len = SHA_DIGEST_LENGTH;
+ hash_type = LIBSSH2_HOSTKEY_HASH_SHA1;
+ } else {
+ fingerprint_len = MD5_DIGEST_LENGTH;
+ hash_type = LIBSSH2_HOSTKEY_HASH_MD5;
+ }
if ((session = (LIBSSH2_SESSION *)zend_fetch_resource(Z_RES_P(zsession),
PHP_SSH2_SESSION_RES_NAME, le_ssh2_session)) == NULL) {
RETURN_FALSE;
}
- fingerprint = (char*)libssh2_hostkey_hash(session, (flags & PHP_SSH2_FINGERPRINT_SHA1) ?
LIBSSH2_HOSTKEY_HASH_SHA1 : LIBSSH2_HOSTKEY_HASH_MD5);
+ fingerprint = (char*)libssh2_hostkey_hash(session, hash_type);
if (!fingerprint) {
php_error_docref(NULL, E_WARNING, "Unable to retrieve fingerprint from specified
session");
RETURN_FALSE;
@@ -632,6 +646,36 @@ PHP_FUNCTION(ssh2_fingerprint)
}
/* }}} */
+/* {{{ proto array ssh2_hostkey(resource session)
+ * Returns the server's raw host key and type
+ */
+PHP_FUNCTION(ssh2_hostkey)
+{
+ LIBSSH2_SESSION *session;
+ zval *zsession;
+ const char *hostkey;
+ size_t hostkey_len;
+ int type;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "r", &zsession) == FAILURE) {
+ return;
+ }
+
+ if ((session = (LIBSSH2_SESSION *)zend_fetch_resource(Z_RES_P(zsession),
PHP_SSH2_SESSION_RES_NAME, le_ssh2_session)) == NULL) {
+ RETURN_FALSE;
+ }
+
+ hostkey = libssh2_session_hostkey(session, &hostkey_len, &type);
+ if (!hostkey) {
+ RETURN_FALSE;
+ }
+
+ array_init(return_value);
+ add_assoc_stringl(return_value, "key", (char *)hostkey, hostkey_len);
+ add_assoc_long(return_value, "type", type);
+}
+/* }}} */
+
/* {{{ proto array ssh2_auth_none(resource session, string username)
* Attempt "none" authentication, returns a list of allowed methods on failed
authentication,
* false on utter failure, or true on success
@@ -1460,6 +1504,15 @@ PHP_MINIT_FUNCTION(ssh2)
REGISTER_LONG_CONSTANT("SSH2_FINGERPRINT_SHA1", PHP_SSH2_FINGERPRINT_SHA1, CONST_CS |
CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SSH2_FINGERPRINT_HEX", PHP_SSH2_FINGERPRINT_HEX, CONST_CS |
CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SSH2_FINGERPRINT_RAW", PHP_SSH2_FINGERPRINT_RAW, CONST_CS |
CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("SSH2_FINGERPRINT_SHA256", PHP_SSH2_FINGERPRINT_SHA256, CONST_CS
| CONST_PERSISTENT);
+
+ REGISTER_LONG_CONSTANT("SSH2_HOSTKEY_TYPE_UNKNOWN", LIBSSH2_HOSTKEY_TYPE_UNKNOWN, CONST_CS
| CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("SSH2_HOSTKEY_TYPE_RSA", LIBSSH2_HOSTKEY_TYPE_RSA, CONST_CS |
CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("SSH2_HOSTKEY_TYPE_DSS", LIBSSH2_HOSTKEY_TYPE_DSS, CONST_CS |
CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("SSH2_HOSTKEY_TYPE_ECDSA_256", LIBSSH2_HOSTKEY_TYPE_ECDSA_256, CONST_CS
| CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("SSH2_HOSTKEY_TYPE_ECDSA_384", LIBSSH2_HOSTKEY_TYPE_ECDSA_384, CONST_CS
| CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("SSH2_HOSTKEY_TYPE_ECDSA_521", LIBSSH2_HOSTKEY_TYPE_ECDSA_521, CONST_CS
| CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("SSH2_HOSTKEY_TYPE_ED25519", LIBSSH2_HOSTKEY_TYPE_ED25519, CONST_CS
| CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SSH2_TERM_UNIT_CHARS", PHP_SSH2_TERM_UNIT_CHARS, CONST_CS |
CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SSH2_TERM_UNIT_PIXELS", PHP_SSH2_TERM_UNIT_PIXELS, CONST_CS |
CONST_PERSISTENT);
@@ -1556,6 +1609,10 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_ssh2_fingerprint, 0, 0, 1)
ZEND_ARG_INFO(0, flags)
ZEND_END_ARG_INFO()
+ZEND_BEGIN_ARG_INFO(arginfo_ssh2_hostkey, 1)
+ ZEND_ARG_INFO(0, session)
+ZEND_END_ARG_INFO()
+
ZEND_BEGIN_ARG_INFO(arginfo_ssh2_auth_none, 2)
ZEND_ARG_INFO(0, session)
ZEND_ARG_INFO(0, username)
@@ -1770,6 +1827,7 @@ zend_function_entry ssh2_functions[] = {
#endif
PHP_FE(ssh2_methods_negotiated, arginfo_ssh2_methods_negotiated)
PHP_FE(ssh2_fingerprint, arginfo_ssh2_fingerprint)
+ PHP_FE(ssh2_hostkey, arginfo_ssh2_hostkey)
PHP_FE(ssh2_auth_none, arginfo_ssh2_auth_none)
PHP_FE(ssh2_auth_password, arginfo_ssh2_auth_password)
diff --git a/tests/ssh2_hostkey.phpt b/tests/ssh2_hostkey.phpt
new file mode 100644
index 0000000..51afefd
--- /dev/null
+++ b/tests/ssh2_hostkey.phpt
@@ -0,0 +1,28 @@
+--TEST--
+Raw host-key metadata and SHA-256 fingerprints describe the same server key
+--SKIPIF--
+<?php require('ssh2_skip.inc'); ?>
+--FILE--
+<?php
+require('ssh2_test.inc');
+
+$ssh = ssh2_connect(TEST_SSH2_HOSTNAME, TEST_SSH2_PORT);
+$hostkey = ssh2_hostkey($ssh);
+$fingerprint = ssh2_fingerprint($ssh, SSH2_FINGERPRINT_SHA256 | SSH2_FINGERPRINT_RAW);
+
+var_dump(is_array($hostkey));
+var_dump(is_string($hostkey['key']));
+var_dump($hostkey['key'] !== '');
+var_dump(is_int($hostkey['type']));
+var_dump($hostkey['type'] !== SSH2_HOSTKEY_TYPE_UNKNOWN);
+var_dump(strlen($fingerprint));
+var_dump($fingerprint === hash('sha256', $hostkey['key'], true));
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+int(32)
+bool(true)