diff options
Diffstat (limited to 'src/tls_remote.c')
-rw-r--r-- | src/tls_remote.c | 387 |
1 files changed, 387 insertions, 0 deletions
diff --git a/src/tls_remote.c b/src/tls_remote.c new file mode 100644 index 0000000..1318b4e --- /dev/null +++ b/src/tls_remote.c @@ -0,0 +1,387 @@ +#include <unistd.h> +#include "ucspissl.h" +#include "fmt.h" +#include "stralloc.h" +#include "str.h" +#include "byte.h" +#include "case.h" +#include "dns.h" +#include "constmap.h" +#include "tls_remote.h" +#include "tls_errors.h" + +/** @file tls_remote.c -- TLS client functions + @brief connection functions: tls_conn, tls_exit; + verification functions: tls_certkey, tls_checkpeer, tls_fingerprint, tlsa_check; + tls_destination, tls_domaincert + dummy functions: tls_crlcheck + + tls_checkpeer: r = 0 -> ADH, r = 1 -> wildcard DN, r = 2 -> DN, r = 3 -> CA; r < 0 -> error + tls_fingerprint: r = 0 -> failed, r = 1 -> ok; r < 0 -> error + tlsa_check: r = 0 -> nothing, r = usage + 1, r < 0 -> error +*/ + +/* Caution: OpenSSL's X509_pubkey_digest() does not work as expected. + I've included now: X509_pkey_digest() and X509_cert_digest() (as makro) */ + +#define X509_cert_digest X509_digest + +int tls_certkey(SSL_CTX *ctx,const char *cert,const char *key,char *ppwd) +{ + if (!cert) return 0; + + if (SSL_CTX_use_certificate_chain_file(ctx,cert) != 1) + return -1; + + if (!key) key = cert; + + if (ppwd) SSL_CTX_set_default_passwd_cb_userdata(ctx,ppwd); + + if (SSL_CTX_use_PrivateKey_file(ctx,key,SSL_FILETYPE_PEM) != 1) + return -2; + + if (SSL_CTX_check_private_key(ctx) != 1) + return -3; + + return 0; +} + +int tls_conn(SSL *ssl,int smtpfd) +{ + SSL_set_options(ssl,SSL_OP_NO_SSLv2); + SSL_set_options(ssl,SSL_OP_NO_SSLv3); + return SSL_set_fd(ssl,smtpfd); +} + +int tls_checkpeer(SSL *ssl,X509 *cert,const stralloc host,const int flag,const int verify) +{ + STACK_OF(GENERAL_NAME) *extensions; + const GENERAL_NAME *ext; + char buf[SSL_NAME_LEN]; + char *dnsname = 0; + int dname = 0; + int num; + int len; + int fflag; + int i; + int rc = 0; + + fflag = flag; + if (flag > 20) fflag = flag - 20; + if (flag > 10) fflag = flag - 10; + + /* X.509 CA DN/SAN name validation against DNS */ + + if (host.len && fflag > 4) { + extensions = (GENERAL_NAME *)X509_get_ext_d2i(cert,NID_subject_alt_name,0,0); + num = sk_GENERAL_NAME_num(extensions); /* num = 0, if no SAN extensions */ + + for (i = 0; i < num; ++i) { + ext = sk_GENERAL_NAME_value(extensions,i); + if (ext->type == GEN_DNS) { + #if (OPENSSL_VERSION_NUMBER < 0x10100000L) // 0xmnnffppsL + if (ASN1_STRING_type(ext->d.ia5) != V_ASN1_IA5STRING) continue; + dnsname = (char *)ASN1_STRING_data(ext->d.ia5); + #else + if (OBJ_sn2nid((const char*)ext->d.ia5) != V_ASN1_IA5STRING) continue; + dnsname = (char *)ASN1_STRING_get0_data(ext->d.ia5); + #endif + len = ASN1_STRING_length(ext->d.ia5); + dname = 1; + } + } + + if (!dname) { + X509_NAME_get_text_by_NID(X509_get_subject_name(cert),NID_commonName,buf,sizeof(buf)); + buf[SSL_NAME_LEN - 1] = 0; + dnsname = buf; + len = SSL_NAME_LEN - 1; + } + + switch (fflag) { + case 5: if (dnsname[0] == '*' && dnsname[1] == '.') + if (case_diffrs(dnsname + 1,host.s)) return -3; + if (case_diffrs(dnsname,host.s)) return -3; + rc = 3; break; + case 6: if (case_diffs(dnsname,host.s)) return -3; + rc = 2; break; + } + } + + /* X.509 CA Verification: root CA must be available */ + + if (fflag > 3 && verify > -2) { + if (SSL_get_verify_result(ssl) != X509_V_OK) return -2; + else rc = 1; + } + + return rc; +} + +int tls_checkcrl(SSL *ssl) // not implemented yet +{ + + return 0; +} + +int dig_ascii(char *digascii,const char *digest,const int len) +{ + static const char hextab[] = "0123456789abcdef"; + int j; + + for (j = 0; j < len; j++) { + digascii[2 * j] = hextab[(unsigned char)digest[j] >> 4]; + digascii[2 * j + 1] = hextab[(unsigned char)digest[j] & 0x0f]; + } + digascii[2 * len] = '\0'; + + return (2 * j); // 2*len +} + +/* X509_pkey_digest() takes the same args as X509_digest(); + however returning the correct hash of pubkey in md. + Subjects keys are restricted to 2048 byte in size. + Return codes: 1: sucess, 0: failed. */ + +int X509_pkey_digest(const X509 *cert,const EVP_MD *type,unsigned char *md,unsigned int *dlen) +{ + unsigned int len = 0; + unsigned int size = 2048; + unsigned char *buf; + unsigned char *buf2; + unsigned char buffer[size]; // avoid malloc + +/* Following Viktor's suggestion */ + + if (!X509_get0_pubkey_bitstr(cert)) return 0; // no Subject public key + + len = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(cert),0); + if (len > size) return 0; + buf2 = buf = buffer; + i2d_X509_PUBKEY(X509_get_X509_PUBKEY(cert),(unsigned char **)&buf2); + if (buf2 - buf != len) return 0; + + if (!EVP_Digest(buf,len,md,dlen,type,0)) return 0; // OpenSSL voodoo + return 1; +} + +/* Return codes: -4: no X.509 cert (fatal), -3: matching error (deferred), + -2: unsupported type, -1: weird TLSA record + 0: No X.509 cert; seen: usage++; */ + +int tlsa_check(const STACK_OF(X509) *certs,const stralloc host,const unsigned long p) +{ + const EVP_MD *methodsha256 = EVP_sha256(); + const EVP_MD *methodsha512 = EVP_sha512(); + stralloc out = {0}; + stralloc sa = {0}; + stralloc cn = {0}; + unsigned char digest[EVP_MAX_MD_SIZE]; + unsigned int dlen = 0; + unsigned int n = 0; + int i = 0; + int r; + char port[FMT_ULONG]; + uint16 type; + uint16 selector; + uint16 usage; + +// construct TLSA FQDN -- simple procedure; returning Usage + + if (host.len < 2) return 0; + if (!stralloc_copyb(&sa,"_",1)) temp_nomem(); + port[fmt_ulong(port,p)] = 0; + if (!stralloc_cats(&sa,port)) temp_nomem(); + if (!stralloc_cats(&sa,"._tcp.")) temp_nomem(); + if (!stralloc_cats(&sa,host.s)) temp_nomem(); + + if (dns_cname(&cn,&sa) > 0) // query name could be a cname + { if (dns_tlsa(&out,&cn) <= 0) return 0; } + else + { if (dns_tlsa(&out,&sa) <= 0) return 0; } + if (out.len < 5) return -1; + + /* https://www.openssl.org/docs/man3.0/man3/X509_digest.html (1.1.1): + "The len parameter, if not NULL, points to a place where the digest size will be stored." + [sigh] + */ + + do { + usage = (unsigned char) out.s[i]; // Usage: PKIX-TA [0], PKIX-EE [1], DANE-TA [2], DANE-EE [3] + selector = (unsigned char) out.s[i + 1]; // Selector: 0 = Cert, 1 = SPKI + type = (unsigned char) out.s[i + 2]; // Type: 0/1/2 = [Cert|SPKI]/SHA256/SHA512 + + unsigned len = sk_X509_num(certs); + for (n = 0; n < len; n++) { + X509 *cert = sk_X509_value(certs,n); + if (type == 1) { + if (selector == 0) r = X509_cert_digest(cert,methodsha256,digest,&dlen); + if (selector == 1) r = X509_pkey_digest(cert,methodsha256,digest,&dlen); + } else if (type == 2) { + if (selector == 0) r = X509_cert_digest(cert,methodsha512,digest,&dlen); + if (selector == 1) r = X509_pkey_digest(cert,methodsha512,digest,&dlen); + } else + return -2; + + if (!byte_diff(digest,dlen,out.s + i + 3)) return ++usage; + } + + i += (dlen + 3); + } while (i < out.len - 4); + + return -3; +} + +int tls_fingerprint(X509 *cert,const char *fingerprint,int dlen) +{ + const EVP_MD *methodsha1 = EVP_sha1(); + const EVP_MD *methodsha224 = EVP_sha224(); + const EVP_MD *methodsha256 = EVP_sha256(); + const EVP_MD *methodsha512 = EVP_sha512(); + unsigned char digest[EVP_MAX_MD_SIZE]; + unsigned char digascii[257]; + unsigned int len; + + switch (dlen) { /* fetch digest from cert; len = bitlength/8 */ + case 40: if (!X509_digest(cert,methodsha1,digest,&len)) return -2; + case 56: if (!X509_digest(cert,methodsha224,digest,&len)) return -2; + case 64: if (!X509_digest(cert,methodsha256,digest,&len)) return -2; + case 128: if (!X509_digest(cert,methodsha512,digest,&len)) return -2; + default: return -3; + } + + len = dig_ascii(digascii,digest,len); + if (!str_diffn(digascii,fingerprint,len)) return 1; + + return 0; +} + +int tls_exit(SSL *ssl) +{ + if (SSL_shutdown(ssl) == 0) + SSL_shutdown(ssl); + + return 0; +} + +/** @brief tls_destination + @param stralloc hostname (maybe 0-terminated) + + Certificate Fallthru + + @return values: | ADH | Cert *DN FQDN Hash | noTLSA noTLS + ----------+-----+--------------------+------------- + optional TLS | 1 | 3 - - - | - 9 + mandatory TLS | 2 | 4 5 6 7 | 8 + + no TLS -1 + */ + +int tls_destination(const stralloc hostname) +{ + int i; + stralloc tlshost = {0}; + stralloc tlsdest = {0}; + + if (!stralloc_copy(&tlshost,&hostname)) temp_nomem(); + if (!stralloc_0(&tlshost)) temp_nomem(); + +// Host rules + + if (!stralloc_copys(&tlsdest,"!")) temp_nomem(); + if (!stralloc_cats(&tlsdest,tlshost.s)) temp_nomem(); + if ((tlsdestinfo = constmap(&maptlsdestinations,tlsdest.s,tlsdest.len))) return -1; + + if (!stralloc_copys(&tlsdest,"?")) temp_nomem(); + if (!stralloc_cats(&tlsdest,tlshost.s)) temp_nomem(); + if ((tlsdestinfo = constmap(&maptlsdestinations,tlsdest.s,tlsdest.len))) return 9; + + if (!stralloc_copys(&tlsdest,"/")) temp_nomem(); + if (!stralloc_cats(&tlsdest,tlshost.s)) temp_nomem(); + if ((tlsdestinfo = constmap(&maptlsdestinations,tlsdest.s,tlsdest.len))) return 8; + + if (!stralloc_copys(&tlsdest,"%")) temp_nomem(); // CERT + hash + if (!stralloc_cats(&tlsdest,tlshost.s)) temp_nomem(); + if ((tlsdestinfo = constmap(&maptlsdestinations,tlsdest.s,tlsdest.len))) return 7; + + if (!stralloc_copys(&tlsdest,"=")) temp_nomem(); // CERT + FQDN + if (!stralloc_cats(&tlsdest,tlshost.s)) temp_nomem(); + if ((tlsdestinfo = constmap(&maptlsdestinations,tlsdest.s,tlsdest.len))) return 6; + + if (!stralloc_copys(&tlsdest,"~")) temp_nomem(); // CERT + Wild + if (!stralloc_cats(&tlsdest,tlshost.s)) temp_nomem(); + if ((tlsdestinfo = constmap(&maptlsdestinations,tlsdest.s,tlsdest.len))) return 5; + +// Domain rules + + for (i = 0; i < tlshost.len; ++i) // TLS fallthru + if ((i == 0) || (tlshost.s[i] == '.')) { + if (!stralloc_copys(&tlsdest,"?")) temp_nomem(); + if (!stralloc_cats(&tlsdest,tlshost.s + i)) temp_nomem(); + if ((tlsdestinfo = constmap(&maptlsdestinations,tlsdest.s,tlsdest.len))) return 9; + } + + for (i = 0; i < tlshost.len; ++i) // no TLSA + if ((i == 0) || (tlshost.s[i] == '.')) { + if (!stralloc_copys(&tlsdest,"/")) temp_nomem(); + if (!stralloc_cats(&tlsdest,tlshost.s + i)) temp_nomem(); + if ((tlsdestinfo = constmap(&maptlsdestinations,tlsdest.s,tlsdest.len))) return 8; + } + + for (i = 0; i < tlshost.len; ++i) // CERT + Wild + if ((i == 0) || (tlshost.s[i] == '.')) { + if (!stralloc_copys(&tlsdest,"~")) temp_nomem(); + if (!stralloc_cats(&tlsdest,tlshost.s + i)) temp_nomem(); + if ((tlsdestinfo = constmap(&maptlsdestinations,tlsdest.s,tlsdest.len))) return 5; + } + + for (i = 0; i < tlshost.len; ++i) // CERT - generic + if ((i == 0) || (tlshost.s[i] == '.')) { + if (!stralloc_copys(&tlsdest,"")) temp_nomem(); + if (!stralloc_cats(&tlsdest,tlshost.s + i)) temp_nomem(); + if ((tlsdestinfo = constmap(&maptlsdestinations,tlsdest.s,tlsdest.len))) return 4; + } + + for (i = 0; i < tlshost.len; ++i) // ADH per host/domain + if ((i == 0) || (tlshost.s[i] == '.')) { + if (!stralloc_copys(&tlsdest,"-")) temp_nomem(); + if (!stralloc_cats(&tlsdest,tlshost.s + i)) temp_nomem(); + if ((tlsdestinfo = constmap(&maptlsdestinations,tlsdest.s,tlsdest.len))) return 2; + } + +// General rules (mandatory TLS) + + tlsdestinfo = 0; + if (constmap(&maptlsdestinations,"/*",2)) return 8; // no TLSA + if (constmap(&maptlsdestinations,"=*",2)) return 6; // CERT + FQDN + if (constmap(&maptlsdestinations,"~*",2)) return 5; // CERT + Wild + if (constmap(&maptlsdestinations,"+*",2)) return 4; // CERT + if (constmap(&maptlsdestinations,"-*",2)) return 2; // ADH + +// Fall thru rules (optional TLS) + + if (constmap(&maptlsdestinations,"?",1)) return 9; // fallback to no TLS + if (constmap(&maptlsdestinations,"*",1)) return 3; // CERT + if (constmap(&maptlsdestinations,"-",1)) return 1; // ADH + + return 0; +} + +int tls_domaincerts(const stralloc domainname) +{ + int i; + tlsdomaininfo = 0; // extern + +/* Our Certs - per domain */ + + if (domainname.len) + for (i = 0; i < domainname.len; ++i) + if ((i == 0) || (domainname.s[i] == '.')) + if ((tlsdomaininfo = constmap(&mapdomaincerts,domainname.s + i,domainname.len - i))) return 2; + +/* Standard Cert (if any) */ + + if ((tlsdomaininfo = constmap(&mapdomaincerts,"*",1))) return 1; + + return 0; +} |