如何显示证书的主题替代名称?

我找到的最接近的答案是使用“ grep”。

> openssl x509 -text -noout -in cert.pem | grep DNS

还有更好的方法吗? 我只喜欢命令行。

谢谢。

133570 次浏览

Note that you can limit the output of -text to just the extensions by adding the following option:

-certopt no_subject,no_header,no_version,no_serial,no_signame,no_validity,no_issuer,no_pubkey,no_sigdump,no_aux

i.e.:

openssl x509 -text -noout -in cert.pem \
-certopt no_subject,no_header,no_version,no_serial,no_signame,no_validity,no_issuer,no_pubkey,no_sigdump,no_aux

However, you'll still need to apply some text parsing logic to get just the Subject Alternative Name.

If that isn't sufficient, I think you'll need to write a small program that uses the openssl library to extract the specific field you are looking for. Here are some example programs that show how to parse a cert, including extracting extension fields such as Subject Alternative Name:

https://zakird.com/2013/10/13/certificate-parsing-with-openssl

Note that you don't have to use openssl and C if you go the programming route... you can pick your favorite language and ASN.1 parser library, and use that. For example, in Java, you could use http://jac-asn1.sourceforge.net/, and many others.

You can use awk to get closer to the SAN, piping the above options into the awk statement:

openssl x509 -in mycertfile.crt -text -noout \
-certopt no_subject,no_header,no_version,no_serial,no_signame,no_validity,no_subject,no_issuer,no_pubkey,no_sigdump,no_aux \
| awk '/X509v3 Subject Alternative Name/','/X509v3 Basic Constraints/'

Fetch Certificate Data

With gnutls and certtool

$ gnutls-cli example.com -p 443 --print-cert < /dev/null | certtool -i | grep -C3 -i dns

With openssl

Taken from https://stackoverflow.com/a/13128918/1695680

$ openssl s_client -connect example.com:443 < /dev/null | openssl x509 -noout -text | grep -C3 -i dns

Extracting Certificate Data

| grep -C3 -i dns works for a simple-case, if your reviewing this data by hand sure works well enough. However certificate data is hierarchical, not line-oriented (so greping will be messy, particularly for ca chains).

I don't know of any x509 command line tools that can do key-value extraction, most systems I work with have python on-box or nearby so here is an approach using python, x509 interface provided by cryptography on pypi. Using cryptography is a little verbose, I didn't feel comfortable condensing this into a oneliner, but with this script you can extract dns names from certificates passed to stdin

#!/usr/bin/env python3


import sys


import cryptography.x509
import cryptography.hazmat.backends
import cryptography.hazmat.primitives


DEFAULT_FINGERPRINT_HASH = cryptography.hazmat.primitives.hashes.SHA256




def _x509_san_dns_names(certificate):
""" Return a list of strings containing san dns names
"""
crt_san_data = certificate.extensions.get_extension_for_oid(
cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME
)


dns_names = crt_san_data.value.get_values_for_type(
cryptography.x509.DNSName
)


return dns_names




def _find_certificate_pem(stream):
""" Yield hunks of pem certificates
"""
certificate_pem = []
begin_certificate = False
for line in stream:
if line == b'-----END CERTIFICATE-----\n':
begin_certificate = False
certificate_pem.append(line)
yield b''.join(certificate_pem)
certificate_pem = []


if line == b'-----BEGIN CERTIFICATE-----\n':
begin_certificate = True


if begin_certificate:
certificate_pem.append(line)




def _dump_stdincert_san_dnsnames():
""" Print line-oriented certificate fingerprint and san dns name
"""
for certificate_pem in _find_certificate_pem(sys.stdin.buffer):
certificate = cryptography.x509.load_pem_x509_certificate(
certificate_pem,
cryptography.hazmat.backends.default_backend()
)
certificate_fingerprint = certificate.fingerprint(
DEFAULT_FINGERPRINT_HASH(),
)
certificate_fingerprint_str = ':'.join(
'{:02x}'.format(i) for i in certificate_fingerprint
)
try:
for dns_name in _x509_san_dns_names(certificate):
sys.stdout.write('{} {}\n'.format(certificate_fingerprint_str, dns_name))


except cryptography.x509.extensions.ExtensionNotFound:
sys.stderr.write('{} Certificate has no extension SubjectAlternativeName\n'.format(certificate_fingerprint_str))




