Node.js CAAレコード取得の代替方法:dns.resolveCaa()と外部API

2025-05-16

CAA レコードとは?

CAA レコードは、ドメイン名に対してどの認証局 (CA) が証明書を発行できるかを指定するための DNS レコードです。これにより、意図しない認証局による証明書の発行を防ぎ、セキュリティを向上させることができます。

dnsPromises.resolveCaa(hostname) の詳細

  • 戻り値: この関数は Promise を返します。
    • 成功時 (resolve): CAA レコードが見つかった場合、それらのレコードを含む配列で Promise が解決されます。各要素は、CAA レコードの情報をオブジェクトとして含んでいます。このオブジェクトは通常、以下のプロパティを持ちます。
      • critical: 真偽値で、この CAA レコードがクリティカルかどうかを示します。クリティカルなレコードが存在する場合、認証局は指定されたポリシーに従う必要があります。
      • tag: CAA レコードの「タグ」部分を表す文字列 (例: 'issue', 'issuewild', 'iodef')。
      • value: CAA レコードの「値」部分を表す文字列 (例: 'letsencrypt.org', '0xE04B9AA9B3A6')。
    • 失敗時 (reject): DNS の問い合わせに失敗した場合や、指定されたホスト名に CAA レコードが存在しない場合、エラーとともに Promise が拒否されます。一般的なエラーとしては、dns.NODATA (レコードが見つからない) や dns.TIMEOUT (タイムアウト) などがあります。
  • 引数 hostname: CAA レコードを問い合わせたいドメイン名 (例: 'example.com') を文字列で指定します。
  • resolveCaa(hostname): この関数は、引数として与えられた hostname (ホスト名) に関連する CAA レコードを DNS サーバーに問い合わせます。

使用例

const dns = require('node:dns').promises;

async function getCaaRecords(hostname) {
  try {
    const caaRecords = await dns.resolveCaa(hostname);
    console.log(`CAA レコード (${hostname}):`, caaRecords);
  } catch (err) {
    if (err.code === 'ENOTFOUND') {
      console.error(`ホスト名 '${hostname}' が見つかりませんでした。`);
    } else if (err.code === 'NODATA') {
      console.log(`'${hostname}' に CAA レコードは存在しません。`);
    } else {
      console.error('CAA レコードの解決中にエラーが発生しました:', err);
    }
  }
}

getCaaRecords('example.com');
getCaaRecords('invalid-hostname-example.com');
getCaaRecords('cloudflare.com'); // CAA レコードが存在する可能性が高い

この例では、getCaaRecords という非同期関数を定義し、Promise ベースの dns.resolveCaa() を使用して指定されたホスト名の CAA レコードを取得しています。取得に成功した場合はレコードの内容を表示し、失敗した場合はエラーの種類に応じて適切なメッセージを表示しています。



Error: getaddrinfo ENOTFOUND <hostname>

  • トラブルシューティング
    • ホスト名が正しいかどうか再度確認してください。
    • ネットワーク接続を確認してください。他のウェブサイトにアクセスできるかなどを試すと良いでしょう。
    • 使用している DNS サーバーの設定を確認してください。必要であれば、別の DNS サーバー (例: Google Public DNS (8.8.8.8, 8.8.4.4) や Cloudflare DNS (1.1.1.1)) を試してみてください。
    • 他の DNS 解決ツール (例: nslookupdig コマンド) を使用して、同じホスト名が解決できるか確認してみてください。もし他のツールでも解決できない場合は、ホスト名自体に問題がある可能性が高いです。
  • 原因
    • 指定したホスト名が間違っている。スペルミスや大文字・小文字の違いなどが考えられます。
    • ネットワーク接続に問題がある。インターネットに接続されていない、または DNS サーバーにアクセスできない可能性があります。
    • DNS サーバーの設定に誤りがある。使用している DNS サーバーが正しく設定されていない場合があります。
  • エラー内容
    指定されたホスト名が見つからない場合に発生します。DNS サーバーがホスト名を IP アドレスに解決できなかったことを意味します。

Error: queryA ENODATA <hostname> (または類似の ENODATA エラー)

  • トラブルシューティング
    • 他の DNS 検索ツール (例: dig <hostname> CAA) を使用して、実際に CAA レコードが存在するかどうかを確認してください。もし他のツールでも CAA レコードが見つからない場合は、そのドメインに CAA レコードは設定されていません。
    • 時間をおいて再度試してみてください。一時的な DNS サーバーの問題であれば、時間が経つと解決することがあります。
  • 原因
    • 指定されたドメインに CAA レコードが設定されていない。多くのドメインでは CAA レコードが設定されていない場合があります。
    • DNS サーバーが CAA レコードの情報を保持していない。一時的な DNS サーバーの問題である可能性も考えられます。
  • エラー内容
    指定されたホスト名に対して、要求された DNS レコード (この場合は CAA レコード) が存在しない場合に発生します。

