Control: tags -1 patch
Please note that I am going to upload a new upload version to DELAYED/10 in order to fix this. Please find the debdiff with the changes attached.
diff -Nru hash-slinger-3.1/CHANGES hash-slinger-3.5/CHANGES --- hash-slinger-3.1/CHANGES 2021-01-14 11:10:30.000000000 +0100 +++ hash-slinger-3.5/CHANGES 2026-01-26 17:44:59.000000000 +0100 @@ -1,4 +1,25 @@ -v3.1 (January 14, 2020) +v3.5 (January, 26, 2026) +- Replace the M2Crypto library with the Python cryptography library and standard ssl module for TLS/SSL handling [Bastian Germann] +- Use cryptography.x509 for X.509 certificate parsing and manipulation [Bastian Germann] +- Use standard ssl module for TLS connections instead of M2Crypto.SSL [Bastian Germann] +- Replace sslStartTLSConnect() with do_starttls_handshake() that works with plain sockets before SSL wrapping [Bastian Germann] +- Update getHash() to accept use_pubkey parameter instead of accepting both certificate and public key objects [Bastian Germann] + +v3.4 (April, 14, 2025) +- minor cleanup + +v3.3 (June, 6, 2023) +- tlsa: Add /usr/share/dns/root.key to cauldron [Vijay Sarvepalli] +- tlsa: Give warning on TLSA create routine for certificate name mismatch to hostname provided [Vijay Sarvepalli] +- openpgp: Fix crash when exporting generic format [Dirk Stöcker] + +v3.2 (May, 4, 2022) +- openpgpkey: fix fetch option [Dirk Stöcker] +- sshfp: fix UTF-8 encoding issue [Dirk Stöcker] +- sshfp: fix handling when xmss is not supported [Dirk Stöcker] +- sshfp: don't warn for default known hosts file and option scan [Dirk Stöcker] + +v3.1 (January 14, 2021) - tlsa: bug fixes [Peter van Dijk, Dirk Stöcker] - tlsa: separate duplicate output due to multiple IP addresses [Dirk Stöcker] - openpgpkey: update docs [Arsen Stasic] diff -Nru hash-slinger-3.1/debian/changelog hash-slinger-3.5/debian/changelog --- hash-slinger-3.1/debian/changelog 2023-10-05 16:37:58.000000000 +0200 +++ hash-slinger-3.5/debian/changelog 2026-01-26 20:53:48.000000000 +0100 @@ -1,3 +1,11 @@ +hash-slinger (3.5-0.1) unstable; urgency=medium + + * Non-maintainer upload + * Import new upstream version (Closes: #1126463, #1118256) + * d/copyright: correct Source + + -- Bastian Germann <[email protected]> Mon, 26 Jan 2026 20:53:48 +0100 + hash-slinger (3.1-1.2) unstable; urgency=medium * Non-maintainer upload. diff -Nru hash-slinger-3.1/debian/control hash-slinger-3.5/debian/control --- hash-slinger-3.1/debian/control 2022-02-10 07:03:46.000000000 +0100 +++ hash-slinger-3.5/debian/control 2026-01-26 20:53:48.000000000 +0100 @@ -19,7 +19,7 @@ openssh-client, python3-dnspython, python3-gnupg (>= 0.3.7), - python3-m2crypto (>= 0.24.0), + python3-cryptography, python3-unbound, ${misc:Depends}, ${python3:Depends}, diff -Nru hash-slinger-3.1/debian/copyright hash-slinger-3.5/debian/copyright --- hash-slinger-3.1/debian/copyright 2021-02-14 17:40:02.000000000 +0100 +++ hash-slinger-3.5/debian/copyright 2026-01-26 20:53:48.000000000 +0100 @@ -1,9 +1,19 @@ Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: hash-slinger -Source: http://people.redhat.com/pwouters/hash-slinger/ +Source: https://github.com/letoams/hash-slinger Files: * -Copyright: 2013, 2014 Paul Wouters <[email protected]> +Copyright: 2013-2022 Paul Wouters <[email protected]> +License: GPL-2+ + +Files: sshfp +Copyright: + Copyright 2006-2010 by Xelerance http://www.xelerance.com/ (Paul Wouters) + Copyright 2012 Paul Wouters <[email protected]> + Copyright 2014 Gerald Turner <[email protected]> + Copyright 2015 Jean-Michel Nirgal Vourgere <[email protected]> + Copyright 2015 Dirk Stoecker <[email protected]> + Copyright 2019 Kishan Takoordyal <[email protected]> License: GPL-2+ Files: tlsa diff -Nru hash-slinger-3.1/debian/patches/0001-Debian-default-root.key-resides-in-usr-share-dns-roo.patch hash-slinger-3.5/debian/patches/0001-Debian-default-root.key-resides-in-usr-share-dns-roo.patch --- hash-slinger-3.1/debian/patches/0001-Debian-default-root.key-resides-in-usr-share-dns-roo.patch 2021-02-14 17:40:02.000000000 +0100 +++ hash-slinger-3.5/debian/patches/0001-Debian-default-root.key-resides-in-usr-share-dns-roo.patch 2026-01-26 20:53:48.000000000 +0100 @@ -3,9 +3,6 @@ Subject: Debian default root.key resides in /usr/share/dns/root.key --- - openpgpkey | 4 ++-- - tlsa | 2 +- - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpgpkey b/openpgpkey index e423817..d3b80ef 100755 @@ -29,16 +26,3 @@ for root in cauldron: if os.path.isfile(root): rootanchor=root -diff --git a/tlsa b/tlsa -index ae925ea..1c6c949 100755 ---- a/tlsa -+++ b/tlsa -@@ -37,7 +37,7 @@ from hashlib import sha256, sha512 - from ipaddress import IPv4Address, IPv6Address - - ROOTKEY="none" --cauldron = ( "/var/lib/unbound/root.anchor", "/var/lib/unbound/root.key", "/etc/unbound/root.key" ) -+cauldron = ( "/usr/share/dns/root.key", "/var/lib/unbound/root.anchor", "/var/lib/unbound/root.key", "/etc/unbound/root.key" ) - for root in cauldron: - if os.path.isfile(root): - ROOTKEY=root diff -Nru hash-slinger-3.1/debian/patches/0001-fix-generic-TLSA-record-generation.patch hash-slinger-3.5/debian/patches/0001-fix-generic-TLSA-record-generation.patch --- hash-slinger-3.1/debian/patches/0001-fix-generic-TLSA-record-generation.patch 2023-10-05 16:36:07.000000000 +0200 +++ hash-slinger-3.5/debian/patches/0001-fix-generic-TLSA-record-generation.patch 1970-01-01 01:00:00.000000000 +0100 @@ -1,34 +0,0 @@ -From e3bec6e2a6b1bda7c52b4c585474fd7cc23ab643 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Charaoui?= <[email protected]> -Date: Wed, 4 Oct 2023 22:05:26 -0400 -Subject: [PATCH] fix generic TLSA record generation -Applied-Upstream: https://github.com/letoams/hash-slinger/commit/0bb0dba91c51d367d9a37297f13e07f33c01bfdc - -It seems like the calculation for the TLSA record never really worked, -as we're doing float division here on the `len()` field. In our case, -that field returned `35.0` which is not valid in our environment. - -Doing an integer division gives the correct result in most cases, I -believe. - -Closes: #45 ---- - tlsa | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/tlsa b/tlsa -index cea7230..ec97150 100755 ---- a/tlsa -+++ b/tlsa -@@ -513,7 +513,7 @@ class TLSARecord: - def getRecord(self, generic=False): - """Returns the RR string of this TLSARecord, either in rfc (default) or generic format""" - if generic: -- return '%s IN TYPE52 \# %s %s%s%s%s' % (self.name, (len(self.cert)/2)+3 , self._toHex(self.usage), self._toHex(self.selector), self._toHex(self.mtype), self.cert) -+ return '%s IN TYPE52 \# %s %s%s%s%s' % (self.name, (len(self.cert)//2)+3 , self._toHex(self.usage), self._toHex(self.selector), self._toHex(self.mtype), self.cert) - return '%s IN TLSA %s %s %s %s' % (self.name, self.usage, self.selector, self.mtype, self.cert) - - def _toHex(self, val): --- -2.39.2 - diff -Nru hash-slinger-3.1/debian/patches/series hash-slinger-3.5/debian/patches/series --- hash-slinger-3.1/debian/patches/series 2023-10-05 16:36:07.000000000 +0200 +++ hash-slinger-3.5/debian/patches/series 2026-01-26 20:53:48.000000000 +0100 @@ -1,2 +1 @@ 0001-Debian-default-root.key-resides-in-usr-share-dns-roo.patch -0001-fix-generic-TLSA-record-generation.patch diff -Nru hash-slinger-3.1/fedora/hash-slinger.spec hash-slinger-3.5/fedora/hash-slinger.spec --- hash-slinger-3.1/fedora/hash-slinger.spec 2021-01-14 11:10:30.000000000 +0100 +++ hash-slinger-3.5/fedora/hash-slinger.spec 2026-01-26 17:44:59.000000000 +0100 @@ -1,16 +1,16 @@ Summary: Generate and verify various DNS records such as SSHFP, TLSA and OPENPGPKEY Name: hash-slinger -Version: 3.1 -Release: 1%{?dist} +Version: 3.5 +Release: 2%{?dist} License: GPLv2+ -Url: http://people.redhat.com/pwouters/%{name}/ -Source: http://people.redhat.com/pwouters/%{name}/%{name}-%{version}.tar.gz +Url: https://github.com/letoams/%{name}/ +Source: %{url}archive/%{version}/%{name}-%{version}.tar.gz # Only to regenerate the man page, which is shipped per default # Buildrequires: xmlto -BuildRequires: python3-devel +BuildRequires: python3-devel, make Requires: python3 >= 3.4 Requires: python3-dns, python3-unbound -Requires: openssh-clients >= 4, python3-m2crypto, python3-gnupg >= 0.3.7 +Requires: openssh-clients >= 4, python3-cryptography, python3-gnupg >= 0.3.7 BuildArch: noarch Obsoletes: sshfp < 2.0 Provides: sshfp = %{version} @@ -20,30 +20,44 @@ sshfp Generate RFC-4255 SSHFP DNS records from known_hosts files or ssh-keyscan -tlsa Generate RFC-6698 TLSA DNS records via TLS -openpgpkey Generate draft-ietf-dane-openpgpkey DNS records from OpenPGP +tlsa Generate RFC-6698 TLSA DNS records via TLS +openpgpkey Generate RFC-7929 OPENPGP DNS records from OpenPGP keyrings ipseckey Generate RFC-4025 IPSECKEY DNS records on Libreswan - IPsec servers + IPsec servers This package has incorporated the old 'sshfp' and 'swede' commands/packages %prep -%setup -q +%autosetup %build -make all +%make_build all %install -rm -rf ${RPM_BUILD_ROOT} -make DESTDIR=${RPM_BUILD_ROOT} install +%make_install %files -%doc BUGS CHANGES README COPYING +%license COPYING +%doc BUGS CHANGES README %{_bindir}/* %doc %{_mandir}/man1/* %changelog +* Sun Jan 09 2022 Frank Crawford <[email protected]> - 3.1-2 +- Update spec file following review + +* Sat Sep 25 2021 Frank Crawford <[email protected]> - 3.1-1 +- Updated to 3.1 +- Add BuildRequires make +- Clean up spec file for Fedora review + +* Sun Nov 03 2019 Frank Crawford <[email protected]> - 3.0-1 +- Update to Python3 + +* Sat Apr 13 2019 Paul Wouters <[email protected]> - 2.8-1 +- Remove Requires: for python-argparse which is now part of python core + * Wed Sep 21 2016 Paul Wouters <[email protected]> - 2.7-3 - Remove Requires: for python-argparse which is now part of python core diff -Nru hash-slinger-3.1/ipseckey hash-slinger-3.5/ipseckey --- hash-slinger-3.1/ipseckey 2021-01-14 11:10:30.000000000 +0100 +++ hash-slinger-3.5/ipseckey 2026-01-26 17:44:59.000000000 +0100 @@ -18,7 +18,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -VERSION = "3.1" +VERSION = "3.5" import os import sys diff -Nru hash-slinger-3.1/openpgpkey hash-slinger-3.5/openpgpkey --- hash-slinger-3.1/openpgpkey 2021-01-14 11:10:30.000000000 +0100 +++ hash-slinger-3.5/openpgpkey 2026-01-26 17:44:59.000000000 +0100 @@ -22,7 +22,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -VERSION="3.1" +VERSION="3.5" OPENPGPKEY=61 import sys @@ -37,7 +37,7 @@ def asctohex(s): empty = '' # I use this construct because I find ''.join() too dense - return empty.join(['%02x' % ord(c) for c in s]) # the %02 pads when needed + return empty.join(['%02x' % c for c in s]) # the %02 pads when needed def createOPENPGPKEY(email, gpgdisk, keyid, output, debug): ekey64 = "".join(gpgdisk.export_keys(keyid,minimal=True, secret=False, armor=True).split("\n")[2:-3]) @@ -51,7 +51,7 @@ if debug: print("Length for generic record is %s" % len(ekey)) print("; keyid: %s" % keyid) - print("%s._openpgpkey.%s. IN TYPE61 \# %s %s" % (euser, domain, len(ekey), asctohex(ekey))) + print(r"%s._openpgpkey.%s. IN TYPE61 \# %s %s" % (euser, domain, len(ekey), asctohex(ekey))) def sha256trunc(data): """Compute SHA2-256 hash truncated to 28 octets.""" @@ -169,11 +169,12 @@ sys.exit(1) if not args.uid and not args.keyid: pubkey = gpgnet.export_keys(args.email, minimal=True) - print('openpgpkey: Received OpenPGP data does not contain a key with keyid %s'%args.email, file=sys.stderr) - print('(add --uid <uid> to override with any of the below received uids)', file=sys.stderr) - for id in gpgnet.list_keys()[0]['uids']: - print("# %s"%id, file=sys.stderr) - sys.exit(1) + if not pubkey: + print('openpgpkey: Received OpenPGP data does not contain a key with keyid %s'%args.email, file=sys.stderr) + print('(add --uid <uid> to override with any of the below received uids)', file=sys.stderr) + for id in gpgnet.list_keys()[0]['uids']: + print("# %s"%id, file=sys.stderr) + sys.exit(1) pubkey = pubkey.replace("Version:","Comment: %s key obtained from DNS\nVersion:"%args.email) if args.insecure: diff -Nru hash-slinger-3.1/openpgpkey.1 hash-slinger-3.5/openpgpkey.1 --- hash-slinger-3.1/openpgpkey.1 2021-01-14 11:10:30.000000000 +0100 +++ hash-slinger-3.5/openpgpkey.1 2026-01-26 17:44:59.000000000 +0100 @@ -90,15 +90,15 @@ openpgpkey \-\-create paul@nohats\&.ca .SH "SEE ALSO" .PP -draft\-ietf\-dane\-openpgpkey +RFC-7929 .PP -\m[blue]\fBhttp://people\&.redhat\&.com/pwouters/hash\-slinger/\fR\m[] +\m[blue]\fBhttps://github\&.com/letoams/hash\-slinger\fR\m[] .SH "AUTHORS" .PP Paul Wouters <pwouters@redhat\&.com> .SH "COPYRIGHT" .PP -Copyright 2014\-2015 +Copyright 2014\-2022 .PP This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version\&. See <\m[blue]\fBhttp://www\&.fsf\&.org/copyleft/gpl\&.txt\fR\m[]>\&. .PP diff -Nru hash-slinger-3.1/openpgpkey.1.xml hash-slinger-3.5/openpgpkey.1.xml --- hash-slinger-3.1/openpgpkey.1.xml 2021-01-14 11:10:30.000000000 +0100 +++ hash-slinger-3.5/openpgpkey.1.xml 2026-01-26 17:44:59.000000000 +0100 @@ -103,9 +103,9 @@ <refsect1 id='see_also'><title>SEE ALSO</title> -<para>draft-ietf-dane-openpgpkey</para> +<para>RFC-7929</para> -<para><ulink url='http://people.redhat.com/pwouters/hash-slinger/'>http://people.redhat.com/pwouters/hash-slinger/</ulink></para> +<para><ulink url='https://github.com/letoams/hash-slinger'>https://github.com/letoams/hash-slinger</ulink></para> </refsect1> <refsect1 id='authors'><title>AUTHORS</title> @@ -113,7 +113,7 @@ </refsect1> <refsect1 id='copyright'><title>COPYRIGHT</title> -<para>Copyright 2014-2015</para> +<para>Copyright 2014-2022</para> <para>This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the diff -Nru hash-slinger-3.1/README hash-slinger-3.5/README --- hash-slinger-3.1/README 2021-01-14 11:10:30.000000000 +0100 +++ hash-slinger-3.5/README 2026-01-26 17:44:59.000000000 +0100 @@ -4,7 +4,7 @@ sshfp Generate RFC-4255 SSHFP DNS records from known_hosts files or ssh-keyscan -tlsa Generate RFC-6698 TLSA DNS records via TLS +tlsa Generate RFC-6698 TLSA DNS records via TLS openpgpkey Generate RFC-7929 OPENPGPKEY DNS records using gpg ipseckey Generate RFC-4025 IPSECKEY DNS records on Libreswan IPsec servers @@ -17,20 +17,21 @@ Contributors: -Chrisopher Olah <[email protected]> -James Brown <[email protected]> -Patrick Uiterwijk <[email protected]> -Gerald Turner <[email protected]> -Ondřej Surý <[email protected]> -Jan Vcelak <[email protected]> -Dirk Stöcker <[email protected]> -Frank Crawford <[email protected]> +Chrisopher Olah +James Brown +Patrick Uiterwijk +Gerald Turner +Ondřej Surý +Jan Vcelak +Dirk Stöcker +Frank Crawford +Bastian Germann REQUIREMENTS: python-dns http://www.dnspython.org/ python-gnupg http://pythonhosted.org/python-gnupg/ -m2crypto http://gitlab.com/m2crypto/m2crypto/ +cryptography https://cryptography.io/ unbound-python http://www.unbound.net/ ssh-keygen from openssh gpg from gnupg diff -Nru hash-slinger-3.1/sshfp hash-slinger-3.5/sshfp --- hash-slinger-3.1/sshfp 2021-01-14 11:10:30.000000000 +0100 +++ hash-slinger-3.5/sshfp 2026-01-26 17:44:59.000000000 +0100 @@ -23,7 +23,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -VERSION = "3.1" +VERSION = "3.5" import os import sys @@ -172,7 +172,7 @@ salt = base64.b64decode(salt64) hash = base64.b64decode(hash64) for host in hostnames: - if hash == hmac.new(salt, host, hashlib.sha1).digest(): + if hash == hmac.new(salt, host.encode(encoding="utf-8"), hashlib.sha1).digest(): return host return "" @@ -301,7 +301,6 @@ action="store", dest="known_hosts", metavar="KNOWN_HOSTS_FILE", - default=DEFAULT_KNOWN_HOSTS_FILE, help="obtain public ssh keys from the known_hosts file KNOWN_HOSTS_FILE") parser.add_option("-s", "--scan", action="store_true", @@ -363,7 +362,7 @@ (options, args) = parser.parse_args() # parse options - khfile = options.known_hosts + khfile = options.known_hosts or DEFAULT_KNOWN_HOSTS_FILE dodns = options.scan nameserver = "" domain = "" @@ -373,6 +372,14 @@ trailing = options.trailing_dot timeout = options.timeout algos = options.algo or ["rsa", "ecdsa", "ed25519", "xmss"] + for algo in algos: + cmd = ["ssh-keyscan", "-t", algo] + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + (stdout, stderr) = process.communicate() + if "Unknown key type" in stderr: + algos.remove(algo) + if not quiet: + print("WARNING: key type %s not supported" % algo, file=sys.stderr) digests = options.digest or ["sha256"] all_hosts = options.all_hosts port = options.port @@ -384,7 +391,7 @@ if not quiet and port != 22: print("WARNING: non-standard port numbers are not designated in SSHFP records", file=sys.stderr) if not quiet and options.known_hosts and options.scan: - print("WARNING: Ignoring known hosts option -k , -s was passed", file=sys.stderr) + print("WARNING: Ignoring known hosts option -k, -s was passed", file=sys.stderr) if options.nameserver and not options.scan and not options.all_hosts: print("ERROR: Cannot specify -n without -s and -a", file=sys.stderr) sys.exit(1) diff -Nru hash-slinger-3.1/sshfp.1 hash-slinger-3.5/sshfp.1 --- hash-slinger-3.1/sshfp.1 2021-01-14 11:10:30.000000000 +0100 +++ hash-slinger-3.5/sshfp.1 2026-01-26 17:44:59.000000000 +0100 @@ -32,16 +32,17 @@ .SH "SYNTAX" .PP sshfp [\fB\-k\fR -<\fIknownhosts_file\fR>] [\fB\-d\fR] [\fB\-a\fR] [\fB\-\-type\fR +<\fIknownhosts_file\fR>] [\fB\-d\fR] [\fB\-a\fR] [\fB\-t\fR <algo>] [\fB\-\-digest\fR <digest>] [<\fIhost1\fR> [\fIhost2 \&.\&.\&.]\fR] .PP sshfp \fB\-s\fR [\fB\-p\fR -<\fIport\fR>] [\fB\-d\fR] [\fB\-a\fR] [\fB\-\-type\fR +<\fIport\fR>] [\fB\-d\fR] [\fB\-a\fR] [\fB\-t\fR <algo>] [\fB\-\-digest\fR -<digest>] [\fB\-n <nameserver\fR>\fI] <domain1\fR> [\fIdomain2\fR] <\fIhost1\fR> [\fIhost2 \&.\&.\&.\fR] > +<digest>] [\fB\-n\fR +<\fInameserver\fR>] <\fIdomain1\fR> [\fIdomain2\fR] <\fIhost1\fR> [\fIhost2 \&.\&.\&.\fR] .SH "DESCRIPTION" .PP sshfp generates RFC\-4255 SSHFP DNS records based on the public keys stored in a known_hosts file, which implies the user has previously trusted this key, or public keys can be obtained by using ssh\-keyscan (1)\&. Using ssh\-keyscan (1) implies a secure path to connect to the hosts being scanned\&. It also implies a trust in the DNS to obtain the IP address of the hostname to be scanned\&. If the nameserver of the domain allows zone transfers (AXFR), an entire domain can be processed for all its A records\&. @@ -49,12 +50,12 @@ .PP \fB\-s / \-\-scan\fR <\fIhostname1\fR> [hostname2 \&.\&.\&.] .RS 4 -Scan hosts or domain for public SSH keys using ssh\-keyscan +Scan hosts or domain for public SSH keys using ssh\-keyscan\&. .RE .PP \fB\-k / \-\-knownhosts <\fR\fIknownhosts_file\fR\fI> <\fR\fIhostname1\fR\fI> [hostname2 \&.\&.\&.]\fR .RS 4 -Obtain public SSH keys from a known_hosts file\&. Defaults to using ~/\&.ssh/known_hosts +Obtain public SSH keys from a known_hosts file\&. Defaults to using ~/\&.ssh/known_hosts\&. .RE .PP \fB\-a / \-\-all\fR @@ -67,9 +68,19 @@ Add a trailing dot to the hostname in the SSHFP records\&. It is not possible to determine whether a known_hosts or dns query is for a FQDN (eg www\&.redhat\&.com) or not (eg www) or not (unless \-d domainname \-a is used, in which case a trailing dot is always appended)\&. Non\-FQDN get their domainname appended through /etc/resolv\&.conf These non\-FQDN will happen when using a non\-FQDN (eg sshfp \-k www) or known_hosts entries obtained by running ssh www\&.sub where \&.domain\&.com is implied\&. When \-d is used, all hostnames not ending with a dot, that at least contain two parts in their hostname (eg www\&.sub but not www get a trailing dot\&. Note that the output of sshfp can also just be manually edited for trailing dots\&. .RE .PP +\fB\-\-digest\fR <\fIdigest\fR> +.RS 4 +Fingerprint hash function (may be specified more than once, default sha1,sha256)\&. +.RE +.PP +\fB\-n / \-\-nameserver\fR <\fInameserver\fR> +.RS 4 +Nameserver to use for AXFR (only valid with \-s \-a)\&. +.RE +.PP \fB\-o / \-\-output\fR <\fIfilename\fR> .RS 4 -Write to filename instead of stdout +Write to filename instead of stdout\&. .RE .PP \fB\-p / \-\-port\fR <\fIportnumber\fR> @@ -82,6 +93,16 @@ Output help information and exit\&. .RE .PP +\fB\-T / \-\-timeout\fR <\fIseconds\fR> +.RS 4 +Scanning timeout, in seconds (default 5)\&. +.RE +.PP +\fB\-t / \-\-type\fR <\fIalgo\fR> +.RS 4 +Key type to fetch (may be specified more than once, default rsa,ecdsa,ed25519,dsa,xmss)\&. +.RE +.PP \fB\-v / \-\-version\fR .RS 4 Output version information and exit\&. @@ -89,7 +110,7 @@ .PP \fB\-q / \-\-quiet\fR .RS 4 -Output less miscellany to stderr +Output less miscellany to stderr\&. .RE .SH "FILES" .PP @@ -123,7 +144,9 @@ sshfp \-a \-d \-d nohats\&.ca \-n ns0\&.nohats\&.ca >> /var/named/primary/nohats\&.ca .SH "SEE ALSO" .PP -\fBssh-keyscan\fR(1)\fBssh\fR(1)\fBtlsa\fR(1) +\fBssh-keyscan\fR(1) +\fBssh\fR(1) +\fBtlsa\fR(1) and RFC\-4255 .SH "AUTHORS" .PP diff -Nru hash-slinger-3.1/sshfp.1.xml hash-slinger-3.5/sshfp.1.xml --- hash-slinger-3.1/sshfp.1.xml 2021-01-14 11:10:30.000000000 +0100 +++ hash-slinger-3.5/sshfp.1.xml 2026-01-26 17:44:59.000000000 +0100 @@ -17,10 +17,9 @@ <!-- body begins here --> <refsect1 id='syntax'><title>SYNTAX</title> -<para>sshfp [<option>-k</option> <<emphasis remap='I'>knownhosts_file</emphasis>>] [<option>-d</option>] [<option>-a</option>] [<option>--type</option> <algo>] [<option>--digest</option> <digest>] [<<emphasis remap='I'>host1</emphasis>> [<emphasis remap='I'>host2 ...]</emphasis>] -</para> +<para>sshfp [<option>-k</option> <<emphasis remap='I'>knownhosts_file</emphasis>>] [<option>-d</option>] [<option>-a</option>] [<option>-t</option> <algo>] [<option>--digest</option> <digest>] [<<emphasis remap='I'>host1</emphasis>> [<emphasis remap='I'>host2 ...]</emphasis>]</para> <!-- .br --> -<para>sshfp <option>-s</option> [<option>-p</option> <<emphasis remap='I'>port</emphasis>>] [<option>-d</option>] [<option>-a</option>] [<option>--type</option> <algo>] [<option>--digest</option> <digest>] [<option>-n <nameserver</option>><emphasis remap='P->I'>] <domain1</emphasis>> [<emphasis remap='I'>domain2</emphasis>] <<emphasis remap='I'>host1</emphasis>> [<emphasis remap='I'>host2 ...</emphasis>] ></para> +<para>sshfp <option>-s</option> [<option>-p</option> <<emphasis remap='I'>port</emphasis>>] [<option>-d</option>] [<option>-a</option>] [<option>-t</option> <algo>] [<option>--digest</option> <digest>] [<option>-n</option> <<emphasis remap='I'>nameserver</emphasis>>] <<emphasis remap='I'>domain1</emphasis>> [<emphasis remap='I'>domain2</emphasis>] <<emphasis remap='I'>host1</emphasis>> [<emphasis remap='I'>host2 ...</emphasis>]</para> </refsect1> <refsect1 id='description'><title>DESCRIPTION</title> @@ -37,13 +36,13 @@ <varlistentry> <term><option>-s / --scan</option> <<emphasis remap='I'>hostname1</emphasis>> [hostname2 ...]</term> <listitem> -<para>Scan hosts or domain for public SSH keys using ssh-keyscan</para> +<para>Scan hosts or domain for public SSH keys using ssh-keyscan.</para> </listitem> </varlistentry> <varlistentry> <term><option>-k / --knownhosts <</option><emphasis remap='I'>knownhosts_file</emphasis><emphasis remap='P->B'>> <</emphasis><emphasis remap='I'>hostname1</emphasis><emphasis remap='P->B'>> [hostname2 ...]</emphasis></term> <listitem> -<para>Obtain public SSH keys from a known_hosts file. Defaults to using ~/.ssh/known_hosts</para> +<para>Obtain public SSH keys from a known_hosts file. Defaults to using ~/.ssh/known_hosts.</para> </listitem> </varlistentry> <varlistentry> @@ -67,9 +66,21 @@ </listitem> </varlistentry> <varlistentry> + <term><option>--digest</option> <<emphasis remap='I'>digest</emphasis>></term> + <listitem> +<para>Fingerprint hash function (may be specified more than once, default sha1,sha256).</para> + </listitem> + </varlistentry> + <varlistentry> + <term><option>-n / --nameserver</option> <<emphasis remap='I'>nameserver</emphasis>></term> + <listitem> +<para>Nameserver to use for AXFR (only valid with -s -a).</para> + </listitem> + </varlistentry> + <varlistentry> <term><option>-o / --output</option> <<emphasis remap='I'>filename</emphasis>></term> <listitem> -<para>Write to filename instead of stdout</para> +<para>Write to filename instead of stdout.</para> </listitem> </varlistentry> <varlistentry> @@ -85,6 +96,18 @@ </listitem> </varlistentry> <varlistentry> + <term><option>-T / --timeout</option> <<emphasis remap='I'>seconds</emphasis>></term> + <listitem> +<para>Scanning timeout, in seconds (default 5).</para> + </listitem> + </varlistentry> + <varlistentry> + <term><option>-t / --type</option> <<emphasis remap='I'>algo</emphasis>></term> + <listitem> +<para>Key type to fetch (may be specified more than once, default rsa,ecdsa,ed25519,dsa,xmss).</para> + </listitem> + </varlistentry> + <varlistentry> <term><option>-v / --version</option></term> <listitem> <para>Output version information and exit.</para> @@ -93,7 +116,7 @@ <varlistentry> <term><option>-q / --quiet</option></term> <listitem> -<para>Output less miscellany to stderr</para> +<para>Output less miscellany to stderr.</para> </listitem> </varlistentry> </variablelist> diff -Nru hash-slinger-3.1/tlsa hash-slinger-3.5/tlsa --- hash-slinger-3.1/tlsa 2021-01-14 11:10:30.000000000 +0100 +++ hash-slinger-3.5/tlsa 2026-01-26 17:44:59.000000000 +0100 @@ -23,21 +23,25 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -VERSION="3.1" +VERSION="3.5" import sys import os import socket +import ssl import unbound import subprocess import re -from M2Crypto import X509, SSL, BIO, m2 +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.backends import default_backend +from cryptography.x509.oid import ExtensionOID, NameOID from binascii import b2a_hex from hashlib import sha256, sha512 from ipaddress import IPv4Address, IPv6Address ROOTKEY="none" -cauldron = ( "/var/lib/unbound/root.anchor", "/var/lib/unbound/root.key", "/etc/unbound/root.key" ) +cauldron = ( "/var/lib/unbound/root.anchor", "/var/lib/unbound/root.key", "/etc/unbound/root.key", "/usr/share/dns/root.key" ) for root in cauldron: if os.path.isfile(root): ROOTKEY=root @@ -60,10 +64,10 @@ if record.isValid: if record.selector == 0: # Hash the Full certificate - record.cert = getHash(certificate, record.mtype) + record.cert = getHash(certificate, record.mtype, use_pubkey=False) else: # Hash only the SubjectPublicKeyInfo - record.cert = getHash(certificate.get_pubkey(), record.mtype) + record.cert = getHash(certificate, record.mtype, use_pubkey=True) record.isValid(raiseException=True) @@ -81,13 +85,13 @@ certsleft -= 1 if usage == 1 or usage == 3: # The first cert is the end-entity cert - print('Got a certificate for %s with Subject: %s' % (address, chaincert.get_subject())) + print('Got a certificate for %s with Subject: %s' % (address, get_subject_str(chaincert))) cert = chaincert break else: - if (usage == 0 and chaincert.check_ca()) or usage == 2: + if (usage == 0 and check_ca(chaincert)) or usage == 2: if certsleft: # don't ask for the last one - sys.stdout.write('Got a certificate with the following Subject:\n\t%s\nUse this as certificate to match? [y/N] ' % chaincert.get_subject()) + sys.stdout.write('Got a certificate with the following Subject:\n\t%s\nUse this as certificate to match? [y/N] ' % get_subject_str(chaincert)) input_ok = False while not input_ok: user_input = input() @@ -99,7 +103,7 @@ else: sys.stdout.write('Please answer Y or N') else: - print('Using certificate with the following Subject:\n\t%s\n' % chaincert.get_subject()) + print('Using certificate with the following Subject:\n\t%s\n' % get_subject_str(chaincert)) cert = chaincert if cert: break @@ -111,7 +115,6 @@ else: print(genTLSA(hostname, protocol, port, cert, output, usage, selector, mtype)) - # Clear the cert from memory (to stop M2Crypto from segfaulting) cert=None def getA(hostname, secure=True): @@ -222,50 +225,66 @@ else: raise DNSLookupError('Unsuccessful DNS lookup or no data returned for rrtype %s (%s).' % (rrtypestr, rrtype)) -# identical to Connection.connect() except for the last parameter -def sslStartTLSConnect(connection, addr, starttls=None): - connection.socket.connect(addr) - connection.addr = addr - if starttls: - # primitive method, no error checks yet - if starttls == "smtp": - data = connection.socket.recv(500) - connection.socket.send("EHLO M2Crypto\r\n".encode('ascii')) - data = connection.socket.recv(500) - connection.socket.send("STARTTLS\r\n".encode('ascii')) - data = connection.socket.recv(500) - elif starttls == "imap": - data = connection.socket.recv(500) - connection.socket.send(". STARTTLS\r\n".encode('ascii')) - data = connection.socket.recv(500) - elif starttls == "ftp": - data = connection.socket.recv(500) - connection.socket.send("AUTH TLS\r\n".encode('ascii')) - data = connection.socket.recv(500) - elif starttls == "pop3": - data = connection.socket.recv(500) - connection.socket.send("STLS\r\n".encode('ascii')) - data = connection.socket.recv(500) - connection.setup_ssl() - connection.set_connect_state() - ret = connection.connect_ssl() - check = getattr(connection, 'postConnectionCheck', connection.clientPostConnectionCheck) - if check is not None: - if not check(connection.get_peer_cert(), connection.addr[0]): - raise Checker.SSLVerificationError('post connection check failed') - return ret +def get_subject_str(cert): + """Get a string representation of the certificate subject""" + return cert.subject.rfc4514_string() -def getHash(certificate, mtype): +def check_ca(cert): + """Check if a certificate is a CA certificate""" + try: + bc = cert.extensions.get_extension_for_oid(ExtensionOID.BASIC_CONSTRAINTS) + return bc.value.ca + except x509.ExtensionNotFound: + return False + +def get_subject_alt_names(cert): + """Get subject alternative names from a certificate""" + try: + ext = cert.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME) + return [name.value for name in ext.value if isinstance(name, x509.DNSName)] + except x509.ExtensionNotFound: + return [] + +# Perform STARTTLS handshake on plain socket before wrapping with SSL +def do_starttls_handshake(sock, starttls): + """Perform STARTTLS protocol handshake on a plain socket""" + if starttls == "smtp": + data = sock.recv(500) + sock.send("EHLO tlsa\r\n".encode('ascii')) + data = sock.recv(500) + sock.send("STARTTLS\r\n".encode('ascii')) + data = sock.recv(500) + elif starttls == "imap": + data = sock.recv(500) + sock.send(". STARTTLS\r\n".encode('ascii')) + data = sock.recv(500) + elif starttls == "ftp": + data = sock.recv(500) + sock.send("AUTH TLS\r\n".encode('ascii')) + data = sock.recv(500) + elif starttls == "pop3": + data = sock.recv(500) + sock.send("STLS\r\n".encode('ascii')) + data = sock.recv(500) + +def getHash(certificate, mtype, use_pubkey=False): """Hashes the certificate based on the mtype. - The certificate should be an M2Crypto.X509.X509 object (or the result of the get_pubkey() function on said object) + The certificate should be a cryptography x509.Certificate object. + If use_pubkey is True, hash only the SubjectPublicKeyInfo. """ - certificate = certificate.as_der() + if use_pubkey: + cert_bytes = certificate.public_key().public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + else: + cert_bytes = certificate.public_bytes(serialization.Encoding.DER) if mtype == 0: - return certificate.hex() + return cert_bytes.hex() elif mtype == 1: - return sha256(certificate).hexdigest() + return sha256(cert_bytes).hexdigest() elif mtype == 2: - return sha512(certificate).hexdigest() + return sha512(cert_bytes).hexdigest() else: raise Exception('mtype should be 0,1,2') @@ -303,17 +322,17 @@ def verifyCertMatch(record, cert): """ Verify the certificate with the record. - record should be a TLSARecord and cert should be a M2Crypto.X509.X509 + record should be a TLSARecord and cert should be a cryptography x509.Certificate """ - if not isinstance(cert, X509.X509): + if not isinstance(cert, x509.Certificate): return if not isinstance(record, TLSARecord): return if record.selector == 1: - certhash = getHash(cert.get_pubkey(), record.mtype) + certhash = getHash(cert, record.mtype, use_pubkey=True) else: - certhash = getHash(cert, record.mtype) + certhash = getHash(cert, record.mtype, use_pubkey=False) if not certhash: return @@ -324,8 +343,8 @@ return False def verifyCertNameWithHostName(cert, hostname, with_msg=False): - """Verify the name on the certificate with a hostname, we need this because we get the cert based on IP address and thusly cannot rely on M2Crypto to verify this""" - if not isinstance(cert, X509.X509): + """Verify the name on the certificate with a hostname, we need this because we get the cert based on IP address and thusly cannot rely on ssl to verify this""" + if not isinstance(cert, x509.Certificate): return if not isinstance(hostname, str): return @@ -335,15 +354,15 @@ hostname = hostname.lower() certnames = [] - for entry in str(cert.get_subject()).lower().split("/"): - if entry.startswith("cn="): - certnames.append(entry[3:]) - try: - for value in re.split(", ?", cert.get_ext('subjectAltName').get_value().lower()): - if value.startswith("dns:"): - certnames.append(value[4:]) - except: - pass + # Get CN from subject + for attr in cert.subject: + if attr.oid == NameOID.COMMON_NAME: + certnames.append(attr.value.lower()) + # Get SubjectAltName DNS entries + san_names = get_subject_alt_names(cert) + for name in san_names: + certnames.append(name.lower()) + if hostname in certnames: return True for certname in certnames: @@ -351,7 +370,8 @@ return True if with_msg: - print('WARNING: Name on the certificate (Subject: %s, SubjectAltName: %s) doesn\'t match requested hostname (%s).' % (str(cert.get_subject()), cert.get_ext('subjectAltName').get_value(), hostname)) + san_str = ', '.join(san_names) if san_names else 'None' + print('WARNING: Name on the certificate (Subject: %s, SubjectAltName: %s) doesn\'t match requested hostname (%s).' % (get_subject_str(cert), san_str, hostname)) return False def checkChainLink(chain, record=None): @@ -360,33 +380,35 @@ matched = None for cert in chain: if previous_issuer: - if not str(previous_issuer) == str(cert.get_subject()): # The chain cannot be valid + if not previous_issuer == cert.subject: # The chain cannot be valid chained = False break - previous_issuer = cert.get_issuer() + previous_issuer = cert.issuer if record and verifyCertMatch(record, cert): matched = cert return chained, matched def getLocalChain(filename, debug): - """Returns list of M2Crypto.X509.X509 objects and verification result""" + """Returns list of cryptography x509.Certificate objects and verification result""" chain = [] - bio = BIO.openfile(filename) - Err = None - while True: + with open(filename, 'rb') as f: + pem_data = f.read() + # Parse all PEM certificates in the file + import re as regex + pem_certs = regex.findall( + rb'-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----', + pem_data + ) + for pem_cert in pem_certs: try: - cptr = m2.x509_read_pem(bio._ptr()) + cert = x509.load_pem_x509_certificate(pem_cert, default_backend()) + chain.append(cert) except Exception as e: - Err = e - break; - if not cptr: - break - chain.append(X509.X509(cptr, _pyfree=1)) + if debug: + print('Error parsing certificate: %s' % e, file=sys.stderr) + continue if not chain: - if Err: - raise - else: - raise Exception("Could not load %s" % filename) + raise Exception("Could not load %s" % filename) # FIXME - is this possible using the library (without a call to openssl tool)? cmd = ["openssl", "verify", filename] process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -403,7 +425,8 @@ names = ["PKIX-CA", "PKIX-EE", "DANE-TA", "DANE-EE"] return "Usage %d [%s]" % (usage, names[usage]) -def verifyChain(chain, verify_result, pre_exit, local, source): +def verifyChain(record, chain, verify_result, pre_exit, local, source): + source = "%s for %s" % (record.getShortText(), source) if local: text_cert = "Local certificate" text_chain = "certificate in the local certificate chain" @@ -429,7 +452,7 @@ reason = getVerificationErrorReason(verify_result) print('FAIL (%s): %s matches the one mentioned in the TLSA record but the following error was raised during PKIX validation (%s): %s' % (ut, text_cert, source, reason)) if pre_exit == 0: pre_exit = 2 - if args.debug: print('The matched certificate has Subject: %s' % cert.get_subject()) + if args.debug: print('The matched certificate has Subject: %s' % get_subject_str(matched)) else: print('FAIL: %s does not match the TLSA record (%s)' % (text_cert, source)) if pre_exit == 0: pre_exit = 2 @@ -445,7 +468,7 @@ if not chained: print("WARN: Certificates don't chain") if matched: - if cert.check_ca(): + if check_ca(matched): if verify_result == 0: print('SUCCESS (%s): A %s matches the one mentioned in the TLSA record and is a CA certificate (%s)' % (ut, text_chain, source)) else: @@ -458,7 +481,7 @@ else: print('FAIL (%s): A %s matches the one mentioned in the TLSA record but is not a CA certificate (%s)' % (ut, text_chain, source)) if pre_exit == 0: pre_exit = 2 - if args.debug: print('The matched certificate has Subject: %s' % cert.get_subject()) + if args.debug: print('The matched certificate has Subject: %s' % get_subject_str(matched)) else: print('FAIL (%s): No %s matches the TLSA record (%s)' % (ut, text_chain, source)) if pre_exit == 0: pre_exit = 2 @@ -495,7 +518,7 @@ """When instanciated, this class contains all the fields of a TLSA record. """ def __init__(self, name, usage, selector, mtype, cert): - """name is the name of the RR in the format: /^(_\d{1,5}|\*)\._(tcp|udp|sctp)\.([a-z0-9]*\.){2,}$/ + r"""name is the name of the RR in the format: /^(_\d{1,5}|\*)\._(tcp|udp|sctp)\.([a-z0-9]*\.){2,}$/ usage, selector and mtype should be an integer cert should be a hexidecimal string representing the certificate to be matched field """ @@ -513,7 +536,7 @@ def getRecord(self, generic=False): """Returns the RR string of this TLSARecord, either in rfc (default) or generic format""" if generic: - return '%s IN TYPE52 \# %s %s%s%s%s' % (self.name, (len(self.cert)/2)+3 , self._toHex(self.usage), self._toHex(self.selector), self._toHex(self.mtype), self.cert) + return r'%s IN TYPE52 \# %s %s%s%s%s' % (self.name, (len(self.cert)//2)+3 , self._toHex(self.usage), self._toHex(self.selector), self._toHex(self.mtype), self.cert) return '%s IN TLSA %s %s %s %s' % (self.name, self.usage, self.selector, self.mtype, self.cert) def _toHex(self, val): @@ -554,20 +577,23 @@ def isNameValid(self): """Check if the name if in the correct format""" - if not re.match('^(_\d{1,5}|\*)\._(tcp|udp|sctp)\.([-a-z0-9]*\.){2,}$', self.name): + if not re.match(r'^(_\d{1,5}|\*)\._(tcp|udp|sctp)\.([-a-z0-9]*\.){2,}$', self.name): return False return True def getProtocol(self): """Returns the protocol based on the name""" - return re.split('\.', self.name)[1][1:] + return re.split(r'\.', self.name)[1][1:] def getPort(self): """Returns the port based on the name""" - if re.split('\.', self.name)[0][0] == '*': + if re.split(r'\.', self.name)[0][0] == '*': return '*' else: - return re.split('\.', self.name)[0][1:] + return re.split(r'\.', self.name)[0][1:] + + def getShortText(self): + return "%d %d %d %.8s..." % (self.usage, self.selector, self.mtype, self.cert) class ARecord: """An object representing an A Record (IPv4 address)""" @@ -703,9 +729,8 @@ records = getTLSA(args.host, args.port, args.protocol, secure) if len(records) == 0: sys.exit(1) - + pre_exit = 0 for record in records: - pre_exit = 0 # First, check if the first three fields have correct values. if args.debug: print('Received the following record for name %s:' % record.name) @@ -745,62 +770,68 @@ if args.certificate: try: chain, verify_result = getLocalChain(args.certificate, args.debug) - pre_exit = verifyChain(chain, verify_result, pre_exit, True, args.certificate) + pre_exit = verifyChain(record, chain, verify_result, pre_exit, True, args.certificate) except Exception as e: print('Could not verify local certificate: %s.' % e) sys.exit(0) for address in addresses: if args.debug: print('Got the following IP: %s' % str(address)) - # We do the certificate handling here, as M2Crypto keeps segfaulting when we do it in a method - ctx = SSL.Context() + # Create SSL context + ctx = ssl.create_default_context() if os.path.isfile(args.ca_cert): - if ctx.load_verify_locations(cafile=args.ca_cert) != 1: raise Exception('No CA cert') + ctx.load_verify_locations(cafile=args.ca_cert) elif os.path.exists(args.ca_cert): - if ctx.load_verify_locations(capath=args.ca_cert) != 1: raise Exception('No CA certs') + ctx.load_verify_locations(capath=args.ca_cert) else: print('%s is neither a file nor a directory, unable to continue' % args.ca_cert, file=sys.stderr) sys.exit(1) # Don't error when the verification fails in the SSL handshake - ctx.set_verify(SSL.verify_none, depth=9) + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_OPTIONAL if isinstance(address, AAAARecord): sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) else: - sock = None - connection = SSL.Connection(ctx, sock=sock) - try: - connection.set_tlsext_host_name(snihost) - if(args.debug): - print('Did set servername %s' % snihost) - except AttributeError: - print('Could not set SNI (old M2Crypto version?)') - except: - print('Could not set SNI') + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: + sock.connect((str(address), int(args.port))) if args.starttls: - sslStartTLSConnect(connection, (str(address), int(args.port)), args.starttls) - #connection.connect((str(address), int(args.port)), args.starttls) - else: - connection.connect((str(address), int(args.port))) - #except TypeError: - # print 'Cannot connect to %s (old M2Crypto version not supporting start script?)' % address - # continue - except SSL.Checker.WrongHost as e: - # The name on the remote cert doesn't match the hostname because we connect on IP, not hostname (as we want secure lookup) + do_starttls_handshake(sock, args.starttls) + connection = ctx.wrap_socket(sock, server_hostname=snihost) + if args.debug: + print('Did set servername %s' % snihost) + except ssl.SSLCertVerificationError: + # Certificate verification failed but we still want to check TLSA pass except socket.error as e: print('Cannot connect to %s: %s' % (address, str(e))) continue - chain = connection.get_peer_cert_chain() - verify_result = connection.get_verify_result() + # Get peer certificate chain + if hasattr(connection, 'get_peer_cert_chain'): + der_certs = connection.get_peer_cert_chain(binary_form=True) + if der_certs: + chain = [x509.load_der_x509_certificate(der_cert, default_backend()) for der_cert in der_certs] + else: + chain = None + else: + # Fallback for Python < 3.13 without get_peer_cert_chain + der_cert = connection.getpeercert(binary_form=True) + if der_cert: + chain = [x509.load_der_x509_certificate(der_cert, default_backend())] + else: + chain = None + if not chain: + print('Could not get certificate from %s' % address) + continue + # Get verification result (0 means OK in ssl module when no exception) + verify_result = 0 # If we got here without exception, verification passed # Good, now let's verify - pre_exit = verifyChain(chain, verify_result, pre_exit, False, address) - # Cleanup, just in case - connection.clear() + pre_exit = verifyChain(record, chain, verify_result, pre_exit, False, address) + # Cleanup connection.close() - ctx.close() # END for address in addresses # END for record in records @@ -848,44 +879,49 @@ for address in addresses: if args.debug: print('Attempting to get certificate from %s' % str(address)) - # We do the certificate handling here, as M2Crypto keeps segfaulting when try to do stuff with the cert if we don't - ctx = SSL.Context() - ctx.set_verify(SSL.verify_none, depth=9) + # Create SSL context + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE if isinstance(address, AAAARecord): sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) else: - sock = None - connection = SSL.Connection(ctx, sock=sock) - try: - connection.set_tlsext_host_name(snihost) - if(args.debug): - print('Did set servername %s' % snihost) - except AttributeError: - print('Could not set SNI (old M2Crypto version?)') - except: - print('Could not set SNI') + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: + sock.connect((str(address), int(connection_port))) if args.starttls: - sslStartTLSConnect(connection, (str(address), int(connection_port)), args.starttls) - #connection.connect((str(address), int(connection_port)), args.starttls) - else: - connection.connect((str(address), int(connection_port))) - #except TypeError: - # print 'Cannot connect to %s (old M2Crypto version not supporting start script?)' % address - # continue - except SSL.Checker.WrongHost: - pass + do_starttls_handshake(sock, args.starttls) + connection = ctx.wrap_socket(sock, server_hostname=snihost) + if args.debug: + print('Did set servername %s' % snihost) except socket.error as e: print('Cannot connect to %s: %s' % (address, str(e))) continue - chain = connection.get_peer_cert_chain() + # Get peer certificate chain + if hasattr(connection, 'get_peer_cert_chain'): + der_certs = connection.get_peer_cert_chain(binary_form=True) + if der_certs: + chain = [x509.load_der_x509_certificate(der_cert, default_backend()) for der_cert in der_certs] + else: + chain = None + else: + # Fallback for Python < 3.13 without get_peer_cert_chain + der_cert = connection.getpeercert(binary_form=True) + if der_cert: + chain = [x509.load_der_x509_certificate(der_cert, default_backend())] + else: + chain = None + if not chain: + print('Could not get certificate from %s' % address) + continue + if not verifyCertNameWithHostName(cert=chain[0], hostname=str(args.host), with_msg=args.debug): + print('WARNING: Certificate name does not match the hostname') genRecords(args.host, address, args.protocol, args.port, chain, args.output, args.usage, args.selector, args.mtype) - # Cleanup the connection and context - connection.clear() + # Cleanup connection.close() - ctx.close() else: # Pass the path to the certificate to the genTLSA function try: diff -Nru hash-slinger-3.1/tlsa.1 hash-slinger-3.5/tlsa.1 --- hash-slinger-3.1/tlsa.1 2021-01-14 11:10:30.000000000 +0100 +++ hash-slinger-3.5/tlsa.1 2026-01-26 17:44:59.000000000 +0100 @@ -31,7 +31,7 @@ tlsa \- Create and verify RFC\-6698 TLSA DNS records .SH "SYNTAX" .PP -tlsa [\fB\-h\fR] [\fB\-\-verify\fR] [\fB\-create\fR] [\fB\-\-version\fR] [\fB\-4\fR] [\fB\-6\fR] [\fB\-\-insecure\fR] [\fB\-\-resolv\&.conf /PATH/TO/RESOLV\&.CONF\fR] [\fB\-\-port PORT\fR] [\fB\-\-starttls {auto,smtp,imap,pop3,ftp}\fR] [\fB\-\-protocol {tcp,udp,sctp}\fR] [\fB\-\-only\-rr\fR] [\fB\-\-rootkey /PATH/TO/ROOT\&.KEY\fR] [\fB\-\-ca\-cert /PATH/TO/CERTSTORE\fR] [\fB\-\-debug\fR] [\fB\-\-quiet\fR] [\fB\-\-certificate CERTIFICATE\fR] [\fB\-\-output {rfc,generic,both}\fR] [\fB\-\-usage {0,1,2,3}\fR] [\fB\-\-selector {0,1}\fR] [\fB\-mtype {0,1,2}\fR] +tlsa [\fB\-h\fR] [\fB\-\-verify\fR] [\fB\-create\fR] [\fB\-\-version\fR] [\fB\-4\fR] [\fB\-6\fR] [\fB\-\-insecure\fR] [\fB\-\-resolvconf /PATH/TO/RESOLV\&.CONF\fR] [\fB\-\-port PORT\fR] [\fB\-\-starttls {auto,smtp,imap,pop3,ftp}\fR] [\fB\-\-protocol {tcp,udp,sctp}\fR] [\fB\-\-only\-rr\fR] [\fB\-\-rootkey /PATH/TO/ROOT\&.KEY\fR] [\fB\-\-ca\-cert /PATH/TO/CERTSTORE\fR] [\fB\-\-debug\fR] [\fB\-\-quiet\fR] [\fB\-\-certificate CERTIFICATE\fR] [\fB\-\-output {rfc,generic,both}\fR] [\fB\-\-usage {0,1,2,3}\fR] [\fB\-\-selector {0,1}\fR] [\fB\-mtype {0,1,2}\fR] \fIhostname\fR .SH "DESCRIPTION" .PP @@ -113,7 +113,7 @@ If neither create or verify is specified, create is used\&. .SH "REQUIREMENTS" .PP -tlsa requires the following python libraries: unbound, m2crypto, argparse and ipaddr +tlsa requires the following python libraries: unbound, cryptography, argparse and ipaddr .SH "BUGS" .PP ipv4/ipv6 handling @@ -128,9 +128,9 @@ tlsa \-\-create \-\-insecure fedoraproject\&.org .SH "SEE ALSO" .PP -\fBsshfp\fR(1)\fBssh-keygen\fR(1)and RFC\-6698 +\fBsshfp\fR(1)\fB, ssh-keygen\fR(1) and RFC\-6698 .PP -\m[blue]\fBhttp://people\&.redhat\&.com/pwouters/hash\-slinger/\fR\m[] +\m[blue]\fBhttps://github\&.com/letoams/hash\-slinger\fR\m[] .PP \m[blue]\fBhttp://os3sec\&.org/\fR\m[] .SH "AUTHORS" diff -Nru hash-slinger-3.1/tlsa.1.xml hash-slinger-3.5/tlsa.1.xml --- hash-slinger-3.1/tlsa.1.xml 2021-01-14 11:10:30.000000000 +0100 +++ hash-slinger-3.5/tlsa.1.xml 2026-01-26 17:44:59.000000000 +0100 @@ -19,7 +19,7 @@ <refsect1 id='syntax'><title>SYNTAX</title> <para>tlsa [<option>-h</option>] [<option>--verify</option>] [<option>-create</option>] [<option>--version</option>] [<option>-4</option>] [<option>-6</option>] [<option>--insecure</option>] - [<option>--resolv.conf /PATH/TO/RESOLV.CONF</option>] + [<option>--resolvconf /PATH/TO/RESOLV.CONF</option>] [<option>--port PORT</option>] [<option>--starttls {auto,smtp,imap,pop3,ftp}</option>] [<option>--protocol {tcp,udp,sctp}</option>] [<option>--only-rr</option>] [<option>--rootkey /PATH/TO/ROOT.KEY</option>] @@ -134,7 +134,7 @@ </refsect1> <refsect1 id='requirements'><title>REQUIREMENTS</title> -<para>tlsa requires the following python libraries: unbound, m2crypto, argparse and ipaddr</para> +<para>tlsa requires the following python libraries: unbound, cryptography, argparse and ipaddr</para> </refsect1> <refsect1 id='bugs'><title>BUGS</title> @@ -156,7 +156,7 @@ <refsect1 id='see_also'><title>SEE ALSO</title> <para><citerefentry><refentrytitle>sshfp</refentrytitle><manvolnum>1</manvolnum></citerefentry> <citerefentry><refentrytitle>ssh-keygen</refentrytitle><manvolnum>1</manvolnum></citerefentry>and RFC-6698</para> -<para><ulink url='http://people.redhat.com/pwouters/hash-slinger/'>http://people.redhat.com/pwouters/hash-slinger/</ulink></para> +<para><ulink url='https://github.com/letoams/hash-slinger'>https://github.com/letoams/hash-slinger</ulink></para> <para><ulink url='http://os3sec.org/'>http://os3sec.org/</ulink></para> </refsect1>