def main():
_dump_stdincert_san_dnsnames()




if __name__ == '__main__':
main()


#### Example
$ true | openssl s_client -connect localhost:8443 | openssl x509 -noout -text | grep DNS:
depth=2 C = US, ST = NC, L = SomeCity, O = SomeCompany Security, OU = SomeOU, CN = SomeCN
verify error:num=19:self signed certificate in certificate chain
DONE
DNS:localhost, DNS:127.0.0.1, DNS:servername1.somedom.com, DNS:servername2.somedom.local

How to display the Subject Alternative Name of a certificate?

There could be multiple SANs in a X509 certificate. The following is from the OpenSSL wiki at SSL/TLS Client. It loops over the names and prints them.

You get the X509* from a function like SSL_get_peer_certificate from a TLS connection, d2i_X509 from memory or PEM_read_bio_X509 from the filesystem.

void print_san_name(const char* label, X509* const cert)
{
int success = 0;
GENERAL_NAMES* names = NULL;
unsigned char* utf8 = NULL;


do
{
if(!cert) break; /* failed */


names = X509_get_ext_d2i(cert, NID_subject_alt_name, 0, 0 );
if(!names) break;


int i = 0, count = sk_GENERAL_NAME_num(names);
if(!count) break; /* failed */


for( i = 0; i < count; ++i )
{
GENERAL_NAME* entry = sk_GENERAL_NAME_value(names, i);
if(!entry) continue;


if(GEN_DNS == entry->type)
{
int len1 = 0, len2 = -1;


len1 = ASN1_STRING_to_UTF8(&utf8, entry->d.dNSName);
if(utf8) {
len2 = (int)strlen((const char*)utf8);
}


if(len1 != len2) {
fprintf(stderr, "  Strlen and ASN1_STRING size do not match (embedded null?): %d vs %d\n", len2, len1);
}


/* If there's a problem with string lengths, then     */
/* we skip the candidate and move on to the next.     */
/* Another policy would be to fails since it probably */
/* indicates the client is under attack.              */
if(utf8 && len1 && len2 && (len1 == len2)) {
fprintf(stdout, "  %s: %s\n", label, utf8);
success = 1;
}


if(utf8) {
OPENSSL_free(utf8), utf8 = NULL;
}
}
else
{
fprintf(stderr, "  Unknown GENERAL_NAME type: %d\n", entry->type);
}
}


} while (0);


if(names)
GENERAL_NAMES_free(names);


if(utf8)
OPENSSL_free(utf8);


if(!success)
fprintf(stdout, "  %s: <not available>\n", label);


}

There is my solution (using and ):

first

sed -ne '
s/^\( *\)[Ss]ubject[:=] */  \1/p;
/X509v3 Subject Alternative Name/{
N;
s/^.*\n//;
:a;
s/^\( *\)\(.*\), /\1\2\n\1/;
ta;
p;
q;
}' < <(openssl x509 -in cert.pem -noout -subject -ext subjectAltName)

could be written:

sed -ne 's/^\( *\)[Ss]ubject[=:] */  \1/p;/X509v3 Subj.*Alt.*Name/{
N;s/^.*\n//;:a;s/^\( *\)\(.*\), /\1\2\n\1/;ta;p;q; }' < <(
openssl x509 -in cert.pem -noout -subject -ext subjectAltName)

and could render something like:

         CN=www.example.com
DNS:il0001.sample.com
DNS:example.com
DNS:demodomain.com
DNS:testsite.com
DNS:www.il0001.sample.com
DNS:www.il0001.sample.com.vsite.il0001.sample.com
DNS:www.example.com
DNS:www.example.com.vsite.il0001.sample.com
DNS:www.demodomain.com
DNS:www.demodomain.com.vsite.il0001.sample.com
DNS:www.testsite.com
DNS:www.testsite.com.vsite.il0001.sample.com

Same for live server

sed -ne 's/^\( *\)[Ss]ubject[=:] */  \1/p;/X509v3 Subject Alternative Name/{
N;s/^.*\n//;:a;s/^\( *\)\(.*\), /\1\2\n\1/;ta;p;q; }' < <(
openssl x509 -noout -subject -ext subjectAltName -in <(
openssl s_client -ign_eof 2>/dev/null <<<$'HEAD / HTTP/1.0\r\n\r' \
-connect google.com:443 ) )

