user180574
user180574

Reputation: 6084

How to display the Subject Alternative Name of a certificate?

The closest answer that I found is using "grep".

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

Is there better way to do this? I only prefer command line.

Thanks.

Upvotes: 104

Views: 195072

Answers (13)

Faisal
Faisal

Reputation: 61

Use the openssl's -ext option, refer here for openssl x509 command options and here for available -extensions flags you can use.

Sample output:

$ openssl x509 -noout -ext subjectAltName -in /etc/core/.pki/kong.pem 
X509v3 Subject Alternative Name: 
        DNS:localhost, DNS:dr.dev.local, DNS:pnpserver.dev.local, DNS:kong, DNS:kong.core-system, DNS:kong.core-system.svc, DNS:kong.core-system.svc.cluster, DNS:kong.core-system.svc.cluster.local, DNS:kong-frontend, DNS:kong-frontend.core-system, DNS:kong-frontend.core-system.svc, DNS:kong-frontend.core-system.svc.cluster, DNS:kong-frontend.core-system.svc.cluster.local, IP Address:196.196.196.101, IP Address:10.23.214.43, IP Address:192.168.101.99, IP Address:192.168.101.100, IP Address:196.196.196.100, IP Address:10.23.214.44

dd

Upvotes: 6

NerdyDeeds
NerdyDeeds

Reputation: 434

Parsed into an array

Just one more option if anyone prefers. This will capture any number of Alt Names, "push"ing each into a Bash array. Below the parsing code, I'll explain how to retrieve any single entry, all the entries, the count of the entries, and explain the command pipe by pipe.

Lets assume we have a cert that contains:

[...]
X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Subject Alternative Name:
                IP Address:127.0.0.1, IP Address:10.0.2.2, DNS:localhost
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
[...]

(with IP Address:127.0.0.1, IP Address:10.0.2.2, DNS:localhost, of course, being our targets)

Parse code

$ ALT_NAMES=($(openssl x509 -in cert.pem -noout -text | grep -A 1 "Subject Alternative Name" | tail -1 | sed -E "s/ //g" | tr "," "\n" | sed -E "s/^.*:(\S*)/\1/g" | tr "\n" " "))

$ # Let's see what we got
$ echo "${ALT_NAMES[*]}"
  127.0.0.1 10.0.2.2 localhost

$ # Hey, great! How about just the second one?
$ echo "${ALT_NAMES[1]}"     # (Zero-indexed array)
  10.0.2.2

$ # Okay... total count?
$ echo "${#ALT_NAMES[*]}"
  3

How's that go again?

# Defining an array ( VAR=(<space separated values>) )...
ALT_NAMES=([...]

   # ... expand the following expression into the contents of said array...
         $(

   # ... the OpenSSL command we're using to read the cert...
         openssl x509 -in cert.pem -noout -text | 

   # ... grep for the Alt Names. The "-A 1" (read: -A(fter) 1) gets the next line after our match, too...
         grep -A 1 "Subject Alternative Name" | 
#result: "            X509v3 Subject Alternative Name:
                         IP Address:127.0.0.1, IP Address:10.0.2.2, DNS:localhost"

   # ... (that extra line being the only one we actually wanted anyway)...
         tail -1 | 
#result: "               IP Address:127.0.0.1, IP Address:10.0.2.2, DNS:localhost"

   # ... strip all whitespace...
         sed -E "s/ //g" | 
#result: "IPAddress:127.0.0.1,IPAddress:10.0.2.2,DNS:localhost"

   # ... Text Replace the commas with new lines...
         tr "," "\n" | 
#result: "IPAddress:127.0.0.1
          IPAddress:10.0.2.2
          DNS:localhost"

   # ... so we can discard all the text from start of each line to ":"...
         sed -E "s/^.*:(\S*)/\1/g" | 
#result: "127.0.0.1
          10.0.2.2
          localhost"

   # ... then knock them all back to a single, space-delimited line...
         tr "\n" " "
#result: "127.0.0.1 10.0.2.2 localhost"

   # ... before terminating our expansion...
         )
#result: "127.0.0.1 10.0.2.2 localhost"

# ... and finally conclude by ending the array declaration.
)
#result: "ALT_NAMES=(127.0.0.1 10.0.2.2 localhost)"

TLDR

$ ALT_NAMES=($(openssl x509 -in cert.pem -noout -text | grep -A 1 "Subject Alternative Name" | tail -1 | sed -E "s/ //g" | tr "," "\n" | sed -E "s/^.*:(\S*)/\1/g" | tr "\n" " "))
$ printf "Number of entries: ${#ALT_NAMES[*]}\nAll entry values: ${ALT_NAMES[*]}\nSingle entry: ${ALT_NAMES[1]}"

# Outputs:
    Number of entries: 3
    All entry values: 127.0.0.1 10.0.2.2 localhost
    Single entry: 10.0.2.2

I know this is way late to the party... but maybe it'll help someone with a similar need someday. <3

(Edit: Hey, sooo... I'm no Bash guru... I just use it a lot. If someone has a better way to perform this or streamline/improve it, please: drop me a comment below? I love to learn new techniques!)

Upvotes: 0

F. Hauri  - Give Up GitHub
F. Hauri - Give Up GitHub

Reputation: 70722

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?!!

Upvotes: 14

ThorSummoner
ThorSummoner

Reputation: 18089

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

Upvotes: 43

shadyabhi
shadyabhi

Reputation: 17234

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

➤

Upvotes: 0

Ely
Ely

Reputation: 791

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

Upvotes: 79

NOZUONOHIGH
NOZUONOHIGH

Reputation: 2006

Maybe this is enough:

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

Upvotes: 0

sastorsl
sastorsl

Reputation: 2135

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>

Upvotes: 0

Otheus
Otheus

Reputation: 1032

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
...

Upvotes: 0

Raman
Raman

Reputation: 19565

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.

Upvotes: 67

ThinGuy
ThinGuy

Reputation: 71

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

Upvotes: 7

RandomW
RandomW

Reputation: 9

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/'

Upvotes: 1

jww
jww

Reputation: 102205

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);

}

Upvotes: 4

Related Questions