int main( int ac, char *av[ ] ) { yastr test_str; struct dmarc *d; if (( ac < 2 ) || ( ac > 5 )) { fprintf( stderr, "Usage:\t\t%s hostname [ 5322.From domain ] [ SPF domain ] [ DKIM domain ]\n", av[ 0 ] ); exit( 1 ); } if ( simta_read_config( SIMTA_FILE_CONFIG ) < 0 ) { exit( 1 ); } if ( simta_config( ) != 0 ) { exit( 1 ); } simta_openlog( 0, LOG_PERROR ); dmarc_init( &d ); dmarc_lookup( d, av[ 1 ] ); printf( "DMARC lookup result: policy %s, percent %d, result %s\n", dmarc_result_str( d->policy ), d->pct, dmarc_result_str( d->result )); if ( ac > 2 ) { d->domain = av[ 2 ]; } test_str = yaslauto( d->domain ); if ( ac > 3 ) { dmarc_spf_result( d, av[ 3 ] ); test_str = yaslcat( test_str, "/" ); test_str = yaslcat( test_str, av[ 3 ] ); } if ( ac > 4 ) { dmarc_dkim_result( d, av[ 4 ] ); test_str = yaslcat( test_str, "/" ); test_str = yaslcat( test_str, av[ 4 ] ); } printf( "DMARC policy result for %s: %s\n", test_str, dmarc_result_str( dmarc_result( d ))); exit( 0 ); }
struct spf * spf_lookup( const char *helo, const char *email, const struct sockaddr *addr ) { char *p; struct spf *s; s = calloc( 1, sizeof( struct spf )); s->spf_queries = 0; s->spf_sockaddr = addr; s->spf_helo = yaslauto( helo ); if ( strlen( email ) == 0 ) { /* RFC 7208 2.4 The "MAIL FROM" Identity * When the reverse-path is null, this document defines the "MAIL FROM" * identity to be the mailbox composed of the local-part "postmaster" * and the "HELO" identity */ s->spf_domain = yaslauto( helo ); s->spf_localpart = yaslauto( "postmaster" ); } else if (( p = strrchr( email, '@' )) != NULL ) { s->spf_domain = yaslauto( p + 1 ); s->spf_localpart = yaslnew( email, (size_t) (p - email )); } else { /* RFC 7208 4.3 Initial Processing * If the <sender> has no local-part, substitute the string * "postmaster" for the local-part. */ s->spf_domain = yaslauto( email ); s->spf_localpart = yaslauto( "postmaster" ); } simta_debuglog( 2, "SPF %s: localpart %s", s->spf_domain, s->spf_localpart ); s->spf_result = spf_check_host( s, s->spf_domain ); return( s ); }
yastr env_dkim_sign( struct envelope *env ) { char df[ MAXPATHLEN + 1 ]; DKIM_LIB *libhandle; unsigned int flags; DKIM *dkim; DKIM_STAT result; yastr signature = NULL; yastr key = NULL; yastr tmp = NULL; yastr *split = NULL; size_t tok_count = 0; char buf[ 1024 * 1024 ]; unsigned char *dkim_header; SNET *snet; ssize_t chunk; sprintf( df, "%s/D%s", env->e_dir, env->e_id ); if (( snet = snet_open( simta_dkim_key, O_RDONLY, 0, 1024 * 1024 )) == NULL ) { syslog( LOG_ERR, "Liberror: env_dkim_sign snet_open %s: %m", simta_dkim_key ); return( NULL ); } key = yaslempty(); while (( chunk = snet_read( snet, buf, 1024 * 1024, NULL )) > 0 ) { key = yaslcatlen( key, buf, chunk ); } snet_close( snet ); if (( libhandle = dkim_init( NULL, NULL )) == NULL ) { syslog( LOG_ERR, "Liberror: env_dkim_sign dkim_init" ); return( NULL ); } /* Data is stored in UNIX format, so tell libopendkim to fix * CRLF issues. */ flags = DKIM_LIBFLAGS_FIXCRLF; dkim_options( libhandle, DKIM_OP_SETOPT, DKIM_OPTS_FLAGS, &flags, sizeof( flags )); /* Only sign the headers recommended by RFC 6376 */ dkim_options( libhandle, DKIM_OP_SETOPT, DKIM_OPTS_SIGNHDRS, dkim_should_signhdrs, sizeof( unsigned char ** )); if (( dkim = dkim_sign( libhandle, (unsigned char *)(env->e_id), NULL, (unsigned char *)key, (unsigned char *)simta_dkim_selector, (unsigned char *)simta_dkim_domain, DKIM_CANON_RELAXED, DKIM_CANON_RELAXED, DKIM_SIGN_RSASHA256, -1, &result )) == NULL ) { syslog( LOG_NOTICE, "Liberror: env_dkim_sign dkim_sign: %s", dkim_getresultstr( result )); goto error; } if (( snet = snet_open( df, O_RDONLY, 0, 1024 * 1024 )) == NULL ) { syslog( LOG_ERR, "Liberror: env_dkim_sign snet_open %s: %m", buf ); goto error; } while (( chunk = snet_read( snet, buf, 1024 * 1024, NULL )) > 0 ) { if (( result = dkim_chunk( dkim, (unsigned char *)buf, chunk )) != 0 ) { syslog( LOG_NOTICE, "Liberror: env_dkim_sign dkim_chunk: %s: %s", dkim_getresultstr( result ), dkim_geterror( dkim )); snet_close( snet ); goto error; } } snet_close( snet ); if (( result = dkim_chunk( dkim, NULL, 0 )) != 0 ) { syslog( LOG_NOTICE, "Liberror: env_dkim_sign dkim_chunk: %s: %s", dkim_getresultstr( result ), dkim_geterror( dkim )); goto error; } if (( result = dkim_eom( dkim, NULL )) != 0 ) { syslog( LOG_NOTICE, "Liberror: env_dkim_sign dkim_eom: %s: %s", dkim_getresultstr( result ), dkim_geterror( dkim )); goto error; } if (( result = dkim_getsighdr_d( dkim, 16, &dkim_header, (size_t *)&chunk )) != 0 ) { syslog( LOG_NOTICE, "Liberror: env_dkim_sign dkim_getsighdr_d: %s: %s", dkim_getresultstr( result ), dkim_geterror( dkim )); goto error; } /* Get rid of carriage returns in libopendkim output */ split = yaslsplitlen( (const char *)dkim_header, strlen( (const char *)dkim_header ), "\r", 1, &tok_count ); tmp = yasljoinyasl( split, tok_count, "", 0 ); signature = yaslcatyasl( yaslauto( "DKIM-Signature: " ), tmp ); error: yaslfree( tmp ); yaslfreesplitres( split, tok_count ); yaslfree( key ); dkim_free( dkim ); dkim_close( libhandle ); return( signature ); }
int spf_check_host( struct spf *s, const yastr domain ) { int i, j, rc, qualifier, ret = SPF_RESULT_NONE; struct dnsr_result *dnsr_res, *dnsr_res_mech = NULL; struct dnsr_string *txt; yastr record = NULL, redirect = NULL, domain_spec, tmp; size_t tok_count = 0; yastr *split = NULL; char *p; unsigned long cidr, cidr6; int mech_queries = 0; /* RFC 7208 3.1 DNS Resource Records * SPF records MUST be published as a DNS TXT (type 16) Resource Record * (RR) [RFC1035] only. */ if (( dnsr_res = get_txt( domain )) == NULL ) { syslog( LOG_WARNING, "SPF %s [%s]: TXT lookup %s failed", s->spf_domain, domain, domain ); return( SPF_RESULT_TEMPERROR ); } for ( i = 0 ; i < dnsr_res->r_ancount ; i++ ) { if ( dnsr_res->r_answer[ i ].rr_type == DNSR_TYPE_TXT ) { txt = dnsr_res->r_answer[ i ].rr_txt.txt_data; /* RFC 7208 4.5 Selecting Records * Starting with the set of records that were returned by the * lookup, discard records that do not begin with a version section * of exactly "v=spf1". Note that the version section is * terminated by either an SP character or the end of the record. */ if (( strncasecmp( txt->s_string, "v=spf1", 6 ) == 0 ) && (( txt->s_string[ 6 ] == ' ' ) || ( txt->s_string[ 6 ] == '\0' ))) { if ( record != NULL ) { /* RFC 7208 3.2 Multiple DNS Records * A domain name MUST NOT have multiple records that would * cause an authorization check to select more than one * record. */ syslog( LOG_ERR, "SPF %s [%s]: multiple v=spf1 records found", s->spf_domain, domain ); ret = SPF_RESULT_PERMERROR; goto cleanup; } record = yaslempty( ); /* RFC 7208 3.3 Multiple Strings in a Single DNS Record * If a published record contains multiple character-strings, * then the record MUST be treated as if those strings are * concatenated together without adding spaces. */ for ( ; txt != NULL ; txt = txt->s_next ) { record = yaslcat( record, txt->s_string ); } } } } if ( record == NULL ) { simta_debuglog( 1, "SPF %s [%s]: no SPF record found", s->spf_domain, domain ); goto cleanup; } simta_debuglog( 2, "SPF %s [%s]: record: %s", s->spf_domain, domain, record ); split = yaslsplitlen( record, yasllen( record ), " ", 1, &tok_count ); /* Start at 1, 0 is v=spf1 */ for ( i = 1 ; i < tok_count ; i++ ) { /* multiple spaces in a record will result in empty elements */ if ( yasllen( split[ i ] ) == 0 ) { continue; } /* RFC 7208 4.6.4 DNS Lookup Limits * Some mechanisms and modifiers (collectively, "terms") cause DNS * queries at the time of evaluation [...] SPF implementations MUST * limit the total number of those terms to 10 during SPF evaluation, * to avoid unreasonable load on the DNS. If this limit is exceeded, * the implementation MUST return "permerror". */ /* In real life strictly enforcing a limit of ten will break SPF * evaluation of multiple major domains, so we use a higher limit. */ if ( s->spf_queries > 25 ) { syslog( LOG_WARNING, "SPF %s [%s]: DNS lookup limit exceeded", s->spf_domain, domain ); ret = SPF_RESULT_PERMERROR; goto cleanup; } /* RFC 7208 4.6.2 Mechanisms * The possible qualifiers, and the results they cause check_host() to * return, are as follows: * * "+" pass * "-" fail * "~" softfail * "?" neutral * * The qualifier is optional and defaults to "+". */ switch ( *split[ i ] ) { case '+': qualifier = SPF_RESULT_PASS; yaslrange( split[ i ], 1, -1 ); break; case '-': qualifier = SPF_RESULT_FAIL; yaslrange( split[ i ], 1, -1 ); break; case '~': qualifier = SPF_RESULT_SOFTFAIL; yaslrange( split[ i ], 1, -1 ); break; case '?': qualifier = SPF_RESULT_NEUTRAL; yaslrange( split[ i ], 1, -1 ); break; default: qualifier = SPF_RESULT_PASS; break; } if ( strncasecmp( split[ i ], "redirect=", 9 ) == 0 ) { s->spf_queries++; redirect = split[ i ]; yaslrange( redirect, 9, -1 ); simta_debuglog( 2, "SPF %s [%s]: redirect to %s", s->spf_domain, domain, redirect ); /* RFC 7208 5.1 "all" * The "all" mechanism is a test that always matches. */ } else if ( strcasecmp( split[ i ], "all" ) == 0 ) { simta_debuglog( 2, "SPF %s [%s]: matched all: %s", s->spf_domain, domain, spf_result_str( qualifier )); ret = qualifier; goto cleanup; /* RFC 7208 5.2 "include" * The "include" mechanism triggers a recursive evaluation of * check_host(). */ } else if ( strncasecmp( split[ i ], "include:", 8 ) == 0 ) { s->spf_queries++; yaslrange( split[ i ], 8, -1 ); simta_debuglog( 2, "SPF %s [%s]: include %s", s->spf_domain, domain, split[ i ] ); rc = spf_check_host( s, split[ i ] ); switch ( rc ) { case SPF_RESULT_NONE: ret = SPF_RESULT_PERMERROR; goto cleanup; case SPF_RESULT_PASS: ret = qualifier; goto cleanup; case SPF_RESULT_TEMPERROR: case SPF_RESULT_PERMERROR: ret = rc; goto cleanup; } /* RFC 7208 5.3 "a" */ } else if (( strcasecmp( split[ i ], "a" ) == 0 ) || ( strncasecmp( split[ i ], "a:", 2 ) == 0 ) || ( strncasecmp( split[ i ], "a/", 2 ) == 0 )) { s->spf_queries++; yaslrange( split[ i ], 1, -1 ); if (( domain_spec = spf_parse_domainspec_cidr( s, domain, split[ i ], &cidr, &cidr6 )) == NULL ) { /* Macro expansion failed, probably a syntax problem. */ ret = SPF_RESULT_PERMERROR; goto cleanup; } rc = spf_check_a( s, domain, cidr, cidr6, domain_spec ); switch( rc ) { case SPF_RESULT_PASS: simta_debuglog( 2, "SPF %s [%s]: matched a %s/%ld/%ld: %s", s->spf_domain, domain, domain_spec, cidr, cidr6, spf_result_str( qualifier )); yaslfree( domain_spec ); ret = qualifier; goto cleanup; case SPF_RESULT_TEMPERROR: yaslfree( domain_spec ); ret = rc; goto cleanup; default: break; } yaslfree( domain_spec ); /* RFC 7208 5.4 "mx" */ } else if (( strcasecmp( split[ i ], "mx" ) == 0 ) || ( strncasecmp( split[ i ], "mx:", 3 ) == 0 ) || ( strncasecmp( split[ i ], "mx/", 3 ) == 0 )) { s->spf_queries++; mech_queries = 0; yaslrange( split[ i ], 2, -1 ); if (( domain_spec = spf_parse_domainspec_cidr( s, domain, split[ i ], &cidr, &cidr6 )) == NULL ) { /* Macro expansion failed, probably a syntax problem. */ ret = SPF_RESULT_PERMERROR; goto cleanup; } if (( dnsr_res_mech = get_mx( domain_spec )) == NULL ) { syslog( LOG_WARNING, "SPF %s [%s]: MX lookup %s failed", s->spf_domain, domain, domain_spec ); yaslfree( domain_spec ); ret = SPF_RESULT_TEMPERROR; goto cleanup; } for ( j = 0 ; j < dnsr_res_mech->r_ancount ; j++ ) { if ( dnsr_res_mech->r_answer[ j ].rr_type == DNSR_TYPE_MX ) { /* RFC 7208 4.6.4 DNS Lookup Limits * When evaluating the "mx" mechanism, the number of "MX" * resource records queried is included in the overall * limit of 10 mechanisms/modifiers that cause DNS lookups */ s->spf_queries++; rc = spf_check_a( s, domain, cidr, cidr6, dnsr_res_mech->r_answer[ j ].rr_mx.mx_exchange ); switch( rc ) { case SPF_RESULT_PASS: simta_debuglog( 2, "SPF %s [%s]: matched mx %s/%ld/%ld: %s", s->spf_domain, domain, domain_spec, cidr, cidr6, spf_result_str( qualifier )); ret = qualifier; dnsr_free_result( dnsr_res_mech ); yaslfree( domain_spec ); goto cleanup; case SPF_RESULT_PERMERROR: case SPF_RESULT_TEMPERROR: ret = rc; dnsr_free_result( dnsr_res_mech ); yaslfree( domain_spec ); goto cleanup; default: break; } } } dnsr_free_result( dnsr_res_mech ); yaslfree( domain_spec ); /* RFC 7208 5.5 "ptr" (do not use) */ } else if (( strcasecmp( split[ i ], "ptr" ) == 0 ) || ( strncasecmp( split[ i ], "ptr:", 4 ) == 0 )) { s->spf_queries++; mech_queries = 0; if (( dnsr_res_mech = get_ptr( s->spf_sockaddr )) == NULL ) { /* RFC 7208 5.5 "ptr" (do not use ) * If a DNS error occurs while doing the PTR RR lookup, * then this mechanism fails to match. */ continue; } if ( dnsr_res_mech->r_ancount == 0 ) { dnsr_free_result( dnsr_res_mech ); continue; } if ( split[ i ][ 3 ] == ':' ) { domain_spec = yaslnew(( split[ i ] + 4 ), ( yasllen( split[ i ] ) - 4 )); } else { domain_spec = yasldup( domain ); } for ( j = 0 ; j < dnsr_res_mech->r_ancount ; j++ ) { if ( dnsr_res_mech->r_answer[ j ].rr_type != DNSR_TYPE_PTR ) { continue; } /* We only care if it's a pass; like the initial PTR query, * DNS errors are treated as a non-match rather than an error. */ /* RFC 7208 4.6.4 DNS Lookup Limits * the evaluation of each "PTR" record MUST NOT result in * querying more than 10 address records -- either "A" or * "AAAA" resource records. If this limit is exceeded, all * records other than the first 10 MUST be ignored. */ if (( mech_queries++ < 10 ) && ( spf_check_a( s, domain, 32, 128, dnsr_res_mech->r_answer[ j ].rr_dn.dn_name ) == SPF_RESULT_PASS )) { tmp = yaslauto( dnsr_res_mech->r_answer[ j ].rr_dn.dn_name ); while (( yasllen( tmp ) > yasllen( domain_spec )) && ( p = strchr( tmp, '.' ))) { yaslrange( tmp, ( p - tmp + 1 ), -1 ); } rc = strcasecmp( tmp, domain_spec ); yaslfree( tmp ); if ( rc == 0 ) { simta_debuglog( 2, "SPF %s [%s]: matched ptr %s (%s): %s", s->spf_domain, domain, domain_spec, dnsr_res_mech->r_answer[ j ].rr_dn.dn_name, spf_result_str( qualifier )); ret = qualifier; yaslfree( domain_spec ); dnsr_free_result( dnsr_res_mech ); goto cleanup; } } } yaslfree( domain_spec ); dnsr_free_result( dnsr_res_mech ); /* RFC 7208 5.6 "ip4" and "ip6" * These mechanisms test whether <ip> is contained within a given * IP network. */ } else if ( strncasecmp( split[ i ], "ip4:", 4 ) == 0 ) { if ( s->spf_sockaddr->sa_family != AF_INET ) { continue; } yaslrange( split[ i ], 4, -1 ); if (( p = strchr( split[ i ], '/' )) != NULL ) { errno = 0; cidr = strtoul( p + 1, NULL, 10 ); if ( errno ) { syslog( LOG_WARNING, "SPF %s [%s]: failed parsing CIDR mask %s: %m", s->spf_domain, domain, p + 1 ); ret = SPF_RESULT_PERMERROR; goto cleanup; } if ( cidr > 32 ) { syslog( LOG_WARNING, "SPF %s [%s]: invalid CIDR mask: %ld", s->spf_domain, domain, cidr ); ret = SPF_RESULT_PERMERROR; goto cleanup; } yaslrange( split[ i ], 0, p - split[ i ] - 1 ); } else { cidr = 32; } if (( rc = simta_cidr_compare( cidr, s->spf_sockaddr, NULL, split[ i ] )) < 0 ) { syslog( LOG_WARNING, "SPF %s [%s]: simta_cidr_compare failed for %s", s->spf_domain, domain, split[ i ] ); ret = SPF_RESULT_PERMERROR; goto cleanup; } else if ( rc == 0 ) { simta_debuglog( 2, "SPF %s [%s]: matched ip4 %s/%ld: %s", s->spf_domain, domain, split[ i ], cidr, spf_result_str( qualifier )); ret = qualifier; goto cleanup; } } else if ( strncasecmp( split[ i ], "ip6:", 4 ) == 0 ) { if ( s->spf_sockaddr->sa_family != AF_INET6 ) { continue; } yaslrange( split[ i ], 4, -1 ); if (( p = strchr( split[ i ], '/' )) != NULL ) { errno = 0; cidr = strtoul( p + 1, NULL, 10 ); if ( errno ) { syslog( LOG_WARNING, "SPF %s [%s]: failed parsing CIDR mask %s: %m", s->spf_domain, domain, p + 1 ); } if ( cidr > 128 ) { syslog( LOG_WARNING, "SPF %s [%s]: invalid CIDR mask: %ld", s->spf_domain, domain, cidr ); ret = SPF_RESULT_PERMERROR; goto cleanup; } yaslrange( split[ i ], 0, p - split[ i ] - 1 ); } else { cidr = 128; } if (( rc = simta_cidr_compare( cidr, s->spf_sockaddr, NULL, split[ i ] )) < 0 ) { syslog( LOG_WARNING, "SPF %s [%s]: simta_cidr_compare failed for %s", s->spf_domain, domain, split[ i ] ); ret = SPF_RESULT_PERMERROR; goto cleanup; } else if ( rc == 0 ) { simta_debuglog( 2, "SPF %s [%s]: matched ip6 %s/%ld: %s", s->spf_domain, domain, split[ i ], cidr, spf_result_str( qualifier )); ret = qualifier; goto cleanup; } /* RFC 7208 5.7 "exists" */ } else if ( strncasecmp( split[ i ], "exists:", 7 ) == 0 ) { s->spf_queries++; yaslrange( split[ i ], 7, -1 ); if (( domain_spec = spf_macro_expand( s, domain, split[ i ] )) == NULL ) { /* Macro expansion failed, probably a syntax problem. */ ret = SPF_RESULT_PERMERROR; goto cleanup; } if (( dnsr_res_mech = get_a( domain_spec )) == NULL ) { syslog( LOG_WARNING, "SPF %s [%s]: A lookup %s failed", s->spf_domain, domain, domain_spec ); yaslfree( domain_spec ); ret = SPF_RESULT_TEMPERROR; goto cleanup; } if ( dnsr_res_mech->r_ancount > 0 ) { simta_debuglog( 2, "SPF %s [%s]: matched exists %s: %s", s->spf_domain, domain, domain_spec, spf_result_str( qualifier )); dnsr_free_result( dnsr_res_mech ); yaslfree( domain_spec ); ret = qualifier; goto cleanup; } yaslfree( domain_spec ); dnsr_free_result( dnsr_res_mech ); } else { for ( p = split[ i ] ; isalnum( *p ) ; p++ ); if ( *p == '=' ) { /* RFC 7208 6 Modifier Definitions * Unrecognized modifiers MUST be ignored */ simta_debuglog( 1, "SPF %s [%s]: %s unknown modifier %s", s->spf_domain, domain, spf_result_str( qualifier ), split[ i ] ); } else { syslog( LOG_WARNING, "SPF %s [%s]: %s unknown mechanism %s", s->spf_domain, domain, spf_result_str( qualifier ), split[ i ] ); ret = SPF_RESULT_PERMERROR; goto cleanup; } } } if ( redirect != NULL ) { if (( domain_spec = spf_macro_expand( s, domain, redirect )) == NULL ) { /* Macro expansion failed, probably a syntax problem. */ ret = SPF_RESULT_PERMERROR; } else { ret = spf_check_host( s, domain_spec ); yaslfree( domain_spec ); } if ( ret == SPF_RESULT_NONE ) { ret = SPF_RESULT_PERMERROR; } } else { /* RFC 7208 4.7 Default Result * If none of the mechanisms match and there is no "redirect" modifier, * then the check_host() returns a result of "neutral", just as if * "?all" were specified as the last directive. */ ret = SPF_RESULT_NEUTRAL; simta_debuglog( 2, "SPF %s [%s]: default result: %s", s->spf_domain, domain, spf_result_str( ret )); } cleanup: if ( split != NULL ) { yaslfreesplitres( split, tok_count ); } yaslfree( record ); dnsr_free_result( dnsr_res ); return( ret ); }