Error: queryCaa ETIMEOUT <hostname>

  • トラブルシューティング
    • ネットワーク接続を確認してください。
    • 別の DNS サーバーを試してみてください。
    • ファイアウォールの設定を確認し、DNS クエリがブロックされていないか確認してください。
    • 他の DNS 解決ツールで同様にタイムアウトが発生するか確認してみてください。もし他のツールでもタイムアウトする場合は、ネットワークまたは DNS サーバー側の問題である可能性が高いです。
  • 原因
    • ネットワーク接続が不安定である。
    • DNS サーバーの応答が遅い、またはダウンしている。
    • ファイアウォールが DNS クエリ (通常は UDP/TCP ポート 53) をブロックしている。
  • エラー内容
    DNS サーバーへの問い合わせがタイムアウトした場合に発生します。
  • トラブルシューティング
    • 時間をおいて再度試してみてください。
    • 別の DNS サーバーを試してみてください。
    • ネットワーク接続を確認してください。
  • 原因
    • DNS サーバーに一時的な問題が発生している。
    • DNS サーバーの設定に問題がある。
    • ネットワーク経路上で何らかの問題が発生している。
  • エラー内容
    DNS サーバーがクエリに応答できなかったり、拒否したりした場合に発生します。
  • Promise の処理を適切に行う
    async/await.then().catch() を使用して、Promise の成功と失敗の両方を適切に処理するようにしてください。エラーが発生した場合に適切にキャッチし、ログ出力やエラーハンドリングを行うことで、問題の特定が容易になります。
  • Node.js のバージョンを確認する
    古い Node.js のバージョンでは、DNS 関連の処理に既知の問題が存在する可能性があります。最新の安定版にアップデートすることを検討してください。
  • ネットワーク環境を確認する
    ファイアウォールやルーターの設定、プロキシの使用状況などが DNS の解決に影響を与える場合があります。
  • 他の DNS ツールを試す
    nslookupdig などのコマンドラインツールを使用して、Node.js アプリケーションとは独立して DNS の問い合わせを試すことで、問題の切り分けができます。
  • エラーメッセージをよく読む
    エラーメッセージには、問題の原因の手がかりとなる情報が含まれていることが多いです。


例1: 基本的な CAA レコードの取得と表示

この例では、指定されたホスト名の CAA レコードを取得し、成功した場合はその内容をコンソールに表示します。

const dns = require('node:dns').promises;

async function getAndDisplayCaaRecords(hostname) {
  try {
    const caaRecords = await dns.resolveCaa(hostname);
    if (caaRecords.length > 0) {
      console.log(`ホスト名 '${hostname}' の CAA レコード:`);
      caaRecords.forEach((record, index) => {
        console.log(`  [${index + 1}] critical: ${record.critical}, tag: '${record.tag}', value: '${record.value}'`);
      });
    } else {
      console.log(`ホスト名 '${hostname}' に CAA レコードは存在しません。`);
    }
  } catch (err) {
    console.error(`CAA レコードの取得中にエラーが発生しました: ${err.message}`);
  }
}

// CAA レコードが存在する可能性のあるホスト名
getAndDisplayCaaRecords('example.com');
getAndDisplayCaaRecords('cloudflare.com');
// CAA レコードが存在しない可能性のあるホスト名
getAndDisplayCaaRecords('nonexistent-caa.example');

このコードでは、async/await を使用して非同期処理を簡潔に記述しています。dns.resolveCaa() が成功すると、CAA レコードの配列が caaRecords に格納され、内容がコンソールに出力されます。もし CAA レコードが存在しない場合は、その旨のメッセージが表示されます。エラーが発生した場合は、エラーメッセージがコンソールに出力されます。

例2: 特定の認証局 (CA) の発行許可を確認する関数