May output:

         C=US, ST=California, L=Mountain View, O=Google Inc, CN=*.google.com
DNS:*.google.com
DNS:*.android.com
DNS:*.appengine.google.com
DNS:*.cloud.google.com
DNS:*.gcp.gvt2.com
DNS:*.google-analytics.com
DNS:*.google.ca
DNS:*.google.cl
DNS:*.google.co.in
DNS:*.google.co.jp
DNS:*.google.co.uk
DNS:*.google.com.ar
DNS:*.google.com.au
DNS:*.google.com.br
DNS:*.google.com.co
DNS:*.google.com.mx
DNS:*.google.com.tr
DNS:*.google.com.vn
DNS:*.google.de
DNS:*.google.es
DNS:*.google.fr
DNS:*.google.hu
DNS:*.google.it
DNS:*.google.nl
DNS:*.google.pl
DNS:*.google.pt
DNS:*.googleadapis.com
DNS:*.googleapis.cn
DNS:*.googlecommerce.com
DNS:*.googlevideo.com
DNS:*.gstatic.cn
DNS:*.gstatic.com
DNS:*.gvt1.com
DNS:*.gvt2.com
DNS:*.metric.gstatic.com
DNS:*.urchin.com
DNS:*.url.google.com
DNS:*.youtube-nocookie.com
DNS:*.youtube.com
DNS:*.youtubeeducation.com
DNS:*.ytimg.com
DNS:android.clients.google.com
DNS:android.com
DNS:developer.android.google.cn
DNS:g.co
DNS:goo.gl
DNS:google-analytics.com
DNS:google.com
DNS:googlecommerce.com
DNS:urchin.com
DNS:www.goo.gl
DNS:youtu.be
DNS:youtube.com
DNS:youtubeeducation.com

POSIX now

As < <(...) is a bashism, same command have to be written:

openssl x509 -in cert.pem -noout -text | sed -ne '
s/^\( *\)Subject:/\1/p;
/X509v3 Subject Alternative Name/{
N;
s/^.*\n//;
:a;
s/^\( *\)\(.*\), /\1\2\n\1/;
ta;
p;
q;
}'

and

printf 'HEAD / HTTP/1.0\r\n\r\n' |
openssl s_client -ign_eof 2>/dev/null -connect google.com:443 |
openssl x509 -noout -subject -ext subjectAltName |
sed -ne 's/^\( *\)[Ss]ubject[=:] */  \1/p;/X509v3 Subj.*Alt.*Name/{
N;s/^.*\n//;:a;s/^\( *\)\(.*\), /\1\2\n\1/;ta;p;q; }'

Full bash script on another answer

Have a look at bottom of How to determine SSL cert expiration date from a PEM encoded certificate?!!

Very simple solution using grep

openssl x509 -in /path/to/x509/cert -noout -text|grep -oP '(?<=DNS:|IP Address:)[^,]+'|sort -uV

For the google certificate, this outputs:

android.clients.google.com
android.com
developer.android.google.cn
g.co
goo.gl
google.com
googlecommerce.com
google-analytics.com
hin.com
urchin.com
www.goo.gl
youtu.be
youtube.com
youtubeeducation.com
*.android.com
*.appengine.google.com
*.cloud.google.com
*.gcp.gvt2.com
*.googleadapis.com
*.googleapis.cn
*.googlecommerce.com
*.googlevideo.com
*.google.ca
*.google.cl
*.google.com
*.google.com.ar
*.google.com.au
*.google.com.br
*.google.com.co
*.google.com.mx
*.google.com.tr
*.google.com.vn
*.google.co.in
*.google.co.jp
*.google.co.uk
*.google.de
*.google.es
*.google.fr
*.google.hu
*.google.it
*.google.nl
*.google.pl
*.google.pt
*.gstatic.cn
*.gstatic.com
*.gvt1.com
*.gvt2.com
*.metric.gstatic.com
*.urchin.com
*.url.google.com
*.youtubeeducation.com
*.youtube.com
*.ytimg.com
*.google-analytics.com
*.youtube-nocookie.com

An improved awk-based solution (hat-tip: @RandomW):

