/* -*- Mode: C; tab-width: 4 -*-
 *
 * Copyright (c) 2012 Apple Computer, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// ***************************************************************************
// DNSSECSupport.c: Platform specific support for DNSSEC like fetching root
// trust anchor and dnssec probes etc.
// ***************************************************************************

#include "mDNSEmbeddedAPI.h"
#include "DNSCommon.h"                  // For mDNS_Lock, mDNS_Random
#include "dnssec.h"
#include "DNSSECSupport.h"

#include <CommonCrypto/CommonDigest.h>  // For Hash algorithms SHA1 etc.

// Following are needed for fetching the root trust anchor dynamically
#include <CoreFoundation/CoreFoundation.h>
#include <libxml2/libxml/parser.h>
#include <libxml2/libxml/tree.h>
#include <libxml2/libxml/xmlmemory.h>
#include <notify.h>

// 30 days
#define ROOT_TA_UPDATE_INTERVAL  (30 * 24 * 3600)   // seconds

// After 100 days, the test anchors are not valid. Just an arbitrary number
// to configure validUntil. 
#define TEST_TA_EXPIRE_INTERVAL  (100 * 24 * 4600)

// When we can't fetch the root TA due to network errors etc., we start off a timer
// to fire at 60 seconds and then keep doubling it till we fetch it
#define InitialTAFetchInterval 60

#if !TARGET_OS_IPHONE
DNSQuestion DNSSECProbeQuestion;
#endif

mDNSlocal int RegisterNotification(mDNS *const m, unsigned int interval);

mDNSlocal void LinkTrustAnchor(mDNS *const m, TrustAnchor *ta)
{
    int length = 0;
    int i;
    mDNSu8 *p;
    TrustAnchor **t = &m->TrustAnchors;
    char buffer[256];

    while (*t)
        t = &((*t)->next);
    *t = ta;

    buffer[0] = 0;
    p = ta->rds.digest;
    for (i = 0; i < ta->digestLen; i++)
    {
        length += mDNS_snprintf(buffer+length, sizeof(buffer)-1-length, "%x", p[i]);
    }
    LogInfo("LinkTrustAnchor: Zone %##s, keytag %d, alg %d, digestType %d, digestLen %d, digest %s", ta->zone.c, ta->rds.keyTag,
        ta->rds.alg, ta->rds.digestType, ta->digestLen, buffer);
}

mDNSlocal void DelTrustAnchor(mDNS *const m, const domainname *zone)
{
    TrustAnchor **ta = &m->TrustAnchors;
    TrustAnchor *tmp;

    while (*ta && !SameDomainName(&(*ta)->zone, zone))
        ta = &(*ta)->next;

    // First time, we won't find the TrustAnchor in the list as it has
    // not been added.
    if (!(*ta))
        return;

    tmp = *ta;
    *ta = (*ta)->next;                  // Cut this record from the list
    tmp->next = mDNSNULL;
    if (tmp->rds.digest)
        mDNSPlatformMemFree(tmp->rds.digest);
    mDNSPlatformMemFree(tmp);
}

mDNSlocal void AddTrustAnchor(mDNS *const m, const domainname *zone, mDNSu16 keytag, mDNSu8 alg, mDNSu8 digestType, int diglen,
    mDNSu8 *digest)
{
    TrustAnchor *ta, *tmp;
    mDNSu32 t = (mDNSu32) time(NULL); 

    // Check for duplicates
    tmp = m->TrustAnchors;
    while (tmp)
    {
        if (SameDomainName(zone, &tmp->zone) && tmp->rds.keyTag == keytag && tmp->rds.alg == alg && tmp->rds.digestType == digestType &&
            !memcmp(tmp->rds.digest, digest, diglen))
        {
            LogMsg("AddTrustAnchors: Found a duplicate");
            return;
        }
        tmp = tmp->next;
    }

    ta = (TrustAnchor *)mDNSPlatformMemAllocate(sizeof(TrustAnchor));
    if (!ta)
    {
        LogMsg("AddTrustAnchor: malloc failure ta");
        return;
    }
    ta->rds.keyTag = keytag;
    ta->rds.alg = alg;
    ta->rds.digestType = digestType;
    ta->rds.digest = digest;
    ta->digestLen = diglen;
    ta->validFrom = t;
    ta->validUntil = t + TEST_TA_EXPIRE_INTERVAL;
    AssignDomainName(&ta->zone, zone);
    ta->next = mDNSNULL;

    LinkTrustAnchor(m, ta);
}

#define HexVal(X) ( ((X) >= '0' && (X) <= '9') ? ((X) - '0'     ) :   \
                    ((X) >= 'A' && (X) <= 'F') ? ((X) - 'A' + 10) :   \
                    ((X) >= 'a' && (X) <= 'f') ? ((X) - 'a' + 10) : -1)

mDNSlocal mDNSu8 *ConvertDigest(char *digest, int digestType, int *diglen)
{
    int i, j;
    mDNSu8 *dig;

    switch (digestType)
    {
    case SHA1_DIGEST_TYPE:
        *diglen = CC_SHA1_DIGEST_LENGTH;
        break;
    case SHA256_DIGEST_TYPE:
        *diglen = CC_SHA256_DIGEST_LENGTH;
        break;
    default:
        LogMsg("ConvertDigest: digest type %d not supported", digestType);
        return mDNSNULL;
    }
    dig = mDNSPlatformMemAllocate(*diglen);
    if (!dig)
    {
        LogMsg("ConvertDigest: malloc failure");
        return mDNSNULL;
    }

    for (j=0,i=0; i<*diglen*2; i+=2)
    {
        int l, h;
        l = HexVal(digest[i]);
        h = HexVal(digest[i+1]);
        if (l<0 || h<0) { LogMsg("ConvertDigest: Cannot convert digest"); return NULL;}
        dig[j++] = (mDNSu8)((l << 4) | h);
    }
    return dig;
}

// All the children are in a linked list
//
// <TrustAnchor> has two children: <Zone> and <KeyDigest>
// <KeyDigest> has four children <KeyTag> <Algorithm> <DigestType> <Digest>
//
// Returns false if failed to parse the element i.e., malformed xml document.
// Validity of the actual values itself is done outside the function.
mDNSlocal mDNSBool ParseElementChildren(xmlDocPtr tadoc, xmlNode *node, TrustAnchor *ta)
{
    xmlNode *cur_node;
    xmlChar *val1, *val2, *val;
    char *invalid = NULL;

    val = val1 = val2 = NULL;

    for (cur_node = node; cur_node; cur_node = cur_node->next)
    {
        invalid = NULL;
        val1 = val2 = NULL;
        
        val = xmlNodeListGetString(tadoc, cur_node->xmlChildrenNode, 1);
        if (!val)
        {
           LogInfo("ParseElementChildren: NULL value for %s", cur_node->name);
           continue; 
        }
        if (!xmlStrcmp(cur_node->name, (const xmlChar *)"Zone"))
        {
            // MaeDomainNameFromDNSNameString does not work for "."
            if (!xmlStrcmp(val, (const xmlChar *)"."))
            {
                ta->zone.c[0] = 0;
            }
            else if (!MakeDomainNameFromDNSNameString(&ta->zone, (char *)val))
            {
                LogMsg("ParseElementChildren: Cannot parse Zone %s", val);
                goto error;
            }
            else
            {
                LogInfo("ParseElementChildren: Element %s, value %##s", cur_node->name, ta->zone.c);
            }
        }
        else if (!xmlStrcmp(cur_node->name, (const xmlChar *)"KeyTag"))
        {
            ta->rds.keyTag = strtol((const char *)val, &invalid, 10);
            if (*invalid != '\0')
            {
                LogMsg("ParseElementChildren: KeyTag invalid character %d", *invalid);
                goto error;
            }
            else
            {
                LogInfo("ParseElementChildren: Element %s, value %d", cur_node->name, ta->rds.keyTag);
            }
        }
        else if (!xmlStrcmp(cur_node->name, (const xmlChar *)"Algorithm"))
        {
            ta->rds.alg = strtol((const char *)val, &invalid, 10);
            if (*invalid != '\0')
            {
                LogMsg("ParseElementChildren: Algorithm invalid character %c", *invalid);
                goto error;
            }
            else
            {
                LogInfo("ParseElementChildren: Element %s, value %d", cur_node->name, ta->rds.alg);
            }
        }
        else if (!xmlStrcmp(cur_node->name, (const xmlChar *)"DigestType"))
        {
            ta->rds.digestType = strtol((const char *)val, &invalid, 10);
            if (*invalid != '\0')
            {
                LogMsg("ParseElementChildren: Algorithm invalid character %c", *invalid);
                goto error;
            }
            else
            {
                LogInfo("ParseElementChildren: Element %s, value %d", cur_node->name, ta->rds.digestType);
            }
        }
        else if (!xmlStrcmp(cur_node->name, (const xmlChar *)"Digest"))
        {
            int diglen;
            mDNSu8 *dig = ConvertDigest((char *)val, ta->rds.digestType, &diglen);
            if (dig)
            { 
                LogInfo("ParseElementChildren: Element %s, digest %s", cur_node->name, val);
                ta->digestLen = diglen;
                ta->rds.digest = dig;
            }
            else
            {
                LogMsg("ParseElementChildren: Element %s, NULL digest", cur_node->name);
                goto error;
            }
        }
        else if (!xmlStrcmp(cur_node->name, (const xmlChar *)"KeyDigest"))
        {
            struct tm tm;
            val1 = xmlGetProp(cur_node, (const xmlChar *)"validFrom");
            if (val1)
            {
                char *s = strptime((const char *)val1, "%Y-%m-%dT%H:%M:%S", &tm);
                if (!s)
                {
                    LogMsg("ParseElementChildren: Parsing ValidFrom failed %s", val1);
                    goto error;
                }
                else
                {
                    ta->validFrom = (mDNSu32)timegm(&tm);
                }
            }
            val2 = xmlGetProp(cur_node, (const xmlChar *)"validUntil");
            if (val2)
            {
                char *s = strptime((const char *)val2, "%Y-%m-%dT%H:%M:%S", &tm);
                if (!s)
                {
                    LogMsg("ParseElementChildren: Parsing ValidFrom failed %s", val2);
                    goto error;
                }
                else
                {
                    ta->validUntil = (mDNSu32)timegm(&tm);
                }
            }
            else
            {
                // If there is no validUntil, set it to the next probing interval
                mDNSu32 t = (mDNSu32) time(NULL); 
                ta->validUntil = t + ROOT_TA_UPDATE_INTERVAL;
            }
            LogInfo("ParseElementChildren: ValidFrom time %u, validUntil %u", (unsigned)ta->validFrom, (unsigned)ta->validUntil);
        }
        if (val1)
            xmlFree(val1);
        if (val2)
            xmlFree(val2);
        if (val)
            xmlFree(val);
    }
    return mDNStrue;
error:
    if (val1)
        xmlFree(val1);
    if (val2)
        xmlFree(val2);
    if (val)
        xmlFree(val);
    return mDNSfalse;
}

mDNSlocal mDNSBool ValidateTrustAnchor(TrustAnchor *ta)
{
    time_t currTime = time(NULL);

    // Currently only support trust anchor for root.
    if (!SameDomainName(&ta->zone, (const domainname *)"\000"))
    {
        LogInfo("ParseElementChildren: Zone %##s not root", ta->zone.c);
        return mDNSfalse;
    }

    switch (ta->rds.digestType)
    {
    case SHA1_DIGEST_TYPE:
        if (ta->digestLen != CC_SHA1_DIGEST_LENGTH) 
        {
            LogMsg("ValidateTrustAnchor: Invalid digest len %d for SHA1", ta->digestLen);
            return mDNSfalse;
        }
        break;
    case SHA256_DIGEST_TYPE:
        if (ta->digestLen != CC_SHA256_DIGEST_LENGTH) 
        {
            LogMsg("ValidateTrustAnchor: Invalid digest len %d for SHA256", ta->digestLen);
            return mDNSfalse;
        }
        break;
    default:
        LogMsg("ValidateTrustAnchor: digest type %d not supported", ta->rds.digestType);
        return mDNSfalse;
    }
    if (!ta->rds.digest)
    {
        LogMsg("ValidateTrustAnchor: digest NULL for %d", ta->rds.digestType);
        return mDNSfalse;
    }
    switch (ta->rds.alg)
    {
    case CRYPTO_RSA_SHA512:
    case CRYPTO_RSA_SHA256:
    case CRYPTO_RSA_NSEC3_SHA1:
    case CRYPTO_RSA_SHA1:
        break;
    default:
        LogMsg("ValidateTrustAnchor: Algorithm %d not supported", ta->rds.alg);
        return mDNSfalse;
    }
    
    if (DNS_SERIAL_LT(currTime, ta->validFrom))
    {
        LogMsg("ValidateTrustAnchor: Invalid ValidFrom time %u, currtime %u", (unsigned)ta->validFrom, (unsigned)currTime);
        return mDNSfalse;
    }
    if (DNS_SERIAL_LT(ta->validUntil, currTime))
    {
        LogMsg("ValidateTrustAnchor: Invalid ValidUntil time %u, currtime %u", (unsigned)ta->validUntil, (unsigned)currTime);
        return mDNSfalse;
    }
    return mDNStrue;
}

mDNSlocal mDNSBool ParseElement(xmlDocPtr tadoc, xmlNode * a_node, TrustAnchor *ta)
{
    xmlNode *cur_node = NULL;

    for (cur_node = a_node; cur_node; cur_node = cur_node->next)
    {
        if (cur_node->type == XML_ELEMENT_NODE)
        {
            // There could be multiple KeyDigests per TrustAnchor. We keep parsing till we
            // reach the last one or we encounter an error in parsing the document.
            if (!xmlStrcmp(cur_node->name, (const xmlChar *)"KeyDigest"))
            {
                if (ta->rds.digest)
                    mDNSPlatformMemFree(ta->rds.digest);
                ta->rds.digestType = 0;
                ta->digestLen = 0;
            }
            if (!ParseElementChildren(tadoc, cur_node->children, ta))
                return mDNSfalse;
            if (!ParseElement(tadoc, cur_node->children, ta))
                return mDNSfalse;
        }
    }
    return mDNStrue;
}

mDNSlocal void TAComplete(mDNS *const m, void *context)
{
    TrustAnchor *ta = (TrustAnchor *)context;

    DelTrustAnchor(m, &ta->zone);
    LinkTrustAnchor(m, ta);
}

mDNSlocal void FetchRootTA(mDNS *const m)
{
    CFStringRef urlString = CFSTR("https://data.iana.org/root-anchors/root-anchors.xml");
    CFDataRef xmlData;
    CFStringRef fileRef = NULL;
    const char *xmlFileName = NULL;
    char buf[512];
    CFURLRef url = NULL;
    static unsigned int RootTAFetchInterval = InitialTAFetchInterval;

    (void) m;

    TrustAnchor *ta = (TrustAnchor *)mDNSPlatformMemAllocate(sizeof(TrustAnchor));
    if (!ta)
    {
        LogMsg("FetchRootTA: TrustAnchor alloc failed");
        return;
    }
    memset(ta, 0, sizeof(TrustAnchor));

    url = CFURLCreateWithString(NULL, urlString, NULL);
    if (!url)
    {
        LogMsg("FetchRootTA: CFURLCreateWithString error");
        mDNSPlatformMemFree(ta);
        return;
    }

    // If we can't fetch the XML file e.g., network problems, trigger a timer. All other failures
    // should hardly happen in practice for which schedule the normal interval to refetch the TA.
    if (!CFURLCreateDataAndPropertiesFromResource(kCFAllocatorDefault, url, &xmlData, NULL, NULL, NULL))
    {
        LogInfo("FetchRootTA: CFURLCreateDataAndPropertiesFromResource error");
        CFRelease(url);
        mDNSPlatformMemFree(ta);
        RegisterNotification(m, RootTAFetchInterval);
        RootTAFetchInterval *= 2 + 1;
        return;
    }

    // get the name of the last component from the url, libxml will use it if
    // it has to report an error
    fileRef = CFURLCopyLastPathComponent(url);
    if (fileRef)
    {
        xmlFileName = CFStringGetCStringPtr(fileRef, kCFStringEncodingUTF8);
        if (!xmlFileName)
        {
            if (!CFStringGetCString(fileRef, buf, sizeof(buf), kCFStringEncodingUTF8) )
                strlcpy(buf, "nofile.xml", sizeof(buf));
            xmlFileName = (const char *)buf;
        }
    }

    // Parse the XML and get the CFXMLTree.
    xmlDocPtr tadoc = xmlReadMemory((const char*)CFDataGetBytePtr(xmlData),
        (int)CFDataGetLength(xmlData), xmlFileName, NULL, 0);        

    CFRelease(fileRef);
    CFRelease(url);
    CFRelease(xmlData);

    if (!tadoc)
    {
        LogMsg("FetchRootTA: xmlReadMemory failed");
        goto done;
    }

    xmlNodePtr root = xmlDocGetRootElement(tadoc);
    if (!root)
    {
        LogMsg("FetchRootTA: Cannot get root element");
        goto done;
    }

    if (ParseElement(tadoc, root, ta) && ValidateTrustAnchor(ta))
    {
        // Do the actual addition of TA on the main queue.
        mDNSPlatformDispatchAsync(m, ta, TAComplete);
    }
    else
    {
        if (ta->rds.digest)
            mDNSPlatformMemFree(ta->rds.digest);
        mDNSPlatformMemFree(ta);
    }
done:
    if (tadoc)
        xmlFreeDoc(tadoc);
    RegisterNotification(m, ROOT_TA_UPDATE_INTERVAL);
    RootTAFetchInterval = InitialTAFetchInterval;
    return;
}


#if APPLE_OSX_mDNSResponder && !TARGET_OS_IPHONE
mDNSlocal void DNSSECProbeCallback(mDNS *const m, DNSQuestion *question, const ResourceRecord *const answer, QC_result AddRecord)
{
    if (!AddRecord)
        return;

    mDNS_Lock(m);
    if ((m->timenow - question->StopTime) >= 0)
    {
        mDNS_Unlock(m);
        LogDNSSEC("DNSSECProbeCallback: Question %##s (%s) timed out", question->qname.c, DNSTypeName(question->qtype));
        mDNS_StopQuery(m, question);
        return;
    }
    mDNS_Unlock(m);

    // Wait till we get the DNSSEC results. If we get a negative response e.g., no DNS servers, the
    // question will be restarted by the core and we should have the DNSSEC results eventually.
    if (AddRecord != QC_dnssec)
    {
        LogDNSSEC("DNSSECProbeCallback: Question %##s (%s)", question->qname.c, DNSTypeName(question->qtype), RRDisplayString(m, answer));
        return;
    }

    LogDNSSEC("DNSSECProbeCallback: Question %##s (%s), DNSSEC status %s", question->qname.c, DNSTypeName(question->qtype),
            DNSSECStatusName(question->ValidationStatus));

    mDNS_StopQuery(m, question);
}

// Send a DNSSEC probe just for the sake of collecting DNSSEC statistics.
mDNSexport void DNSSECProbe(mDNS *const m)
{
    mDNSu32 rand;

    if (DNSSECProbeQuestion.ThisQInterval != -1)
        return;
    
    rand = mDNSRandom(0x3FFFFFFF) % 100;
    // Probe 5% of the time
    if (rand > 5)
        return;
    
    mDNS_DropLockBeforeCallback();
    InitializeQuestion(m, &DNSSECProbeQuestion, mDNSInterface_Any, (const domainname *)"\003com", kDNSType_DS, DNSSECProbeCallback, mDNSNULL);
    DNSSECProbeQuestion.ValidatingResponse = 0;
    DNSSECProbeQuestion.ValidationRequired = DNSSEC_VALIDATION_SECURE;

    BumpDNSSECStats(m, kStatsActionIncrement, kStatsTypeProbe, 1);
    mDNS_StartQuery(m, &DNSSECProbeQuestion);
    mDNS_ReclaimLockAfterCallback(); 
}
#endif // APPLE_OSX_mDNSResponder && !TARGET_OS_IPHONE

// For now we fetch the root trust anchor and update the local copy
mDNSexport void UpdateTrustAnchors(mDNS *const m)
{
    // Register for a notification to fire immediately which in turn will update
    // the trust anchor
    if (RegisterNotification(m, 1))
    {
        LogMsg("UpdateTrustAnchors: ERROR!! failed to register for notification");
    }
}

mDNSlocal int RegisterNotification(mDNS *const m, unsigned int interval)
{
    int len = strlen("com.apple.system.notify.service.timer:+") + 21; // 21 bytes to accomodate the interval
    char buffer[len];
    unsigned int blen;
    int status;

    // Starting "interval" second from now (+ below indicates relative) register for a notification
    blen = mDNS_snprintf(buffer, sizeof(buffer), "com.apple.system.notify.service.timer:+%us", interval);
    if (blen >= sizeof(buffer))
    {
        LogMsg("RegisterNotification: Buffer too small blen %d, buffer size %d", blen, sizeof(buffer));
        return -1;
    } 
    LogInfo("RegisterNotification: buffer %s", buffer);
    if (m->notifyToken)
    {
        notify_cancel(m->notifyToken);
        m->notifyToken = 0;
    }
    status = notify_register_dispatch(buffer, &m->notifyToken,
                dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
                ^(int t) { (void) t; FetchRootTA(m); });

    if (status != NOTIFY_STATUS_OK)
    {
        LogMsg("RegisterNotification: notify_register_dispatch failed");
        return -1;
    }
    return 0;
}

mDNSexport mStatus DNSSECPlatformInit(mDNS *const m)
{
    int diglen;

    m->TrustAnchors = mDNSNULL;
    m->notifyToken  = 0;

    // Add a couple of trust anchors for testing purposes.
    mDNSlocal const domainname *testZone  = (const domainname*)"\007example";

    char *digest = "F122E47B5B7D2B6A5CC0A21EADA11D96BB9CC927";
    mDNSu8 *dig = ConvertDigest(digest, 1, &diglen);
    AddTrustAnchor(m, testZone, 23044, 5, 1, diglen, dig);

    char *digest1 = "D795AE5E1AFB200C6139474199B70EAD3F3484553FD97BE5A43704B8A4791F21";
    dig = ConvertDigest(digest1, 2, &diglen);
    AddTrustAnchor(m, testZone, 23044, 5, 2, diglen, dig);

    // Add the TA for root zone manually here. We will dynamically fetch the root TA and
    // update it shortly. If that fails e.g., disconnected from the network, we still
    // have something to work with.
    char *digest2 = "49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5";
    dig = ConvertDigest(digest2, 2, &diglen);
    AddTrustAnchor(m, (const domainname *)"\000", 19036, 8, 2, diglen, dig);

#if !TARGET_OS_IPHONE
    DNSSECProbeQuestion.ThisQInterval = -1;
#endif
    return mStatus_NoError;
}