この例では、指定されたホスト名に対して、特定の認証局 (例: Let's Encrypt) が証明書を発行できるかどうかを確認する関数を作成します。

const dns = require('node:dns').promises;

async function canIssueCertificate(hostname, caDomain) {
  try {
    const caaRecords = await dns.resolveCaa(hostname);
    const isAllowed = caaRecords.some(
      (record) => record.tag === 'issue' && record.value === caDomain
    );
    if (isAllowed) {
      console.log(`ホスト名 '${hostname}' は '${caDomain}' による証明書発行を許可しています。`);
    } else {
      console.log(`ホスト名 '${hostname}' は '${caDomain}' による証明書発行を許可していません (または CAA レコードが存在しません)。`);
    }
  } catch (err) {
    if (err.code === 'ENOTFOUND') {
      console.error(`ホスト名 '${hostname}' が見つかりませんでした。`);
    } else if (err.code === 'NODATA') {
      console.log(`'${hostname}' に CAA レコードは存在しません。`);
    } else {
      console.error(`CAA レコードの確認中にエラーが発生しました: ${err.message}`);
    }
  }
}

canIssueCertificate('example.com', 'letsencrypt.org');
canIssueCertificate('cloudflare.com', 'letsencrypt.org');
canIssueCertificate('example.com', 'globalsign.com');

このコードでは、取得した CAA レコードの配列に対して some() メソッドを使用し、tag'issue' であり、value が指定された caDomain と一致するレコードが存在するかどうかを確認しています。

例3: issuewild タグの CAA レコードの確認

issuewild タグは、ワイルドカード証明書の発行を許可する認証局を指定するために使用されます。この例では、指定されたホスト名に issuewild タグの CAA レコードが存在するかどうかを確認します。

const dns = require('node:dns').promises;

async function canIssueWildcardCertificate(hostname, caDomain) {
  try {
    const caaRecords = await dns.resolveCaa(hostname);
    const isAllowed = caaRecords.some(
      (record) => record.tag === 'issuewild' && record.value === caDomain
    );
    if (isAllowed) {
      console.log(`ホスト名 '${hostname}' は '${caDomain}' によるワイルドカード証明書の発行を許可しています。`);
    } else {
      console.log(`ホスト名 '${hostname}' は '${caDomain}' によるワイルドカード証明書の発行を許可していません (または 'issuewild' レコードが存在しません)。`);
    }
  } catch (err) {
    if (err.code === 'ENOTFOUND') {
      console.error(`ホスト名 '${hostname}' が見つかりませんでした。`);
    } else if (err.code === 'NODATA') {
      console.log(`'${hostname}' に CAA レコードは存在しません。`);
    } else {
      console.error(`'issuewild' レコードの確認中にエラーが発生しました: ${err.message}`);
    }
  }
}

canIssueWildcardCertificate('example.com', 'letsencrypt.org');
canIssueWildcardCertificate('cloudflare.com', 'digicert.com');

この例は、issue タグの確認とほぼ同じですが、tag の比較対象が 'issuewild' になっている点が異なります。

例4: クリティカルな CAA レコードの処理

CAA レコードには critical フラグがあり、これが true に設定されている場合、認証局はそのレコードを理解し、従う必要があります。この例では、取得した CAA レコードの中にクリティカルなレコードが存在するかどうかを確認します。

const dns = require('node:dns').promises;

async function hasCriticalCaaRecord(hostname) {
  try {
    const caaRecords = await dns.resolveCaa(hostname);
    const hasCritical = caaRecords.some((record) => record.critical);
    if (hasCritical) {
      console.log(`ホスト名 '${hostname}' にクリティカルな CAA レコードが存在します。`);
    } else {
      console.log(`ホスト名 '${hostname}' にクリティカルな CAA レコードは存在しません。`);
    }
  } catch (err) {
    if (err.code === 'ENOTFOUND') {
      console.error(`ホスト名 '${hostname}' が見つかりませんでした。`);
    } else if (err.code === 'NODATA') {
      console.log(`'${hostname}' に CAA レコードは存在しません。`);
    } else {
      console.error(`クリティカルな CAA レコードの確認中にエラーが発生しました: ${err.message}`);
    }
  }
}

hasCriticalCaaRecord('example.com');
hasCriticalCaaRecord('cloudflare.com');


コールバックベースの dns.resolveCaa() の使用

dns モジュールには、Promise ベースの API (dns.promises) と並行して、伝統的なコールバックベースの API も提供されています。Promise を使用しない場合は、こちらの API を利用できます。

const dns = require('node:dns');

dns.resolveCaa('example.com', (err, records) => {
  if (err) {
    if (err.code === 'ENOTFOUND') {
      console.error('ホスト名が見つかりませんでした。');
    } else if (err.code === 'NODATA') {
      console.log('CAA レコードは存在しません。');
    } else {
      console.error('CAA レコードの解決中にエラーが発生しました:', err);
    }
    return;
  }

  if (records && records.length > 0) {
    console.log('CAA レコード:', records);
    records.forEach((record, index) => {
      console.log(`  [${index + 1}] critical: ${record.critical}, tag: '${record.tag}', value: '${record.value}'`);
    });
  } else {
    console.log('CAA レコードは見つかりませんでした。');
  }
});

この例では、dns.resolveCaa() の第二引数に関数 (コールバック) を渡しています。DNS クエリの結果は、このコールバック関数の引数 err (エラーが発生した場合) と records (CAA レコードの配列) を通じて返されます。コールバックベースの API は、Promise を使用しない従来の非同期処理パターンに従っています。

util.promisify() を使用してコールバックベースの API を Promise 化する

Node.js の util モジュールにある promisify() 関数を使用すると、コールバックベースの関数を Promise を返す関数に変換できます。これにより、dns.resolveCaa() を Promise ベースのコード内でより自然に扱うことができます。

const dns = require('node:dns');
const util = require('node:util');

const resolveCaaPromise = util.promisify(dns.resolveCaa);

async function getCaaRecordsWithPromisify(hostname) {
  try {
    const caaRecords = await resolveCaaPromise(hostname);
    console.log(`CAA レコード (${hostname}):`, caaRecords);
  } catch (err) {
    if (err.code === 'ENOTFOUND') {
      console.error(`ホスト名 '${hostname}' が見つかりませんでした。`);
    } else if (err.code === 'NODATA') {
      console.log(`'${hostname}' に CAA レコードは存在しません。`);
    } else {
      console.error('CAA レコードの解決中にエラーが発生しました:', err);
    }
  }
}

getCaaRecordsWithPromisify('example.com');

この例では、util.promisify(dns.resolveCaa) によって resolveCaa 関数が Promise を返す resolveCaaPromise に変換され、async/await を使用して結果を処理しています。

サードパーティの DNS クライアントライブラリの使用

Node.js の標準の dns モジュール以外にも、より高度な機能や異なる API を提供するサードパーティの DNS クライアントライブラリが存在します。これらのライブラリを使用すると、CAA レコードの取得を含む DNS クエリをより柔軟に行うことができます。

  • node-dns: より機能豊富な DNS クライアントライブラリで、DNS サーバーの実装や DNSSEC の検証などもサポートしています。コールバックベースおよび Promise ベースの API を提供しています。
  • dns2: シンプルで軽量な非同期 DNS クライアントライブラリです。Promise ベースの API を提供しており、様々な DNS レコードタイプに対応しています。

これらのライブラリを使用するには、まず npm や yarn などのパッケージマネージャーでインストールする必要があります。

例 (dns2 を使用する場合)

const dns = require('dns2').promises;

async function getCaaRecordsWithDns2(hostname) {
  try {
    const response = await dns.resolve(hostname, 'CAA');
    if (response.answers && response.answers.length > 0) {
      const caaRecords = response.answers.map(answer => ({
        critical: answer.flags.critical,
        tag: answer.data.tag,
        value: answer.data.value,
      }));
      console.log(`CAA レコード (${hostname}):`, caaRecords);
    } else {
      console.log(`'${hostname}' に CAA レコードは存在しません。`);
    }
  } catch (err) {
    console.error('CAA レコードの解決中にエラーが発生しました:', err);
  }
}

getCaaRecordsWithDns2('example.com');

この例では、dns2 ライブラリの resolve() 関数を使用して CAA レコードを問い合わせています。レスポンスの形式は標準の dns モジュールとは異なるため、取得したデータを適切な形式に変換する必要があります。

外部の DNS 解決サービスの API の利用

DNS の解決を自社のサーバーではなく、Google DNS Public API や Cloudflare API などの外部サービスに委託する方法もあります。これらのサービスは、HTTP ベースの API を提供しており、CAA レコードを含む様々な DNS レコードを JSON 形式などで取得できます。

例 (Google Public DNS API を使用する場合)

const https = require('node:https');

async function getCaaRecordsWithGoogleDns(hostname) {
  return new Promise((resolve, reject) => {
    https.get(`https://dns.google/resolve?name=${hostname}&type=CAA`, (res) => {
      let data = '';
      res.on('data', (chunk) => {
        data += chunk;
      });
      res.on('end', () => {
        try {
          const result = JSON.parse(data);
          if (result.Answer) {
            const caaRecords = result.Answer.map(record => ({
              critical: !!(record.data.split(' ')[0] === '1'),
              tag: record.data.split(' ')[1].replace(/"/g, ''),
              value: record.data.split(' ')[2].replace(/"/g, ''),
            }));
            console.log(`CAA レコード (${hostname}):`, caaRecords);
            resolve(caaRecords);
          } else {
            console.log(`'${hostname}' に CAA レコードは存在しません。`);
            resolve([]);
          }
        } catch (error) {
          reject(error);
        }
      });
    }).on('error', (err) => {
      reject(err);
    });
  });
}

getCaaRecordsWithGoogleDns('example.com');

この例では、Node.js の https モジュールを使用して Google Public DNS API にリクエストを送信し、そのレスポンスを解析して CAA レコードを取得しています。