openssl x509 -in certfile -text -noout \
-certopt no_header,no_version,no_serial,no_signame,no_validity,no_issuer,no_pubkey,no_sigdump,no_aux \
| awk '/X509v3 Subject Alternative Name:/ {san=1;next}
san && /^ *X509v3/ {exit}
san { sub(/DNS:/,"",$1);print $1}'

This prints out a list, as do the grep and sed solutions also found here. The difference is that there is a tighter control of where the information is found. Should the output format ever change, this version is more robust and will better hand the changes. Only the text between "Subject Alternative Name" and the very next "X509v3" section are printed out, and all optional preceding "DNS:" text is stripped out.

android.clients.google.com
android.com
developer.android.google.cn
g.co
goo.gl
...

Adding a python alternative. Prerequisite is that you have a string with the "DNS:" records.

Fetch the certificate details (subprocess, OpenSSL module, etc) dnsstring contains the "DNS:" line of the "openssl" output. Example of how to get the string of DNS names from a text output of the certificate.

for idx, line in enumerate(certoutput.split()):
if ' X509v3 Authority Key Identifier:' in line:
dnsstring = certoutput.split()[idx + 1]


# Get a list
[x.replace('DNS:', '').replace(',', '') for x in dnsstring]


# Format to a comma separated string
', '.join([x.replace('DNS:', '').replace(',', '') for x in dnsstring])

A command line example:

true | \
openssl s_client -showcerts -connect google.com:443 2>/dev/null | \
openssl x509 -noout -text 2>/dev/null | grep " DNS:" | \
python -c"import sys; print ', '.join([x.replace('DNS:', '').replace(',', '') for x in sys.stdin.readlines()[0].split()])"

Output:

*.google.com, *.android.com, <etc>

Maybe this is enough:

openssl x509 -in cert.pem -noout -text -certopt ca_default,no_sigdump

Newer versions of openssl have an '-ext' option that allows you to print only the subjectAltName record. Am using 'OpenSSL 1.1.1b' on Debian 9.9

openssl x509 -noout -ext subjectAltName -in cert.pem

Though you'll still need to parse the output.

The change was made in https://github.com/openssl/openssl/issues/3932

Here's how we can do this via awk.

'/Subject: C=/{printf $NF"\n"} matches any line which has pattern /Subject: C= and {printf $NF"\n"} simply prints the last field with a newline.

/DNS:/{x=gsub(/ *DNS:/, ""); printf "SANS=" $0"\n"} matches line with pattern DNS:. gsub is used to replace unwanted DNS: before every fqdn. printf "SANS="$0"\n" prints the whole line with a newline.

➤ echo | openssl s_client -connect google.com:443 2>&1 | openssl x509 -noout -text |  awk '/Subject: C=/{printf $NF"\n"} /DNS:/{x=gsub(/ *DNS:/, ""); printf "SANS=" $0"\n"}'
CN=*.google.com
SANS=*.google.com,*.android.com,*.appengine.google.com,*.cloud.google.com,*.crowdsource.google.com,*.g.co,*.gcp.gvt2.com,*.gcpcdn.gvt1.com,*.ggpht.cn,*.gkecnapps.cn,*.google-analytics.com,*.google.ca,*.google.cl,*.google.co.in,*.google.co.jp,*.google.co.uk,*.google.com.ar,*.google.com.au,*.google.com.br,*.google.com.co,*.google.com.mx,*.google.com.tr,*.google.com.vn,*.google.de,*.google.es,*.google.fr,*.google.hu,*.google.it,*.google.nl,*.google.pl,*.google.pt,*.googleadapis.com,*.googleapis.cn,*.googlecnapps.cn,*.googlecommerce.com,*.googlevideo.com,*.gstatic.cn,*.gstatic.com,*.gstaticcnapps.cn,*.gvt1.com,*.gvt2.com,*.metric.gstatic.com,*.urchin.com,*.url.google.com,*.wear.gkecnapps.cn,*.youtube-nocookie.com,*.youtube.com,*.youtubeeducation.com,*.youtubekids.com,*.yt.be,*.ytimg.com,android.clients.google.com,android.com,developer.android.google.cn,developers.android.google.cn,g.co,ggpht.cn,gkecnapps.cn,goo.gl,google-analytics.com,google.com,googlecnapps.cn,googlecommerce.com,source.android.google.cn,urchin.com,www.goo.gl,youtu.be,youtube.com,youtubeeducation.com,youtubekids.com,yt.be


➤