[PATCH] Accept IP addresses in server certificate SANs
Hello all,
libpq currently supports server certificates with a single IP address
in the Common Name. It's fairly brittle; as far as I can tell, the
single name you choose has to match the client's address exactly.
Attached is a patch for libpq to support IP addresses in the server's
Subject Alternative Names, which would allow admins to issue certs for
multiple IP addresses, both IPv4 and IPv6, and mix them with
alternative DNS hostnames. These addresses are compared bytewise
instead of stringwise, so the client can contact the server via
alternative spellings of the same IP address.
This patch arose because I was writing tests for the NSS implementation
that used a server cert with both DNS names and IP addresses, and then
they failed when I ran those tests against the OpenSSL implementation.
NSS supports this functionality natively. Anecdotally, I've heard from
at least one client group who is utilizing IP-based certificates in
their cloud deployments. It seems uncommon but still useful.
There are two open questions I have; they're based on NSS
implementation details that I did not port here:
- NSS allows an IPv4 SAN to match an IPv6 mapping of that same address,
and vice-versa. I chose not to implement that behavior, figuring it
is easy enough for people to issue a certificate with both addresses.
Is that okay?
- If a certificate contains only iPAddress SANs, and none of them
match, I fall back to check the certificate Common Name. OpenSSL will
not do this (its X509_check_ip considers only SANs). NSS will only do
this if the client's address is itself a DNS name. The spec says that
we can't fall back to Common Name if the SANs contain any DNS
entries, but it's silent on the subject of IP addresses. What should
the behavior be?
The patchset roadmap:
- 0001 moves inet_net_pton() to src/port, since libpq will need it.
- 0002 implements the new functionality and adds tests.
WDYT?
Thanks,
--Jacob
At Thu, 16 Dec 2021 01:13:57 +0000, Jacob Champion <pchampion@vmware.com> wrote in
This patch arose because I was writing tests for the NSS implementation
that used a server cert with both DNS names and IP addresses, and then
they failed when I ran those tests against the OpenSSL implementation.
NSS supports this functionality natively. Anecdotally, I've heard from
at least one client group who is utilizing IP-based certificates in
their cloud deployments. It seems uncommon but still useful.There are two open questions I have; they're based on NSS
implementation details that I did not port here:- NSS allows an IPv4 SAN to match an IPv6 mapping of that same address,
and vice-versa. I chose not to implement that behavior, figuring it
is easy enough for people to issue a certificate with both addresses.
Is that okay?
- If a certificate contains only iPAddress SANs, and none of them
match, I fall back to check the certificate Common Name. OpenSSL will
not do this (its X509_check_ip considers only SANs). NSS will only do
this if the client's address is itself a DNS name. The spec says that
we can't fall back to Common Name if the SANs contain any DNS
entries, but it's silent on the subject of IP addresses. What should
the behavior be?The patchset roadmap:
- 0001 moves inet_net_pton() to src/port, since libpq will need it.
- 0002 implements the new functionality and adds tests.WDYT?
In RFC2828 and 6125,
In some cases, the URI is specified as an IP address rather than a
hostname. In this case, the iPAddress subjectAltName must be present
in the certificate and must exactly match the IP in the URI.
It seems like saying that we must search for iPAddress and mustn't use
CN nor dNSName if the client connected using IP address. Otherwise, if
the host name is a domain name, we use only dNSName if present, and
use CN otherwise. That behavior seems agreeing to what you wrote as
NSS's behavior. That being said it seems to me we should preserve
that behavior at least for OpenSSL as an established behavior.
In short, I think the current behavior of the patch is the direction
we would go but some documentation is may be needed.
I'm not sure about ipv4 comptible addresses. However, I think we can
identify ipv4 compatible address easily.
+ * pg_inet_net_pton() will accept CIDR masks, which we don't want to
+ * match, so skip the comparison if the host string contains a slash.
+ */
+ if (!strchr(host, '/')
+ && pg_inet_net_pton(PGSQL_AF_INET6, host, addr, -1) == 128)
If a cidr is given, pg_inet_net_pton returns a number less than 128 so
we don't need to check '/' explicity? (I'm not sure '/128' is
sensible but doesn't harm..)
regards.
--
Kyotaro Horiguchi
NTT Open Source Software Center
On 12/15/21 20:13, Jacob Champion wrote:
Hello all,
libpq currently supports server certificates with a single IP address
in the Common Name. It's fairly brittle; as far as I can tell, the
single name you choose has to match the client's address exactly.Attached is a patch for libpq to support IP addresses in the server's
Subject Alternative Names, which would allow admins to issue certs for
multiple IP addresses, both IPv4 and IPv6, and mix them with
alternative DNS hostnames. These addresses are compared bytewise
instead of stringwise, so the client can contact the server via
alternative spellings of the same IP address.
Good job, this is certainly going to be useful.
This patch arose because I was writing tests for the NSS implementation
that used a server cert with both DNS names and IP addresses, and then
they failed when I ran those tests against the OpenSSL implementation.
NSS supports this functionality natively. Anecdotally, I've heard from
at least one client group who is utilizing IP-based certificates in
their cloud deployments. It seems uncommon but still useful.There are two open questions I have; they're based on NSS
implementation details that I did not port here:- NSS allows an IPv4 SAN to match an IPv6 mapping of that same address,
and vice-versa. I chose not to implement that behavior, figuring it
is easy enough for people to issue a certificate with both addresses.
Is that okay?
Sure.
- If a certificate contains only iPAddress SANs, and none of them
match, I fall back to check the certificate Common Name. OpenSSL will
not do this (its X509_check_ip considers only SANs). NSS will only do
this if the client's address is itself a DNS name. The spec says that
we can't fall back to Common Name if the SANs contain any DNS
entries, but it's silent on the subject of IP addresses. What should
the behavior be?
I don't think we should fall back on the CN. It would seem quite odd to
do so for IP addresses but not for DNS names.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
On Thu, 2021-12-16 at 14:54 +0900, Kyotaro Horiguchi wrote:
In RFC2828 and 6125,
In some cases, the URI is specified as an IP address rather than a
hostname. In this case, the iPAddress subjectAltName must be present
in the certificate and must exactly match the IP in the URI.
Ah, right, I misremembered. Disregard my statement that the spec is
"silent on the subject", sorry.
It seems like saying that we must search for iPAddress and mustn't use
CN nor dNSName if the client connected using IP address. Otherwise, if
the host name is a domain name, we use only dNSName if present, and
use CN otherwise. That behavior seems agreeing to what you wrote as
NSS's behavior.
NSS departs slightly from the spec and will additionally try to match
an IP address against the CN, but only if there are no iPAddresses in
the SAN. It roughly matches the logic for DNS names.
Here's the description of the NSS behavior and some of the reasoning
behind it, quoted from a developer on Bugzilla [1]https://bugzilla.mozilla.org/show_bug.cgi?id=103752:
Elsewhere in RFC 2818, it says
If a subjectAltName extension of type dNSName is present, that MUST
be used as the identity. Otherwise, the (most specific) Common Name
field in the Subject field of the certificate MUST be used.Notice that this section is not conditioned upon the URI being a hostname
and not an IP address. So this statement conflicts with the one cited
above.I implemented this policy:
if the URI contains a host name
if the subject alt name is present and has one or more DNS names
use the DNS names in that extension as the server identity
else
use the subject common name as the server identity
else if the URI contains an IP address
if the subject alt name is present and has one or more IP addresses
use the IP addresses in that extension as the server identity
else
compare the URI IP address string with the subject common name.
It sounds like both you and Andrew might be comfortable with that same
behavior? I think it looks like a sane solution, so I'll implement that
and we can see what it looks like. (My work on this will be paused over
the end-of-year holidays.)
That being said it seems to me we should preserve
that behavior at least for OpenSSL as an established behavior.
That part is interesting. I'll talk more about that in my reply to
Andrew.
In short, I think the current behavior of the patch is the direction
we would go but some documentation is may be needed.
Great!
I'm not sure about ipv4 comptible addresses. However, I think we can
identify ipv4 compatible address easily.
Yeah, it would probably not be a difficult feature to add later.
+ * pg_inet_net_pton() will accept CIDR masks, which we don't want to + * match, so skip the comparison if the host string contains a slash. + */ + if (!strchr(host, '/') + && pg_inet_net_pton(PGSQL_AF_INET6, host, addr, -1) == 128)If a cidr is given, pg_inet_net_pton returns a number less than 128 so
we don't need to check '/' explicity? (I'm not sure '/128' is
sensible but doesn't harm..)
Personally I think that, if someone wants your libpq to connect to a
server with a hostname of "some:ipv6::address/128", then they are
trying to pull something (evading a poorly coded blocklist, perhaps?)
and we should not allow that to match an IP. Thoughts?
Thanks for the review!
--Jacob
On Thu, 2021-12-16 at 10:50 -0500, Andrew Dunstan wrote:
Good job, this is certainly going to be useful.
Thanks!
I don't think we should fall back on the CN. It would seem quite odd to
do so for IP addresses but not for DNS names.
So there's at least one compatibility concern with disabling the
fallback, in that there could be existing users that are happily using
a certificate with an IP address CN, and libpq is just ignoring any
iPAddress SANs that the certificate has. Once libpq becomes aware of
those, it will stop accepting the CN and the certificate might stop
working.
Personally I think that's acceptable, but it would probably warrant a
release note or some such.
I will work on implementing behavior that's modeled off of the NSS
matching logic (see my reply to Horiguchi-san), which will at least
make it more logically consistent, and we can see what that looks like?
Thanks for the review!
--Jacob
At Thu, 16 Dec 2021 18:44:54 +0000, Jacob Champion <pchampion@vmware.com> wrote in
On Thu, 2021-12-16 at 14:54 +0900, Kyotaro Horiguchi wrote:
It seems like saying that we must search for iPAddress and mustn't use
CN nor dNSName if the client connected using IP address. Otherwise, if
the host name is a domain name, we use only dNSName if present, and
use CN otherwise. That behavior seems agreeing to what you wrote as
NSS's behavior.NSS departs slightly from the spec and will additionally try to match
an IP address against the CN, but only if there are no iPAddresses in
the SAN. It roughly matches the logic for DNS names.
OpenSSL seems different. X509_check_host() tries SAN then CN iff SAN
doesn't exist. X509_check_ip() tries SAN and completely ignores
iPAdress and CN.
Here's the description of the NSS behavior and some of the reasoning
behind it, quoted from a developer on Bugzilla [1]:Elsewhere in RFC 2818, it says
If a subjectAltName extension of type dNSName is present, that MUST
be used as the identity. Otherwise, the (most specific) Common Name
field in the Subject field of the certificate MUST be used.Notice that this section is not conditioned upon the URI being a hostname
and not an IP address. So this statement conflicts with the one cited
above.I implemented this policy:
if the URI contains a host name
if the subject alt name is present and has one or more DNS names
use the DNS names in that extension as the server identity
else
use the subject common name as the server identity
else if the URI contains an IP address
if the subject alt name is present and has one or more IP addresses
use the IP addresses in that extension as the server identity
else
compare the URI IP address string with the subject common name.
(Wow. The article is 20-years old.)
*I* am fine with it.
It sounds like both you and Andrew might be comfortable with that same
behavior? I think it looks like a sane solution, so I'll implement that
and we can see what it looks like. (My work on this will be paused over
the end-of-year holidays.)
I'm not sure about ipv4 comptible addresses. However, I think we can
identify ipv4 compatible address easily.Yeah, it would probably not be a difficult feature to add later.
I agree.
+ * pg_inet_net_pton() will accept CIDR masks, which we don't want to + * match, so skip the comparison if the host string contains a slash. + */ + if (!strchr(host, '/') + && pg_inet_net_pton(PGSQL_AF_INET6, host, addr, -1) == 128)If a cidr is given, pg_inet_net_pton returns a number less than 128 so
we don't need to check '/' explicity? (I'm not sure '/128' is
sensible but doesn't harm..)Personally I think that, if someone wants your libpq to connect to a
server with a hostname of "some:ipv6::address/128", then they are
trying to pull something (evading a poorly coded blocklist, perhaps?)
and we should not allow that to match an IP. Thoughts?
If the client could connect to the network-address, it could be said
that we can assume that address is the name:p Just kidding.
As the name suggests, the function reads a network address. And the
only user is network_in(). I think we should provide pg_inet_pton()
instead of abusing pg_inet_net_pton(). inet_net_pton_*() functions
can be modified to reject /cidr part without regression so we are able
to have pg_inet_pton() with a small amount of change.
- inet_net_pton_ipv4(const char *src, u_char *dst)
+ inet_net_pton_ipv4_internal(const char *src, u_char *dst, bool netaddr)
+ inet_net_pton_ipv4(const char *src, u_char *dst)
(calls inet_net_pton_ipv4_internal(src, dst, true))
+ inet_pton_ipv4(const char *src, u_char *dst)
(calls inet_net_pton_ipv4_internal(src, dst, false))
Thanks for the review!
--Jacob
--
Kyotaro Horiguchi
NTT Open Source Software Center
Sorry for the silly mistake.
At Fri, 17 Dec 2021 15:40:10 +0900 (JST), Kyotaro Horiguchi <horikyota.ntt@gmail.com> wrote in
NSS departs slightly from the spec and will additionally try to match
an IP address against the CN, but only if there are no iPAddresses in
the SAN. It roughly matches the logic for DNS names.OpenSSL seems different. X509_check_host() tries SAN then CN iff SAN
doesn't exist. X509_check_ip() tries SAN and completely ignores
iPAdress and CN.
OpenSSL seems different. X509_check_host() tries SAN then CN iff SAN
doesn't exist. X509_check_ip() tries iPAddress and completely ignores
CN.
regards.
--
Kyotaro Horiguchi
NTT Open Source Software Center
Hi,
On 2021-12-16 01:13:57 +0000, Jacob Champion wrote:
Attached is a patch for libpq to support IP addresses in the server's
Subject Alternative Names, which would allow admins to issue certs for
multiple IP addresses, both IPv4 and IPv6, and mix them with
alternative DNS hostnames. These addresses are compared bytewise
instead of stringwise, so the client can contact the server via
alternative spellings of the same IP address.
This fails to build on windows:
https://cirrus-ci.com/task/6734650927218688?logs=build#L1029
[14:33:28.277] network.obj : error LNK2019: unresolved external symbol pg_inet_net_pton referenced in function network_in [c:\cirrus\postgres.vcxproj]
Greetings,
Andres Freund
On Fri, 2021-12-17 at 16:54 +0900, Kyotaro Horiguchi wrote:
Sorry for the silly mistake.
At Fri, 17 Dec 2021 15:40:10 +0900 (JST), Kyotaro Horiguchi <horikyota.ntt@gmail.com> wrote in
NSS departs slightly from the spec and will additionally try to match
an IP address against the CN, but only if there are no iPAddresses in
the SAN. It roughly matches the logic for DNS names.OpenSSL seems different. X509_check_host() tries SAN then CN iff SAN
doesn't exist. X509_check_ip() tries SAN and completely ignores
iPAdress and CN.OpenSSL seems different. X509_check_host() tries SAN then CN iff SAN
doesn't exist. X509_check_ip() tries iPAddress and completely ignores
CN.
Right.
On Fri, 2021-12-17 at 15:40 +0900, Kyotaro Horiguchi wrote:
+ * pg_inet_net_pton() will accept CIDR masks, which we don't want to
+ * match, so skip the comparison if the host string contains a slash. + */ + if (!strchr(host, '/') + && pg_inet_net_pton(PGSQL_AF_INET6, host, addr, -1) == 128)If a cidr is given, pg_inet_net_pton returns a number less than 128 so
we don't need to check '/' explicity? (I'm not sure '/128' is
sensible but doesn't harm..)Personally I think that, if someone wants your libpq to connect to a
server with a hostname of "some:ipv6::address/128", then they are
trying to pull something (evading a poorly coded blocklist, perhaps?)
and we should not allow that to match an IP. Thoughts?If the client could connect to the network-address, it could be said
that we can assume that address is the name:p Just kidding.As the name suggests, the function reads a network address. And the
only user is network_in(). I think we should provide pg_inet_pton()
instead of abusing pg_inet_net_pton(). inet_net_pton_*() functions
can be modified to reject /cidr part without regression so we are able
to have pg_inet_pton() with a small amount of change.- inet_net_pton_ipv4(const char *src, u_char *dst) + inet_net_pton_ipv4_internal(const char *src, u_char *dst, bool netaddr)+ inet_net_pton_ipv4(const char *src, u_char *dst) (calls inet_net_pton_ipv4_internal(src, dst, true)) + inet_pton_ipv4(const char *src, u_char *dst) (calls inet_net_pton_ipv4_internal(src, dst, false))
Sounds good, I will make that change. Thanks for the feedback!
--Jacob
On Sun, 2022-01-02 at 13:29 -0800, Andres Freund wrote:
Hi,
On 2021-12-16 01:13:57 +0000, Jacob Champion wrote:
Attached is a patch for libpq to support IP addresses in the server's
Subject Alternative Names, which would allow admins to issue certs for
multiple IP addresses, both IPv4 and IPv6, and mix them with
alternative DNS hostnames. These addresses are compared bytewise
instead of stringwise, so the client can contact the server via
alternative spellings of the same IP address.[14:33:28.277] network.obj : error LNK2019: unresolved external symbol pg_inet_net_pton referenced in function network_in [c:\cirrus\postgres.vcxproj]
Thanks for the heads up; I'll fix that while I'm implementing the
internal API.
--Jacob
On Thu, 2021-12-16 at 18:44 +0000, Jacob Champion wrote:
It sounds like both you and Andrew might be comfortable with that same
behavior? I think it looks like a sane solution, so I'll implement that
and we can see what it looks like. (My work on this will be paused over
the end-of-year holidays.)
v2 implements the discussed CN/SAN fallback behavior and should fix the
build on Windows. Still TODO is the internal pg_inet_pton() refactoring
that you asked for; I'm still deciding how best to approach it.
Changes only in since-v1.diff.txt.
Thanks,
--Jacob
Attachments:
since-v1.diff.txttext/plain; name=since-v1.diff.txtDownload+322-22
v2-0001-Move-inet_net_pton-to-src-port.patchtext/x-patch; name=v2-0001-Move-inet_net_pton-to-src-port.patchDownload+20-8
v2-0002-libpq-allow-IP-address-SANs-in-server-certs.patchtext/x-patch; name=v2-0002-libpq-allow-IP-address-SANs-in-server-certs.patchDownload+647-17
On Mon, 2022-01-03 at 16:19 +0000, Jacob Champion wrote:
On Fri, 2021-12-17 at 15:40 +0900, Kyotaro Horiguchi wrote:
+ inet_net_pton_ipv4(const char *src, u_char *dst) (calls inet_net_pton_ipv4_internal(src, dst, true)) + inet_pton_ipv4(const char *src, u_char *dst) (calls inet_net_pton_ipv4_internal(src, dst, false))Sounds good, I will make that change. Thanks for the feedback!
v3 implements a pg_inet_pton(), but for IPv6 instead of IPv4 as
presented above (since we only need inet_pton() for IPv6 in this case).
It's split into a separate patch (0003) for ease of review.
Thanks!
--Jacob
Attachments:
v3-0001-Move-inet_net_pton-to-src-port.patchtext/x-patch; name=v3-0001-Move-inet_net_pton-to-src-port.patchDownload+20-8
v3-0002-libpq-allow-IP-address-SANs-in-server-certs.patchtext/x-patch; name=v3-0002-libpq-allow-IP-address-SANs-in-server-certs.patchDownload+647-17
v3-0003-squash-libpq-allow-IP-address-SANs-in-server-cert.patchtext/x-patch; name=v3-0003-squash-libpq-allow-IP-address-SANs-in-server-cert.patchDownload+52-25
At Thu, 6 Jan 2022 00:02:27 +0000, Jacob Champion <pchampion@vmware.com> wrote in
On Mon, 2022-01-03 at 16:19 +0000, Jacob Champion wrote:
On Fri, 2021-12-17 at 15:40 +0900, Kyotaro Horiguchi wrote:
+ inet_net_pton_ipv4(const char *src, u_char *dst) (calls inet_net_pton_ipv4_internal(src, dst, true)) + inet_pton_ipv4(const char *src, u_char *dst) (calls inet_net_pton_ipv4_internal(src, dst, false))Sounds good, I will make that change. Thanks for the feedback!
v3 implements a pg_inet_pton(), but for IPv6 instead of IPv4 as
presented above (since we only need inet_pton() for IPv6 in this case).
It's split into a separate patch (0003) for ease of review.
0001 looks fine as it is in the almost same shape withinet_net_pton
about PGSQL_AF_INET and PGSQL_AF_INET6. I'm not sure about the
difference on how to handle AF_INET6 between pg_inet_net_pton and ntop
but that's not a matter of this patch.
However, 0002,
+/*
+ * In a frontend build, we can't include inet.h, but we still need to have
+ * sensible definitions of these two constants. Note that pg_inet_net_ntop()
+ * assumes that PGSQL_AF_INET is equal to AF_INET.
+ */
+#define PGSQL_AF_INET (AF_INET + 0)
+#define PGSQL_AF_INET6 (AF_INET + 1)
+
Now we have the same definition thrice in frontend code. Coulnd't we
define them in, say, libpq-fe.h or inet-fe.h (nonexistent) then
include it from the three files?
+$node->connect_fails(
+ "$common_connstr host=192.0.2.2",
+ "host not matching an IPv4 address (Subject Alternative Name 1)",
It is not the real IP address of the server.
https://datatracker.ietf.org/doc/html/rfc6125
In some cases, the URI is specified as an IP address rather than a
hostname. In this case, the iPAddress subjectAltName must be
present in the certificate and must exactly match the IP in the URI.
When IP address is embedded in URI, it won't be translated to another
IP address. Concretely https://192.0.1.5/hoge cannot reach to the host
192.0.1.8. On the other hand, as done in the test, libpq allows that
when "host=192.0.1.5 hostaddr=192.0.1.8". I can't understand what we
are doing in that case. Don't we need to match the SAN IP address
with hostaddr instead of host?
regards.
--
Kyotaro Horiguchi
NTT Open Source Software Center
On Mon, 2022-01-31 at 17:29 +0900, Kyotaro Horiguchi wrote:
However, 0002,
+/* + * In a frontend build, we can't include inet.h, but we still need to have + * sensible definitions of these two constants. Note that pg_inet_net_ntop() + * assumes that PGSQL_AF_INET is equal to AF_INET. + */ +#define PGSQL_AF_INET (AF_INET + 0) +#define PGSQL_AF_INET6 (AF_INET + 1) +Now we have the same definition thrice in frontend code. Coulnd't we
define them in, say, libpq-fe.h or inet-fe.h (nonexistent) then
include it from the three files?
I started down the inet-fe.h route, and then realized I didn't know
where that should go. Does it need to be included in (or part of)
port.h? And should it be installed as part of the logic in
src/include/Makefile?
+$node->connect_fails( + "$common_connstr host=192.0.2.2", + "host not matching an IPv4 address (Subject Alternative Name 1)",It is not the real IP address of the server.
https://datatracker.ietf.org/doc/html/rfc6125
In some cases, the URI is specified as an IP address rather than a
hostname. In this case, the iPAddress subjectAltName must be
present in the certificate and must exactly match the IP in the URI.When IP address is embedded in URI, it won't be translated to another
IP address. Concretely https://192.0.1.5/hoge cannot reach to the host
192.0.1.8. On the other hand, as done in the test, libpq allows that
when "host=192.0.1.5 hostaddr=192.0.1.8". I can't understand what we
are doing in that case. Don't we need to match the SAN IP address
with hostaddr instead of host?
I thought that host, not hostaddr, was the part that corresponded to
the URI. So in a hypothetical future where postgresqls:// exists, the
two URIs
postgresqls://192.0.2.2:5432/db
postgresqls://192.0.2.2:5432/db?hostaddr=127.0.0.1
should both be expecting the same certificate. That seems to match the
libpq documentation as well.
(Specifying a host parameter is also allowed... that seems like it
could cause problems for a hypothetical postgresqls:// scheme, but it's
probably not relevant for this thread.)
--Jacob
At Wed, 2 Feb 2022 19:46:13 +0000, Jacob Champion <pchampion@vmware.com> wrote in
On Mon, 2022-01-31 at 17:29 +0900, Kyotaro Horiguchi wrote:
+#define PGSQL_AF_INET (AF_INET + 0) +#define PGSQL_AF_INET6 (AF_INET + 1) +Now we have the same definition thrice in frontend code. Coulnd't we
define them in, say, libpq-fe.h or inet-fe.h (nonexistent) then
include it from the three files?I started down the inet-fe.h route, and then realized I didn't know
where that should go. Does it need to be included in (or part of)
port.h? And should it be installed as part of the logic in
src/include/Makefile?
I don't think it should be a part of port.h. Though I suggested
frontend-only header file by the name, isn't it enough to separate out
the definitions from utils/inet.h to common/inet-common.h then include
the inet-common.h from inet.h?
When IP address is embedded in URI, it won't be translated to another
IP address. Concretely https://192.0.1.5/hoge cannot reach to the host
192.0.1.8. On the other hand, as done in the test, libpq allows that
when "host=192.0.1.5 hostaddr=192.0.1.8". I can't understand what we
are doing in that case. Don't we need to match the SAN IP address
with hostaddr instead of host?I thought that host, not hostaddr, was the part that corresponded to
the URI. So in a hypothetical future where postgresqls:// exists, the
two URIspostgresqls://192.0.2.2:5432/db
postgresqls://192.0.2.2:5432/db?hostaddr=127.0.0.1should both be expecting the same certificate. That seems to match the
libpq documentation as well.
Hmm. Well, considering that the objective for the validation is to
check if the server is actually the client is intending to connect, it
is fine. Sorry for the noise.
(Specifying a host parameter is also allowed... that seems like it
could cause problems for a hypothetical postgresqls:// scheme, but it's
probably not relevant for this thread.)
Yeah.
regards.
--
Kyotaro Horiguchi
NTT Open Source Software Center
On Thu, 2022-02-03 at 16:23 +0900, Kyotaro Horiguchi wrote:
At Wed, 2 Feb 2022 19:46:13 +0000, Jacob Champion <pchampion@vmware.com> wrote in
On Mon, 2022-01-31 at 17:29 +0900, Kyotaro Horiguchi wrote:
+#define PGSQL_AF_INET (AF_INET + 0) +#define PGSQL_AF_INET6 (AF_INET + 1) +Now we have the same definition thrice in frontend code. Coulnd't we
define them in, say, libpq-fe.h or inet-fe.h (nonexistent) then
include it from the three files?I started down the inet-fe.h route, and then realized I didn't know
where that should go. Does it need to be included in (or part of)
port.h? And should it be installed as part of the logic in
src/include/Makefile?I don't think it should be a part of port.h. Though I suggested
frontend-only header file by the name, isn't it enough to separate out
the definitions from utils/inet.h to common/inet-common.h then include
the inet-common.h from inet.h?
That works a lot better than what I had in my head. Done that way in
v4. Thanks!
--Jacob
Attachments:
v4-0001-Move-inet_net_pton-to-src-port.patchtext/x-patch; name=v4-0001-Move-inet_net_pton-to-src-port.patchDownload+20-8
v4-0002-libpq-allow-IP-address-SANs-in-server-certs.patchtext/x-patch; name=v4-0002-libpq-allow-IP-address-SANs-in-server-certs.patchDownload+678-50
v4-0003-squash-libpq-allow-IP-address-SANs-in-server-cert.patchtext/x-patch; name=v4-0003-squash-libpq-allow-IP-address-SANs-in-server-cert.patchDownload+52-25
At Fri, 4 Feb 2022 17:06:53 +0000, Jacob Champion <pchampion@vmware.com> wrote in
That works a lot better than what I had in my head. Done that way in
v4. Thanks!
Thanks!
0002:
+#define PGSQL_AF_INET (AF_INET + 0)
+#define PGSQL_AF_INET6 (AF_INET + 1)
..
-#define PGSQL_AF_INET (AF_INET + 0)
-#define PGSQL_AF_INET6 (AF_INET + 1)
I feel this should be a part of 0001. (But the patches will be
finally merged so maybe no need to bother moving it).
* The use of inet_aton() instead of inet_pton() is deliberate; the
* latter cannot handle alternate IPv4 notations ("numbers-and-dots").
I think we should be consistent in handling IP addresses. We have
both inet_pton and inet_aton to parse IPv4 addresses.
We use inet_pton in the inet type (network_in).
We use inet_aton in server addresses.
# Hmm. I'm surprised to see listen_addresses accepts "0x7f.1".
# I think we should accept the same by network_in but it is another
# issue.
So, inet_aton there seems to be the right choice but the comment
doesn't describe the reason for that behavior. I think we should add
an explanation about the reason for the behavior, maybe something like
this:
We accept alternative IPv4 address notations that are accepted by
inet_aton but not by inet_pton as server address.
+ * GEN_IPADD is an OCTET STRING containing an IP address in network byte
+ * order.
+ /* OK to cast from unsigned to plain char, since it's all ASCII. */
+ return pq_verify_peer_name_matches_certificate_ip(conn, (const char *) addrdata, len, store_name);
Aren't the two comments contradicting each other? The retruned general
name looks like an octet array, which is not a subset of ASCII
string. So pq_verify_peer_name_matches_certificate_ip should receive
addrdata as "const unsigned char *", without casting.
+ if (name->type == host_type)
+ check_cn = false;
Don't we want a concise coment for this?
- if (*names_examined == 0)
+ if ((rc == 0) && check_cn)
To me, it seems a bit hard to understand. We can set false to
check_cn in the rc != 0 path in the loop on i, like this:
if (rc != 0) + { + /* + * don't fall back to CN when we have a match or have an error + */ + check_cn = false; break; + }
...
- if ((rc == 0) && check_cn) + if (check_cn)
The following existing code (CN fallback)
rc = openssl_verify_peer_name_matches_certificate_name(conn,
X509_NAME_ENTRY_get_data(X509_NAME_get_entry(subject_name, cn_index)),
first_name);
is expecting that first_name has not been set when it is visited.
However, with this patch, first_name can be set when the cert has any
SAN of unmatching type (DNS/IPADD) and the already-set name leaks. We
need to avoid that memory leak since the path can be visited multiple
times from the user-program of libpq. I came up with two directions.
1. Completely ignore type-unmatching entries. first_name is not set by
such entries. Such unmatching entreis doesn't increase
*names_examined.
2. Avoid overwriting first_name there.
I like 1, but since we don't make distinction between DNS and IPADDR
in the error message emited by the caller, we would take 2?
regards.
--
Kyotaro Horiguchi
NTT Open Source Software Center
On Mon, 2022-02-07 at 17:29 +0900, Kyotaro Horiguchi wrote:
At Fri, 4 Feb 2022 17:06:53 +0000, Jacob Champion <pchampion@vmware.com> wrote in
That works a lot better than what I had in my head. Done that way in
v4. Thanks!Thanks!
0002:
+#define PGSQL_AF_INET (AF_INET + 0) +#define PGSQL_AF_INET6 (AF_INET + 1) .. -#define PGSQL_AF_INET (AF_INET + 0) -#define PGSQL_AF_INET6 (AF_INET + 1)I feel this should be a part of 0001. (But the patches will be
finally merged so maybe no need to bother moving it).
Okay. I can move it easily if you feel like it would help review, but
for now I've kept it in 0002.
* The use of inet_aton() instead of inet_pton() is deliberate; the
* latter cannot handle alternate IPv4 notations ("numbers-and-dots").I think we should be consistent in handling IP addresses. We have
both inet_pton and inet_aton to parse IPv4 addresses.We use inet_pton in the inet type (network_in).
We use inet_aton in server addresses.# Hmm. I'm surprised to see listen_addresses accepts "0x7f.1".
# I think we should accept the same by network_in but it is another
# issue.
Yeah, that's an interesting inconsistency.
So, inet_aton there seems to be the right choice but the comment
doesn't describe the reason for that behavior. I think we should add
an explanation about the reason for the behavior, maybe something like
this:We accept alternative IPv4 address notations that are accepted by
inet_aton but not by inet_pton as server address.
I've pulled this wording into the comment in v5, attached.
+ * GEN_IPADD is an OCTET STRING containing an IP address in network byte + * order.+ /* OK to cast from unsigned to plain char, since it's all ASCII. */ + return pq_verify_peer_name_matches_certificate_ip(conn, (const char *) addrdata, len, store_name);Aren't the two comments contradicting each other? The retruned general
name looks like an octet array, which is not a subset of ASCII
string. So pq_verify_peer_name_matches_certificate_ip should receive
addrdata as "const unsigned char *", without casting.
Bad copy-paste on my part; thanks for the catch. Fixed.
+ if (name->type == host_type) + check_cn = false;Don't we want a concise coment for this?
Added one; see what you think.
- if (*names_examined == 0) + if ((rc == 0) && check_cn)To me, it seems a bit hard to understand. We can set false to
check_cn in the rc != 0 path in the loop on i, like this:if (rc != 0) + { + /* + * don't fall back to CN when we have a match or have an error + */ + check_cn = false; break; + }...
- if ((rc == 0) && check_cn) + if (check_cn)
If I understand right, that's not quite equivalent (and the new tests
fail if I implement it that way). We have to disable fallback if the
SAN exists, whether it matches or not.
The following existing code (CN fallback)
rc = openssl_verify_peer_name_matches_certificate_name(conn,
X509_NAME_ENTRY_get_data(X509_NAME_get_entry(subject_name, cn_index)),
first_name);is expecting that first_name has not been set when it is visited.
However, with this patch, first_name can be set when the cert has any
SAN of unmatching type (DNS/IPADD) and the already-set name leaks. We
need to avoid that memory leak since the path can be visited multiple
times from the user-program of libpq. I came up with two directions.1. Completely ignore type-unmatching entries. first_name is not set by
such entries. Such unmatching entreis doesn't increase
*names_examined.2. Avoid overwriting first_name there.
I like 1, but since we don't make distinction between DNS and IPADDR
in the error message emited by the caller, we would take 2?
Great catch, thanks! I implemented option 2 to start. Option 1 might
make things difficult to debug if you're connecting to a server by IP
address but its certificate only has DNS names.
Thanks!
--Jacob
Attachments:
since-v4.diff.txttext/plain; name=since-v4.diff.txtDownload+25-7
v5-0001-Move-inet_net_pton-to-src-port.patchtext/x-patch; name=v5-0001-Move-inet_net_pton-to-src-port.patchDownload+20-8
v5-0002-libpq-allow-IP-address-SANs-in-server-certs.patchtext/x-patch; name=v5-0002-libpq-allow-IP-address-SANs-in-server-certs.patchDownload+697-51
v5-0003-squash-libpq-allow-IP-address-SANs-in-server-cert.patchtext/x-patch; name=v5-0003-squash-libpq-allow-IP-address-SANs-in-server-cert.patchDownload+52-25
(This needs rebasing)
At Wed, 9 Feb 2022 00:52:48 +0000, Jacob Champion <pchampion@vmware.com> wrote in
On Mon, 2022-02-07 at 17:29 +0900, Kyotaro Horiguchi wrote:
I feel this should be a part of 0001. (But the patches will be
finally merged so maybe no need to bother moving it).Okay. I can move it easily if you feel like it would help review, but
for now I've kept it in 0002.
Thanks.
So, inet_aton there seems to be the right choice but the comment
doesn't describe the reason for that behavior. I think we should add
an explanation about the reason for the behavior, maybe something like
this:We accept alternative IPv4 address notations that are accepted by
inet_aton but not by inet_pton as server address.I've pulled this wording into the comment in v5, attached.
+ if (name->type == host_type) + check_cn = false;Don't we want a concise coment for this?
Added one; see what you think.
That's fine with me.
if (rc != 0) + { + /* + * don't fall back to CN when we have a match or have an error + */ + check_cn = false; break; + }...
- if ((rc == 0) && check_cn) + if (check_cn)If I understand right, that's not quite equivalent (and the new tests
fail if I implement it that way). We have to disable fallback if the
SAN exists, whether it matches or not.
# I forgot to mention that, the test fails for me even without the
# change. I didn't checked what is wrong there, though.
Mmm. after the end of the loop, rc is non-zero only when the loop has
exited by the break and otherwise rc is zero. Why isn't it equivalent
to setting check_cn to false at the break?
Anyway, apart from that detail, I reconfirmed the spec the patch is
going to implement.
* If connhost contains a DNS name, and the certificate's SANs contain any
* dNSName entries, then we'll ignore the Subject Common Name entirely;
* otherwise, we fall back to checking the CN. (This behavior matches the
* RFC.)
Sure.
* If connhost contains an IP address, and the SANs contain iPAddress
* entries, we again ignore the CN. Otherwise, we allow the CN to match,
* EVEN IF there is a dNSName in the SANs. (RFC 6125 prohibits this: "A
* client MUST NOT seek a match for a reference identifier of CN-ID if the
* presented identifiers include a DNS-ID, SRV-ID, URI-ID, or any
* application-specific identifier types supported by the client.")
Actually the patch searches for a match of IP address connhost from
dNSName SANs even if iPAddress SANs exist. I think we've not
explicitly defined thebehavior in that case. I supposed that we only
be deviant in the case "IP address connhost and no SANs of any type
exists". What do you think about it?
- For the certificate that have only dNSNames or no SANs presented, we
serach for a match from all dNSNames if any or otherwise try CN
regardless of the type of connhost.
- Otherwise (the cert has at least one iPAddress SANs) we follow the RFCs.
- For IP-addr connhost, we search only the iPAddress SANs.
- For DNSName connhost, we search only dNSName SANs if any or
otherwise try CN.
Honestly I didn't consider to that detail. On second thought, with
this specification we cannot decide the behavior unless we scanned all
SANs. Maybe we can find an elegant implement but I don't think people
here would welcome even that level of complexity needed only for that
dubious existing use case.
What do you think about this? And I'd like to hear from others.
Great catch, thanks! I implemented option 2 to start. Option 1 might
make things difficult to debug if you're connecting to a server by IP
address but its certificate only has DNS names.
Looks fine. Thanks!
regards.
--
Kyotaro Horiguchi
NTT Open Source Software Center
On Tue, 2022-02-15 at 15:16 +0900, Kyotaro Horiguchi wrote:
(This needs rebasing)
Done in v6, attached.
# I forgot to mention that, the test fails for me even without the
# change. I didn't checked what is wrong there, though.
Ah. We should probably figure that out, then -- what failures do you
see?
Mmm. after the end of the loop, rc is non-zero only when the loop has
exited by the break and otherwise rc is zero. Why isn't it equivalent
to setting check_cn to false at the break?
check_cn can be false if rc is zero, too; it means that we found a SAN
of the correct type but it didn't match.
Anyway, apart from that detail, I reconfirmed the spec the patch is
going to implement.* If connhost contains a DNS name, and the certificate's SANs contain any
* dNSName entries, then we'll ignore the Subject Common Name entirely;
* otherwise, we fall back to checking the CN. (This behavior matches the
* RFC.)Sure.
* If connhost contains an IP address, and the SANs contain iPAddress
* entries, we again ignore the CN. Otherwise, we allow the CN to match,
* EVEN IF there is a dNSName in the SANs. (RFC 6125 prohibits this: "A
* client MUST NOT seek a match for a reference identifier of CN-ID if the
* presented identifiers include a DNS-ID, SRV-ID, URI-ID, or any
* application-specific identifier types supported by the client.")Actually the patch searches for a match of IP address connhost from
dNSName SANs even if iPAddress SANs exist. I think we've not
explicitly defined thebehavior in that case.
That's a good point; I didn't change the prior behavior. I feel more
comfortable leaving that check, since it is technically possible to
push something that looks like an IP address into a dNSName SAN. We
should probably make an explicit decision on that, as you say.
But I don't think that contradicts the code comment, does it? The
comment is just talking about CN fallback scenarios. If you find a
match in a dNSName, there's no reason to fall back to the CN.
I supposed that we only
be deviant in the case "IP address connhost and no SANs of any type
exists". What do you think about it?
We fall back in the case of "IP address connhost and dNSName SANs
exist", which is prohibited by that part of RFC 6125. I don't think we
deviate in the case you described; can you explain further?
- For the certificate that have only dNSNames or no SANs presented, we
serach for a match from all dNSNames if any or otherwise try CN
regardless of the type of connhost.
Correct. (I don't find that way of dividing up the cases very
intuitive, though.)
- Otherwise (the cert has at least one iPAddress SANs) we follow the RFCs.
- For IP-addr connhost, we search only the iPAddress SANs.
We search the dNSNames as well, as you pointed out above. But we don't
fall back to the CN.
- For DNSName connhost, we search only dNSName SANs if any or
otherwise try CN.
Effectively, yes. (We call the IP address verification functions too,
to get alt_name, but they can't match. If that's too confusing, we'd
need to pull the alt_name handling up out of the verification layer.)
Honestly I didn't consider to that detail. On second thought, with
this specification we cannot decide the behavior unless we scanned all
SANs.
Right.
Maybe we can find an elegant implement but I don't think people
here would welcome even that level of complexity needed only for that
dubious existing use case.
Which use case do you mean?
What do you think about this? And I'd like to hear from others.
I think we need to decide whether or not to keep the current "IP
address connhost can match a dNSName SAN" behavior, and if so I need to
add it to the test cases. (And we need to figure out why the tests are
failing in your build, of course.)
Thanks!
--Jacob