| /* |
| * X.509 certificate and private key decoding |
| * |
| * Based on XySSL: Copyright (C) 2006-2008 Christophe Devine |
| * |
| * Copyright (C) 2009 Paul Bakker <polarssl_maintainer at polarssl dot org> |
| * |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * * Neither the names of PolarSSL or XySSL nor the names of its contributors |
| * may be used to endorse or promote products derived from this software |
| * without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
| * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED |
| * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
| * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
| * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| /* |
| * The ITU-T X.509 standard defines a certificate format for PKI. |
| * |
| * http://www.ietf.org/rfc/rfc5280.txt |
| * http://www.ietf.org/rfc/rfc3279.txt |
| * http://www.ietf.org/rfc/rfc6818.txt |
| * |
| * ftp://ftp.rsasecurity.com/pub/pkcs/ascii/pkcs-1v2.asc |
| * |
| * http://www.itu.int/ITU-T/studygroups/com17/languages/X.680-0207.pdf |
| * http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf |
| */ |
| |
| #include <apr_pools.h> |
| #include <apr_tables.h> |
| #include "svn_hash.h" |
| #include "svn_string.h" |
| #include "svn_time.h" |
| #include "svn_checksum.h" |
| #include "svn_utf.h" |
| #include "svn_ctype.h" |
| #include "private/svn_utf_private.h" |
| #include "private/svn_string_private.h" |
| |
| #include "x509.h" |
| |
| #include <string.h> |
| #include <stdio.h> |
| |
| /* |
| * ASN.1 DER decoding routines |
| */ |
| static svn_error_t * |
| asn1_get_len(const unsigned char **p, const unsigned char *end, |
| ptrdiff_t *len) |
| { |
| if ((end - *p) < 1) |
| return svn_error_create(SVN_ERR_ASN1_OUT_OF_DATA, NULL, NULL); |
| |
| if ((**p & 0x80) == 0) |
| *len = *(*p)++; |
| else |
| switch (**p & 0x7F) |
| { |
| case 1: |
| if ((end - *p) < 2) |
| return svn_error_create(SVN_ERR_ASN1_OUT_OF_DATA, NULL, NULL); |
| |
| *len = (*p)[1]; |
| (*p) += 2; |
| break; |
| |
| case 2: |
| if ((end - *p) < 3) |
| return svn_error_create(SVN_ERR_ASN1_OUT_OF_DATA, NULL, NULL); |
| |
| *len = ((*p)[1] << 8) | (*p)[2]; |
| (*p) += 3; |
| break; |
| |
| default: |
| return svn_error_create(SVN_ERR_ASN1_INVALID_LENGTH, NULL, NULL); |
| break; |
| } |
| |
| if (*len > (end - *p)) |
| return svn_error_create(SVN_ERR_ASN1_OUT_OF_DATA, NULL, NULL); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| asn1_get_tag(const unsigned char **p, |
| const unsigned char *end, ptrdiff_t *len, int tag) |
| { |
| if ((end - *p) < 1) |
| return svn_error_create(SVN_ERR_ASN1_OUT_OF_DATA, NULL, NULL); |
| |
| if (**p != tag) |
| return svn_error_create(SVN_ERR_ASN1_UNEXPECTED_TAG, NULL, NULL); |
| |
| (*p)++; |
| |
| return svn_error_trace(asn1_get_len(p, end, len)); |
| } |
| |
| static svn_error_t * |
| asn1_get_int(const unsigned char **p, const unsigned char *end, int *val) |
| { |
| ptrdiff_t len; |
| |
| SVN_ERR(asn1_get_tag(p, end, &len, ASN1_INTEGER)); |
| |
| /* Reject bit patterns that would overflow the output and those that |
| represent negative values. */ |
| if (len > (int)sizeof(int) || (**p & 0x80) != 0) |
| return svn_error_create(SVN_ERR_ASN1_INVALID_LENGTH, NULL, NULL); |
| |
| *val = 0; |
| |
| while (len-- > 0) { |
| /* This would be undefined for bit-patterns of negative values. */ |
| *val = (*val << 8) | **p; |
| (*p)++; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_boolean_t |
| equal(const void *left, apr_size_t left_len, |
| const void *right, apr_size_t right_len) |
| { |
| if (left_len != right_len) |
| return FALSE; |
| |
| return memcmp(left, right, right_len) == 0; |
| } |
| |
| static svn_boolean_t |
| oids_equal(x509_buf *left, x509_buf *right) |
| { |
| return equal(left->p, left->len, |
| right->p, right->len); |
| } |
| |
| /* |
| * Version ::= INTEGER { v1(0), v2(1), v3(2) } |
| */ |
| static svn_error_t * |
| x509_get_version(const unsigned char **p, const unsigned char *end, int *ver) |
| { |
| svn_error_t *err; |
| ptrdiff_t len; |
| |
| /* |
| * As defined in the Basic Certificate fields: |
| * version [0] EXPLICIT Version DEFAULT v1, |
| * the version is the context specific tag 0. |
| */ |
| err = asn1_get_tag(p, end, &len, |
| ASN1_CONTEXT_SPECIFIC | ASN1_CONSTRUCTED | 0); |
| if (err) |
| { |
| if (err->apr_err == SVN_ERR_ASN1_UNEXPECTED_TAG) |
| { |
| svn_error_clear(err); |
| *ver = 0; |
| return SVN_NO_ERROR; |
| } |
| |
| return svn_error_trace(err); |
| } |
| |
| end = *p + len; |
| |
| err = asn1_get_int(p, end, ver); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_VERSION, err, NULL); |
| |
| if (*p != end) |
| { |
| err = svn_error_create(SVN_ERR_ASN1_LENGTH_MISMATCH, NULL, NULL); |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_VERSION, err, NULL); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* |
| * CertificateSerialNumber ::= INTEGER |
| */ |
| static svn_error_t * |
| x509_get_serial(const unsigned char **p, |
| const unsigned char *end, x509_buf * serial) |
| { |
| svn_error_t *err; |
| |
| if ((end - *p) < 1) |
| { |
| err = svn_error_create(SVN_ERR_ASN1_OUT_OF_DATA, NULL, NULL); |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_SERIAL, err, NULL); |
| } |
| |
| if (**p != (ASN1_CONTEXT_SPECIFIC | ASN1_PRIMITIVE | 2) && |
| **p != ASN1_INTEGER) |
| { |
| err = svn_error_create(SVN_ERR_ASN1_UNEXPECTED_TAG, NULL, NULL); |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_SERIAL, err, NULL); |
| } |
| |
| serial->tag = *(*p)++; |
| |
| err = asn1_get_len(p, end, &serial->len); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_SERIAL, err, NULL); |
| |
| serial->p = *p; |
| *p += serial->len; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* |
| * AlgorithmIdentifier ::= SEQUENCE { |
| * algorithm OBJECT IDENTIFIER, |
| * parameters ANY DEFINED BY algorithm OPTIONAL } |
| */ |
| static svn_error_t * |
| x509_get_alg(const unsigned char **p, const unsigned char *end, x509_buf * alg) |
| { |
| svn_error_t *err; |
| ptrdiff_t len; |
| |
| err = asn1_get_tag(p, end, &len, ASN1_CONSTRUCTED | ASN1_SEQUENCE); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_ALG, err, NULL); |
| |
| end = *p + len; |
| alg->tag = **p; |
| |
| err = asn1_get_tag(p, end, &alg->len, ASN1_OID); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_ALG, err, NULL); |
| |
| alg->p = *p; |
| *p += alg->len; |
| |
| if (*p == end) |
| return SVN_NO_ERROR; |
| |
| /* The OID encoding of 1.2.840.113549.1.1.10 (id-RSASSA-PSS) */ |
| #define OID_RSASSA_PSS "\x2a\x86\x48\x86\xf7\x0d\x01\x01\x0a" |
| |
| if (equal(alg->p, alg->len, OID_RSASSA_PSS, sizeof(OID_RSASSA_PSS) - 1)) |
| { |
| /* Skip over algorithm parameters for id-RSASSA-PSS (RFC 8017) |
| * |
| * RSASSA-PSS-params ::= SEQUENCE { |
| * hashAlgorithm [0] HashAlgorithm DEFAULT sha1, |
| * maskGenAlgorithm [1] MaskGenAlgorithm DEFAULT mgf1SHA1, |
| * saltLength [2] INTEGER DEFAULT 20, |
| * trailerField [3] TrailerField DEFAULT trailerFieldBC |
| * } |
| */ |
| err = asn1_get_tag(p, end, &len, ASN1_CONSTRUCTED | ASN1_SEQUENCE); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_ALG, err, NULL); |
| |
| *p += len; |
| } |
| else |
| { |
| /* Algorithm parameters must be NULL for other algorithms */ |
| err = asn1_get_tag(p, end, &len, ASN1_NULL); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_ALG, err, NULL); |
| } |
| |
| if (*p != end) |
| { |
| err = svn_error_create(SVN_ERR_ASN1_LENGTH_MISMATCH, NULL, NULL); |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_ALG, err, NULL); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* |
| * AttributeTypeAndValue ::= SEQUENCE { |
| * type AttributeType, |
| * value AttributeValue } |
| * |
| * AttributeType ::= OBJECT IDENTIFIER |
| * |
| * AttributeValue ::= ANY DEFINED BY AttributeType |
| */ |
| static svn_error_t * |
| x509_get_attribute(const unsigned char **p, const unsigned char *end, |
| x509_name *cur, apr_pool_t *result_pool) |
| { |
| svn_error_t *err; |
| ptrdiff_t len; |
| x509_buf *oid; |
| x509_buf *val; |
| |
| err = asn1_get_tag(p, end, &len, ASN1_CONSTRUCTED | ASN1_SEQUENCE); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_NAME, err, NULL); |
| |
| end = *p + len; |
| |
| oid = &cur->oid; |
| |
| err = asn1_get_tag(p, end, &oid->len, ASN1_OID); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_NAME, err, NULL); |
| |
| oid->tag = ASN1_OID; |
| oid->p = *p; |
| *p += oid->len; |
| |
| if ((end - *p) < 1) |
| { |
| err = svn_error_create(SVN_ERR_ASN1_OUT_OF_DATA, NULL, NULL); |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_NAME, err, NULL); |
| } |
| |
| if (**p != ASN1_BMP_STRING && **p != ASN1_UTF8_STRING && |
| **p != ASN1_T61_STRING && **p != ASN1_PRINTABLE_STRING && |
| **p != ASN1_IA5_STRING && **p != ASN1_UNIVERSAL_STRING) |
| { |
| err = svn_error_create(SVN_ERR_ASN1_UNEXPECTED_TAG, NULL, NULL); |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_NAME, err, NULL); |
| } |
| |
| val = &cur->val; |
| val->tag = *(*p)++; |
| |
| err = asn1_get_len(p, end, &val->len); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_NAME, err, NULL); |
| |
| val->p = *p; |
| *p += val->len; |
| |
| cur->next = NULL; |
| |
| if (*p != end) |
| { |
| err = svn_error_create(SVN_ERR_ASN1_LENGTH_MISMATCH, NULL, NULL); |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_NAME, err, NULL); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* |
| * RelativeDistinguishedName ::= |
| * SET SIZE (1..MAX) OF AttributeTypeAndValue |
| */ |
| static svn_error_t * |
| x509_get_name(const unsigned char **p, const unsigned char *name_end, |
| x509_name *name, apr_pool_t *result_pool) |
| { |
| svn_error_t *err; |
| ptrdiff_t len; |
| const unsigned char *set_end; |
| x509_name *cur = NULL; |
| |
| err = asn1_get_tag(p, name_end, &len, ASN1_CONSTRUCTED | ASN1_SET); |
| if (err || len < 1) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_NAME, err, NULL); |
| |
| set_end = *p + len; |
| |
| /* |
| * iterate until the end of the SET is reached |
| */ |
| while (*p < set_end) |
| { |
| if (!cur) |
| { |
| cur = name; |
| } |
| else |
| { |
| cur->next = apr_palloc(result_pool, sizeof(x509_name)); |
| cur = cur->next; |
| } |
| SVN_ERR(x509_get_attribute(p, set_end, cur, result_pool)); |
| } |
| |
| /* |
| * recurse until end of SEQUENCE (name) is reached |
| */ |
| if (*p == name_end) |
| return SVN_NO_ERROR; |
| |
| cur->next = apr_palloc(result_pool, sizeof(x509_name)); |
| |
| return svn_error_trace(x509_get_name(p, name_end, cur->next, result_pool)); |
| } |
| |
| /* Retrieve the date from the X.509 cert data between *P and END in either |
| * UTCTime or GeneralizedTime format (as defined in RFC 5280 s. 4.1.2.5.1 and |
| * 4.1.2.5.2 respectively) and place the result in WHEN using SCRATCH_POOL |
| * for temporary allocations. */ |
| static svn_error_t * |
| x509_get_date(apr_time_t *when, |
| const unsigned char **p, |
| const unsigned char *end, |
| apr_pool_t *scratch_pool) |
| { |
| svn_error_t *err; |
| apr_status_t ret; |
| int tag; |
| ptrdiff_t len; |
| char *date; |
| apr_time_exp_t xt = { 0 }; |
| char tz; |
| |
| err = asn1_get_tag(p, end, &len, ASN1_UTC_TIME); |
| if (err && err->apr_err == SVN_ERR_ASN1_UNEXPECTED_TAG) |
| { |
| svn_error_clear(err); |
| err = asn1_get_tag(p, end, &len, ASN1_GENERALIZED_TIME); |
| tag = ASN1_GENERALIZED_TIME; |
| } |
| else |
| { |
| tag = ASN1_UTC_TIME; |
| } |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_DATE, err, NULL); |
| |
| date = apr_pstrndup(scratch_pool, (const char *) *p, len); |
| switch (tag) |
| { |
| case ASN1_UTC_TIME: |
| if (sscanf(date, "%2d%2d%2d%2d%2d%2d%c", |
| &xt.tm_year, &xt.tm_mon, &xt.tm_mday, |
| &xt.tm_hour, &xt.tm_min, &xt.tm_sec, &tz) < 6) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_DATE, NULL, NULL); |
| |
| /* UTCTime only provides a 2 digit year. X.509 specifies that years |
| * greater than or equal to 50 must be interpreted as 19YY and years |
| * less than 50 be interpreted as 20YY. This format is not used for |
| * years greater than 2049. apr_time_exp_t wants years as the number |
| * of years since 1900, so don't convert to 4 digits here. */ |
| xt.tm_year += 100 * (xt.tm_year < 50); |
| break; |
| |
| case ASN1_GENERALIZED_TIME: |
| if (sscanf(date, "%4d%2d%2d%2d%2d%2d%c", |
| &xt.tm_year, &xt.tm_mon, &xt.tm_mday, |
| &xt.tm_hour, &xt.tm_min, &xt.tm_sec, &tz) < 6) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_DATE, NULL, NULL); |
| |
| /* GeneralizedTime has the full 4 digit year. But apr_time_exp_t |
| * wants years as the number of years since 1900. */ |
| xt.tm_year -= 1900; |
| break; |
| |
| default: |
| /* shouldn't ever get here because we should error out above in the |
| * asn1_get_tag() bits but doesn't hurt to be extra paranoid. */ |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_DATE, NULL, NULL); |
| break; |
| } |
| |
| /* check that the timezone is GMT |
| * ASN.1 allows for the timezone to be specified but X.509 says it must |
| * always be GMT. A little bit of extra paranoia here seems like a good |
| * idea. */ |
| if (tz != 'Z') |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_DATE, NULL, NULL); |
| |
| /* apr_time_exp_t expects months to be zero indexed, 0=Jan, 11=Dec. */ |
| xt.tm_mon -= 1; |
| |
| /* range checks (as per definition of apr_time_exp_t in apr_time.h) */ |
| if (xt.tm_usec < 0 || |
| xt.tm_sec < 0 || xt.tm_sec > 61 || |
| xt.tm_min < 0 || xt.tm_min > 59 || |
| xt.tm_hour < 0 || xt.tm_hour > 23 || |
| xt.tm_mday < 1 || xt.tm_mday > 31 || |
| xt.tm_mon < 0 || xt.tm_mon > 11 || |
| xt.tm_year < 0 || |
| xt.tm_wday < 0 || xt.tm_wday > 6 || |
| xt.tm_yday < 0 || xt.tm_yday > 365) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_DATE, NULL, NULL); |
| |
| ret = apr_time_exp_gmt_get(when, &xt); |
| if (ret) |
| return svn_error_wrap_apr(ret, NULL); |
| |
| *p += len; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* |
| * Validity ::= SEQUENCE { |
| * notBefore Time, |
| * notAfter Time } |
| * |
| * Time ::= CHOICE { |
| * utcTime UTCTime, |
| * generalTime GeneralizedTime } |
| */ |
| static svn_error_t * |
| x509_get_dates(apr_time_t *from, |
| apr_time_t *to, |
| const unsigned char **p, |
| const unsigned char *end, |
| apr_pool_t *scratch_pool) |
| { |
| svn_error_t *err; |
| ptrdiff_t len; |
| |
| err = asn1_get_tag(p, end, &len, ASN1_CONSTRUCTED | ASN1_SEQUENCE); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_DATE, err, NULL); |
| |
| end = *p + len; |
| |
| SVN_ERR(x509_get_date(from, p, end, scratch_pool)); |
| |
| SVN_ERR(x509_get_date(to, p, end, scratch_pool)); |
| |
| if (*p != end) |
| { |
| err = svn_error_create(SVN_ERR_ASN1_LENGTH_MISMATCH, NULL, NULL); |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_DATE, err, NULL); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| x509_get_sig(const unsigned char **p, const unsigned char *end, x509_buf * sig) |
| { |
| svn_error_t *err; |
| ptrdiff_t len; |
| |
| err = asn1_get_tag(p, end, &len, ASN1_BIT_STRING); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_SIGNATURE, err, NULL); |
| |
| sig->tag = ASN1_BIT_STRING; |
| |
| if (--len < 1 || *(*p)++ != 0) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_SIGNATURE, NULL, NULL); |
| |
| sig->len = len; |
| sig->p = *p; |
| |
| *p += len; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* |
| * X.509 v2/v3 unique identifier (not parsed) |
| */ |
| static svn_error_t * |
| x509_get_uid(const unsigned char **p, |
| const unsigned char *end, x509_buf * uid, int n) |
| { |
| svn_error_t *err; |
| |
| if (*p == end) |
| return SVN_NO_ERROR; |
| |
| err = asn1_get_tag(p, end, &uid->len, |
| ASN1_CONTEXT_SPECIFIC | ASN1_CONSTRUCTED | n); |
| if (err) |
| { |
| if (err->apr_err == SVN_ERR_ASN1_UNEXPECTED_TAG) |
| { |
| svn_error_clear(err); |
| return SVN_NO_ERROR; |
| } |
| |
| return svn_error_trace(err); |
| } |
| |
| uid->tag = ASN1_CONTEXT_SPECIFIC | ASN1_CONSTRUCTED | n; |
| uid->p = *p; |
| *p += uid->len; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* |
| * X.509 v3 extensions (not parsed) |
| */ |
| static svn_error_t * |
| x509_get_ext(apr_array_header_t *dnsnames, |
| const unsigned char **p, |
| const unsigned char *end) |
| { |
| svn_error_t *err; |
| ptrdiff_t len; |
| |
| if (*p == end) |
| return SVN_NO_ERROR; |
| |
| err = asn1_get_tag(p, end, &len, |
| ASN1_CONTEXT_SPECIFIC | ASN1_CONSTRUCTED | 3); |
| if (err) |
| { |
| /* If there aren't extensions that's ok they aren't required */ |
| if (err->apr_err == SVN_ERR_ASN1_UNEXPECTED_TAG) |
| { |
| svn_error_clear(err); |
| return SVN_NO_ERROR; |
| } |
| |
| return svn_error_trace(err); |
| } |
| |
| end = *p + len; |
| |
| SVN_ERR(asn1_get_tag(p, end, &len, ASN1_CONSTRUCTED | ASN1_SEQUENCE)); |
| |
| if (end != *p + len) |
| { |
| err = svn_error_create(SVN_ERR_ASN1_LENGTH_MISMATCH, NULL, NULL); |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_EXTENSIONS, err, NULL); |
| } |
| |
| while (*p < end) |
| { |
| ptrdiff_t ext_len; |
| const unsigned char *ext_start, *sna_end; |
| err = asn1_get_tag(p, end, &ext_len, ASN1_CONSTRUCTED | ASN1_SEQUENCE); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_EXTENSIONS, err, |
| NULL); |
| ext_start = *p; |
| |
| err = asn1_get_tag(p, end, &len, ASN1_OID); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_EXTENSIONS, err, |
| NULL); |
| |
| /* skip all extensions except SubjectAltName */ |
| if (!equal(*p, len, |
| OID_SUBJECT_ALT_NAME, sizeof(OID_SUBJECT_ALT_NAME) - 1)) |
| { |
| *p += ext_len - (*p - ext_start); |
| continue; |
| } |
| *p += len; |
| |
| err = asn1_get_tag(p, end, &len, ASN1_OCTET_STRING); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_EXTENSIONS, err, |
| NULL); |
| |
| /* SubjectAltName ::= GeneralNames |
| |
| GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName |
| |
| GeneralName ::= CHOICE { |
| other Name [0] OtherName, |
| rfc822Name [1] IA5String, |
| dNSName [2] IA5String, |
| x400Address [3] ORAddress, |
| directoryName [4] Name, |
| ediPartyName [5] EDIPartyName, |
| uniformResourceIdentifier [6] IA5String, |
| iPAddress [7] OCTET STRING, |
| registeredID [8] OBJECT IDENTIFIER } */ |
| sna_end = *p + len; |
| |
| err = asn1_get_tag(p, sna_end, &len, ASN1_CONSTRUCTED | ASN1_SEQUENCE); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_EXTENSIONS, err, |
| NULL); |
| |
| if (sna_end != *p + len) |
| { |
| err = svn_error_create(SVN_ERR_ASN1_LENGTH_MISMATCH, NULL, NULL); |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_EXTENSIONS, err, NULL); |
| } |
| |
| while (*p < sna_end) |
| { |
| err = asn1_get_tag(p, sna_end, &len, ASN1_CONTEXT_SPECIFIC | |
| ASN1_PRIMITIVE | 2); |
| if (err) |
| { |
| /* not not a dNSName */ |
| if (err->apr_err == SVN_ERR_ASN1_UNEXPECTED_TAG) |
| { |
| svn_error_clear(err); |
| /* need to skip the tag and then find the length to |
| * skip to ignore this SNA entry. */ |
| (*p)++; |
| SVN_ERR(asn1_get_len(p, sna_end, &len)); |
| *p += len; |
| continue; |
| } |
| |
| return svn_error_trace(err); |
| } |
| else |
| { |
| /* We found a dNSName entry */ |
| x509_buf *dnsname = apr_palloc(dnsnames->pool, sizeof(*dnsname)); |
| dnsname->tag = ASN1_IA5_STRING; /* implicit based on dNSName */ |
| dnsname->len = len; |
| dnsname->p = *p; |
| APR_ARRAY_PUSH(dnsnames, x509_buf *) = dnsname; |
| } |
| |
| *p += len; |
| } |
| |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Escape all non-ascii or control characters similar to |
| * svn_xml_fuzzy_escape() and svn_utf_cstring_from_utf8_fuzzy(). |
| * All of the encoding formats somewhat overlap with ascii (BMPString |
| * and UniversalString are actually always wider so you'll end up |
| * with a bunch of escaped nul bytes, but ideally we don't get here |
| * for those). The result is always a nul-terminated C string. */ |
| static const char * |
| fuzzy_escape(const svn_string_t *src, apr_pool_t *result_pool) |
| { |
| const char *end = src->data + src->len; |
| const char *p = src->data, *q; |
| svn_stringbuf_t *outstr; |
| char escaped_char[6]; /* ? \ u u u \0 */ |
| |
| for (q = p; q < end; q++) |
| { |
| if (!svn_ctype_isascii(*q) || svn_ctype_iscntrl(*q)) |
| break; |
| } |
| |
| if (q == end) |
| return src->data; |
| |
| outstr = svn_stringbuf_create_empty(result_pool); |
| while (1) |
| { |
| q = p; |
| |
| /* Traverse till either unsafe character or eos. */ |
| while (q < end && svn_ctype_isascii(*q) && !svn_ctype_iscntrl(*q)) |
| q++; |
| |
| /* copy chunk before marker */ |
| svn_stringbuf_appendbytes(outstr, p, q - p); |
| |
| if (q == end) |
| break; |
| |
| apr_snprintf(escaped_char, sizeof(escaped_char), "?\\%03u", |
| (unsigned char) *q); |
| svn_stringbuf_appendcstr(outstr, escaped_char); |
| |
| p = q + 1; |
| } |
| |
| return outstr->data; |
| } |
| |
| /* Escape only NUL characters from a string that is presumed to |
| * be UTF-8 encoded and return a nul-terminated C string. */ |
| static const char * |
| nul_escape(const svn_string_t *src, apr_pool_t *result_pool) |
| { |
| const char *end = src->data + src->len; |
| const char *p = src->data, *q; |
| svn_stringbuf_t *outstr; |
| |
| for (q = p; q < end; q++) |
| { |
| if (*q == '\0') |
| break; |
| } |
| |
| if (q == end) |
| return src->data; |
| |
| outstr = svn_stringbuf_create_empty(result_pool); |
| while (1) |
| { |
| q = p; |
| |
| /* Traverse till either unsafe character or eos. */ |
| while (q < end && *q != '\0') |
| q++; |
| |
| /* copy chunk before marker */ |
| svn_stringbuf_appendbytes(outstr, p, q - p); |
| |
| if (q == end) |
| break; |
| |
| svn_stringbuf_appendcstr(outstr, "?\\000"); |
| |
| p = q + 1; |
| } |
| |
| return outstr->data; |
| } |
| |
| |
| /* Convert an ISO-8859-1 (Latin-1) string to UTF-8. |
| ISO-8859-1 is a strict subset of Unicode. */ |
| static svn_error_t * |
| latin1_to_utf8(const svn_string_t **result, const svn_string_t *src, |
| apr_pool_t *result_pool) |
| { |
| apr_int32_t *ucs4buf; |
| svn_membuf_t resultbuf; |
| apr_size_t length; |
| apr_size_t i; |
| svn_string_t *res; |
| |
| ucs4buf = apr_palloc(result_pool, src->len * sizeof(*ucs4buf)); |
| for (i = 0; i < src->len; ++i) |
| ucs4buf[i] = (unsigned char)(src->data[i]); |
| |
| svn_membuf__create(&resultbuf, 2 * src->len, result_pool); |
| SVN_ERR(svn_utf__encode_ucs4_string( |
| &resultbuf, ucs4buf, src->len, &length)); |
| |
| res = apr_palloc(result_pool, sizeof(*res)); |
| res->data = resultbuf.data; |
| res->len = length; |
| *result = res; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Make a best effort to convert a X.509 name to a UTF-8 encoded |
| * string and return it. If we can't properly convert just do a |
| * fuzzy conversion so we have something to display. */ |
| static const char * |
| x509name_to_utf8_string(const x509_name *name, apr_pool_t *result_pool) |
| { |
| const svn_string_t *src_string; |
| const svn_string_t *utf8_string; |
| svn_error_t *err; |
| |
| src_string = svn_string_ncreate((const char *)name->val.p, |
| name->val.len, |
| result_pool); |
| switch (name->val.tag) |
| { |
| case ASN1_UTF8_STRING: |
| if (svn_utf__is_valid(src_string->data, src_string->len)) |
| return nul_escape(src_string, result_pool); |
| else |
| /* not a valid UTF-8 string, who knows what it is, |
| * so run it through the fuzzy_escape code. */ |
| return fuzzy_escape(src_string, result_pool); |
| break; |
| |
| /* Both BMP and UNIVERSAL should always be in Big Endian (aka |
| * network byte order). But rumor has it that there are certs |
| * out there with other endianness and even Byte Order Marks. |
| * If we actually run into these, we might need to do something |
| * about it. */ |
| |
| case ASN1_BMP_STRING: |
| if (0 != src_string->len % sizeof(apr_uint16_t)) |
| return fuzzy_escape(src_string, result_pool); |
| err = svn_utf__utf16_to_utf8(&utf8_string, |
| (const void*)(src_string->data), |
| src_string->len / sizeof(apr_uint16_t), |
| TRUE, result_pool, result_pool); |
| break; |
| |
| case ASN1_UNIVERSAL_STRING: |
| if (0 != src_string->len % sizeof(apr_int32_t)) |
| return fuzzy_escape(src_string, result_pool); |
| err = svn_utf__utf32_to_utf8(&utf8_string, |
| (const void*)(src_string->data), |
| src_string->len / sizeof(apr_int32_t), |
| TRUE, result_pool, result_pool); |
| break; |
| |
| /* Despite what all the IETF, ISO, ITU bits say everything out |
| * on the Internet that I can find treats this as ISO-8859-1. |
| * Even the name is misleading, it's not actually T.61. All the |
| * gory details can be found in the Character Sets section of: |
| * https://www.cs.auckland.ac.nz/~pgut001/pubs/x509guide.txt |
| */ |
| case ASN1_T61_STRING: |
| err = latin1_to_utf8(&utf8_string, src_string, result_pool); |
| break; |
| |
| /* This leaves two types out there in the wild. PrintableString, |
| * which is just a subset of ASCII and IA5 which is ASCII (though |
| * 0x24 '$' and 0x23 '#' may be defined with different symbols |
| * depending on the location, in practice it seems everyone just |
| * treats it as ASCII). Since these are just ASCII run through |
| * the fuzzy_escape code to deal with anything that isn't actually |
| * ASCII. There shouldn't be any other types here but if we find |
| * a cert with some other encoding, the best we can do is the |
| * fuzzy_escape(). Note: Technically IA5 isn't valid in this |
| * context, however in the real world it may pop up. */ |
| default: |
| return fuzzy_escape(src_string, result_pool); |
| } |
| |
| if (err) |
| { |
| svn_error_clear(err); |
| return fuzzy_escape(src_string, result_pool); |
| } |
| |
| return nul_escape(utf8_string, result_pool); |
| } |
| |
| static svn_error_t * |
| x509_name_to_certinfo(apr_array_header_t **result, |
| const x509_name *dn, |
| apr_pool_t *scratch_pool, |
| apr_pool_t *result_pool) |
| { |
| const x509_name *name = dn; |
| |
| *result = apr_array_make(result_pool, 6, sizeof(svn_x509_name_attr_t *)); |
| |
| while (name != NULL) { |
| svn_x509_name_attr_t *attr = apr_palloc(result_pool, sizeof(svn_x509_name_attr_t)); |
| |
| attr->oid_len = name->oid.len; |
| attr->oid = apr_pmemdup(result_pool, name->oid.p, attr->oid_len); |
| attr->utf8_value = x509name_to_utf8_string(name, result_pool); |
| if (!attr->utf8_value) |
| /* this should never happen */ |
| attr->utf8_value = apr_pstrdup(result_pool, "??"); |
| APR_ARRAY_PUSH(*result, const svn_x509_name_attr_t *) = attr; |
| |
| name = name->next; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_boolean_t |
| is_hostname(const char *str) |
| { |
| apr_size_t i, len = strlen(str); |
| |
| for (i = 0; i < len; i++) |
| { |
| char c = str[i]; |
| |
| /* '-' is only legal when not at the start or end of a label */ |
| if (c == '-') |
| { |
| if (i + 1 != len) |
| { |
| if (str[i + 1] == '.') |
| return FALSE; /* '-' precedes a '.' */ |
| } |
| else |
| return FALSE; /* '-' is at end of string */ |
| |
| /* determine the previous character. */ |
| if (i == 0) |
| return FALSE; /* '-' is at start of string */ |
| else |
| if (str[i - 1] == '.') |
| return FALSE; /* '-' follows a '.' */ |
| } |
| else if (c != '*' && c != '.' && !svn_ctype_isalnum(c)) |
| return FALSE; /* some character not allowed */ |
| } |
| |
| return TRUE; |
| } |
| |
| static const char * |
| x509parse_get_cn(apr_array_header_t *subject) |
| { |
| int i; |
| |
| for (i = 0; i < subject->nelts; ++i) |
| { |
| const svn_x509_name_attr_t *attr = APR_ARRAY_IDX(subject, i, const svn_x509_name_attr_t *); |
| if (equal(attr->oid, attr->oid_len, |
| SVN_X509_OID_COMMON_NAME, sizeof(SVN_X509_OID_COMMON_NAME) - 1)) |
| return attr->utf8_value; |
| } |
| |
| return NULL; |
| } |
| |
| |
| static void |
| x509parse_get_hostnames(svn_x509_certinfo_t *ci, x509_cert *crt, |
| apr_pool_t *result_pool, apr_pool_t *scratch_pool) |
| { |
| ci->hostnames = NULL; |
| |
| if (crt->dnsnames->nelts > 0) |
| { |
| int i; |
| |
| ci->hostnames = apr_array_make(result_pool, crt->dnsnames->nelts, |
| sizeof(const char*)); |
| |
| /* Subject Alt Names take priority */ |
| for (i = 0; i < crt->dnsnames->nelts; i++) |
| { |
| x509_buf *dnsname = APR_ARRAY_IDX(crt->dnsnames, i, x509_buf *); |
| const svn_string_t *temp = svn_string_ncreate((const char *)dnsname->p, |
| dnsname->len, |
| scratch_pool); |
| |
| APR_ARRAY_PUSH(ci->hostnames, const char*) |
| = fuzzy_escape(temp, result_pool); |
| } |
| } |
| else |
| { |
| /* no SAN then get the hostname from the CommonName on the cert */ |
| const char *utf8_value; |
| |
| utf8_value = x509parse_get_cn(ci->subject); |
| |
| if (utf8_value && is_hostname(utf8_value)) |
| { |
| ci->hostnames = apr_array_make(result_pool, 1, sizeof(const char*)); |
| APR_ARRAY_PUSH(ci->hostnames, const char*) = utf8_value; |
| } |
| } |
| } |
| |
| /* |
| * Parse one certificate. |
| */ |
| svn_error_t * |
| svn_x509_parse_cert(svn_x509_certinfo_t **certinfo, |
| const char *buf, |
| apr_size_t buflen, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_error_t *err; |
| ptrdiff_t len; |
| const unsigned char *p; |
| const unsigned char *end; |
| x509_cert *crt; |
| svn_x509_certinfo_t *ci; |
| |
| crt = apr_pcalloc(scratch_pool, sizeof(*crt)); |
| p = (const unsigned char *)buf; |
| len = buflen; |
| end = p + len; |
| |
| /* |
| * Certificate ::= SEQUENCE { |
| * tbsCertificate TBSCertificate, |
| * signatureAlgorithm AlgorithmIdentifier, |
| * signatureValue BIT STRING } |
| */ |
| err = asn1_get_tag(&p, end, &len, ASN1_CONSTRUCTED | ASN1_SEQUENCE); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_FORMAT, err, NULL); |
| |
| if (len != (end - p)) |
| { |
| err = svn_error_create(SVN_ERR_ASN1_LENGTH_MISMATCH, NULL, NULL); |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_FORMAT, err, NULL); |
| } |
| |
| /* |
| * TBSCertificate ::= SEQUENCE { |
| */ |
| err = asn1_get_tag(&p, end, &len, ASN1_CONSTRUCTED | ASN1_SEQUENCE); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_FORMAT, err, NULL); |
| |
| end = p + len; |
| |
| /* |
| * Version ::= INTEGER { v1(0), v2(1), v3(2) } |
| * |
| * CertificateSerialNumber ::= INTEGER |
| * |
| * signature AlgorithmIdentifier |
| */ |
| SVN_ERR(x509_get_version(&p, end, &crt->version)); |
| SVN_ERR(x509_get_serial(&p, end, &crt->serial)); |
| SVN_ERR(x509_get_alg(&p, end, &crt->sig_oid1)); |
| |
| crt->version++; |
| |
| if (crt->version > 3) |
| return svn_error_create(SVN_ERR_X509_CERT_UNKNOWN_VERSION, NULL, NULL); |
| |
| /* |
| * issuer Name |
| */ |
| err = asn1_get_tag(&p, end, &len, ASN1_CONSTRUCTED | ASN1_SEQUENCE); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_FORMAT, err, NULL); |
| |
| SVN_ERR(x509_get_name(&p, p + len, &crt->issuer, scratch_pool)); |
| |
| /* |
| * Validity ::= SEQUENCE { |
| * notBefore Time, |
| * notAfter Time } |
| * |
| */ |
| SVN_ERR(x509_get_dates(&crt->valid_from, &crt->valid_to, &p, end, |
| scratch_pool)); |
| |
| /* |
| * subject Name |
| */ |
| err = asn1_get_tag(&p, end, &len, ASN1_CONSTRUCTED | ASN1_SEQUENCE); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_FORMAT, err, NULL); |
| |
| SVN_ERR(x509_get_name(&p, p + len, &crt->subject, scratch_pool)); |
| |
| /* |
| * SubjectPublicKeyInfo ::= SEQUENCE |
| * algorithm AlgorithmIdentifier, |
| * subjectPublicKey BIT STRING } |
| */ |
| err = asn1_get_tag(&p, end, &len, ASN1_CONSTRUCTED | ASN1_SEQUENCE); |
| if (err) |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_FORMAT, err, NULL); |
| |
| /* Skip pubkey. */ |
| p += len; |
| |
| /* |
| * issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, |
| * -- If present, version shall be v2 or v3 |
| * subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, |
| * -- If present, version shall be v2 or v3 |
| * extensions [3] EXPLICIT Extensions OPTIONAL |
| * -- If present, version shall be v3 |
| */ |
| crt->dnsnames = apr_array_make(scratch_pool, 3, sizeof(x509_buf *)); |
| |
| /* Try to parse issuerUniqueID, subjectUniqueID and extensions for *every* |
| * version (X.509 v1, v2 and v3), not just v2 or v3. If they aren't present, |
| * we are fine, but we don't want to throw an error if they are. v1 and v2 |
| * certificates with the corresponding extra fields are ill-formed per RFC |
| * 5280 s. 4.1, but we suspect they could exist in the real world. Other |
| * X.509 parsers (e.g., within OpenSSL or Microsoft CryptoAPI) aren't picky |
| * about these certificates, and we also allow them. */ |
| SVN_ERR(x509_get_uid(&p, end, &crt->issuer_id, 1)); |
| SVN_ERR(x509_get_uid(&p, end, &crt->subject_id, 2)); |
| SVN_ERR(x509_get_ext(crt->dnsnames, &p, end)); |
| |
| if (p != end) |
| { |
| err = svn_error_create(SVN_ERR_ASN1_LENGTH_MISMATCH, NULL, NULL); |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_FORMAT, err, NULL); |
| } |
| |
| end = (const unsigned char*) buf + buflen; |
| |
| /* |
| * signatureAlgorithm AlgorithmIdentifier, |
| * signatureValue BIT STRING |
| */ |
| SVN_ERR(x509_get_alg(&p, end, &crt->sig_oid2)); |
| |
| if (!oids_equal(&crt->sig_oid1, &crt->sig_oid2)) |
| return svn_error_create(SVN_ERR_X509_CERT_SIG_MISMATCH, NULL, NULL); |
| |
| SVN_ERR(x509_get_sig(&p, end, &crt->sig)); |
| |
| if (p != end) |
| { |
| err = svn_error_create(SVN_ERR_ASN1_LENGTH_MISMATCH, NULL, NULL); |
| return svn_error_create(SVN_ERR_X509_CERT_INVALID_FORMAT, err, NULL); |
| } |
| |
| ci = apr_pcalloc(result_pool, sizeof(*ci)); |
| |
| /* Get the subject name */ |
| SVN_ERR(x509_name_to_certinfo(&ci->subject, &crt->subject, |
| scratch_pool, result_pool)); |
| |
| /* Get the issuer name */ |
| SVN_ERR(x509_name_to_certinfo(&ci->issuer, &crt->issuer, |
| scratch_pool, result_pool)); |
| |
| /* Copy the validity range */ |
| ci->valid_from = crt->valid_from; |
| ci->valid_to = crt->valid_to; |
| |
| /* Calculate the SHA1 digest of the certificate, otherwise known as |
| the fingerprint */ |
| SVN_ERR(svn_checksum(&ci->digest, svn_checksum_sha1, buf, buflen, |
| result_pool)); |
| |
| /* Construct the array of host names */ |
| x509parse_get_hostnames(ci, crt, result_pool, scratch_pool); |
| |
| *certinfo = ci; |
| return SVN_NO_ERROR; |
| } |
| |