On 2022-12-15 20:08 +01, Theo Buehler <[email protected]> wrote:
> I would appreciate some testing by people who actually use acme-client
> with multiple SANs. The diff works for me and should not change any
> important behavior.
>
> When I learned about CVE-2021-44532 in node, I was horrified, but oh,
> well, it was node. Little did I suspect that acme-client did something
> rather similar (I had even touched some of this). It's also not really
> security relevant here. Still gross.
>
> Instead of having libcrypto turn the SANs into an undocumented string
> (via X509V3_EXT_print and i2v_GENERAL_NAMES if you really need to know),
> then tokenizing and parsing that, simply grab the relevant info out of
> libcrypto. In detail:
>
> 1. Cache extensions up front. This ensures that the cert has at least
>    somewhat good extensions, in particular most extensions including the
>    SAN can't occur more than once. This trick is standard but not well
>    documented (I have sent out a diff for this to the relevant parties).
>
> 2. Let libcrypto walk the extensions and hand us the list of SANs rather
>    than finding the SANs by hand. We then have a data structure to work
>    with, so the first for loop can go.
>
> 3. Extract the DNS names and compare them to the list passed in.
>    What lands in name_buf is exactly what's printed after "DNS:",
>    so we can minimally modify the second for loop. I do not want to
>    rely on ASN1_IA5STRING being NUL-terminated (they currently are),
>    hence the additional length check and the switch from %s to %.*s.
>
> Finally, I have added two comments that suggest using strnvis().
> libcrypto doesn't do that, but at this point the cert has been
> parsed from disk but we don't know whether the SANs ever went
> through x509_constraints_valid_sandns() or similar successfully.
> Anyway, that's better left to a follow up.
>
> Index: revokeproc.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/revokeproc.c,v
> retrieving revision 1.23
> diff -u -p -r1.23 revokeproc.c
> --- revokeproc.c      15 Dec 2022 17:36:56 -0000      1.23
> +++ revokeproc.c      15 Dec 2022 17:38:18 -0000
> @@ -61,19 +61,15 @@ int
>  revokeproc(int fd, const char *certfile, int force,
>      int revocate, const char *const *alts, size_t altsz)
>  {
> +     GENERAL_NAMES                   *sans = NULL;
>       char                            *der = NULL, *dercp, *der64 = NULL;
> -     char                            *san = NULL, *str, *tok;
> -     int                              rc = 0, cc, i, ssz, len;
> +     int                              rc = 0, cc, i, len;
>       size_t                          *found = NULL;
> -     BIO                             *bio = NULL;
>       FILE                            *f = NULL;
>       X509                            *x = NULL;
>       long                             lval;
>       enum revokeop                    op, rop;
>       time_t                           t;
> -     const STACK_OF(X509_EXTENSION)  *exts;
> -     X509_EXTENSION                  *ex;
> -     ASN1_OBJECT                     *obj;
>       size_t                           j;
>  
>       /*
> @@ -119,6 +115,13 @@ revokeproc(int fd, const char *certfile,
>               goto out;
>       }
>  
> +     /* Cache and sanity check X509v3 extensions. */
> +
> +     if (X509_check_purpose(x, -1, -1) <= 0) {
> +             warnx("%s: invalid X509v3 extensions", certfile);
> +             goto out;
> +     }
> +
>       /* Read out the expiration date. */
>  
>       if ((t = X509expires(x)) == -1) {
> @@ -126,50 +129,10 @@ revokeproc(int fd, const char *certfile,
>               goto out;
>       }
>  
> -     /*
> -      * Next, the long process to make sure that the SAN entries
> -      * listed with the certificate fully cover those passed on the
> -      * command line.
> -      */
> -
> -     exts = X509_get0_extensions(x);
> -
> -     /* Scan til we find the SAN NID. */
> +     /* Extract list of SAN entries from the certificate. */
>  
> -     for (i = 0; i < sk_X509_EXTENSION_num(exts); i++) {
> -             ex = sk_X509_EXTENSION_value(exts, i);
> -             assert(ex != NULL);
> -             obj = X509_EXTENSION_get_object(ex);
> -             assert(obj != NULL);
> -             if (NID_subject_alt_name != OBJ_obj2nid(obj))
> -                     continue;
> -
> -             if (san != NULL) {
> -                     warnx("%s: two SAN entries", certfile);
> -                     goto out;
> -             }
> -
> -             bio = BIO_new(BIO_s_mem());
> -             if (bio == NULL) {
> -                     warnx("BIO_new");
> -                     goto out;
> -             }
> -             if (!X509V3_EXT_print(bio, ex, 0, 0)) {
> -                     warnx("X509V3_EXT_print");
> -                     goto out;
> -             }
> -             if ((san = calloc(1, BIO_number_written(bio) + 1)) == NULL) {
> -                     warn("calloc");
> -                     goto out;
> -             }
> -             ssz = BIO_read(bio, san, BIO_number_written(bio));
> -             if (ssz < 0 || (unsigned)ssz != BIO_number_written(bio)) {
> -                     warnx("BIO_read");
> -                     goto out;
> -             }
> -     }
> -
> -     if (san == NULL) {
> +     sans = X509_get_ext_d2i(x, NID_subject_alt_name, NULL, NULL);
> +     if (sans == NULL) {
>               warnx("%s: does not have a SAN entry", certfile);
>               if (revocate)
>                       goto out;
> @@ -184,25 +147,38 @@ revokeproc(int fd, const char *certfile,
>       }
>  
>       /*
> -      * Parse the SAN line.
> -      * Make sure that all of the domains are represented only once.
> +      * Ensure the certificate's SAN entries fully cover those passed on
> +      * the command line and that all domains are represented only once.
>        */

The SAN entries do not come from the command line, they come from the
config file.

>  
> -     str = san;
> -     while ((tok = strsep(&str, ",")) != NULL) {
> -             if (*tok == '\0')
> -                     continue;
> -             while (isspace((unsigned char)*tok))
> -                     tok++;
> -             if (strncmp(tok, "DNS:", 4))
> +     for (i = 0; i < sk_GENERAL_NAME_num(sans); i++) {
> +             GENERAL_NAME            *gen_name;
> +             ASN1_IA5STRING          *name;
> +             const unsigned char     *name_buf;
> +             int                      name_len;
> +             int                      ntype;
> +
> +             gen_name = sk_GENERAL_NAME_value(sans, i);
> +             assert(gen_name != NULL);
> +
> +             name = GENERAL_NAME_get0_value(gen_name, &ntype);
> +             if (ntype != GEN_DNS)
>                       continue;
> -             tok += 4;
> -             for (j = 0; j < altsz; j++)
> -                     if (strcmp(tok, alts[j]) == 0)
> +
> +             name_buf = ASN1_STRING_get0_data(name);
> +             name_len = ASN1_STRING_length(name);
> +
> +             for (j = 0; j < altsz; j++) {
> +                     if ((size_t)name_len != strlen(alts[j]))
> +                             continue;
> +                     if (strncmp(name_buf, alts[j], name_len) == 0)

This one tripped me up. Would it make sense to turn name_buf into a
proper C-string? Maybe this is normal for people who deal with this all
the time. I'm sure I will get this wrong...
Anyway, I checked the documentation, and it gave me this:

RETURN VALUES
     ASN1_STRING_data() and ASN1_STRING_get0_data() return an internal pointer
     to the data of x.

So that's cool. (Yes, yes, it documents in a different place that it's
not a C-string.)

I played around with SANs, adding and removing them, and I couldn't
break it.

With the comment fixed: OK florian FWIW

>                               break;
> +             }
>               if (j == altsz) {
>                       if (revocate) {
> -                             warnx("%s: unknown SAN entry: %s", certfile, 
> tok);
> +                             /* XXX strnvis? */
> +                             warnx("%s: unknown SAN entry: %.*s",
> +                                 certfile, name_len, name_buf);
>                               goto out;
>                       }
>                       force = 2;
> @@ -210,7 +186,9 @@ revokeproc(int fd, const char *certfile,
>               }
>               if (found[j]++) {
>                       if (revocate) {
> -                             warnx("%s: duplicate SAN entry: %s", certfile, 
> tok);
> +                             /* XXX strnvis? */
> +                             warnx("%s: duplicate SAN entry: %.*s",
> +                                 certfile, name_len, name_buf);
>                               goto out;
>                       }
>                       force = 2;
> @@ -310,8 +288,7 @@ out:
>       if (f != NULL)
>               fclose(f);
>       X509_free(x);
> -     BIO_free(bio);
> -     free(san);
> +     GENERAL_NAMES_free(sans);
>       free(der);
>       free(found);
>       free(der64);
>

-- 
I'm not entirely sure you are real.

Reply via email to