Closed6

どこに「OpenPGP公開鍵サーバにおいて鍵や署名の追記しかできない」と規定されているか

ピン留めされたアイテム
zundazunda

https://text.baldanders.info/remark/2019/07/openpgp-certificate-flooding/

OpenPGP 公開鍵サーバにおいて鍵や署名の追記しかできない

https://www.gnupg.org/documentation/manuals/gnupg/OpenPGP-Key-Management.html

deluid - Note that it is not possible to retract a user id, once it has been send to the public (i.e. to a keyserver). In that case you better use revuid.

MIT PGP Serverの後継、OpenPGP Public Key Server (PKS) pks-0.9.6+cvsのソースコードkd_add.cllist.cをざっくり読んだところ、

  • 追加リクエストで得られた公開鍵パケットをできるだけ廃棄せず既存の公開鍵パケットにマージする
  • 同じユーザーIDに付随する署名パケットは最近のものを先にマージする

ようだ。

SpiegelSpiegel

伝統的な OpenPGP 鍵サーバについては以前にちょっとだけ調べたことがあるのですが Marc Horowitz という人が作った MIT PGP Server がオリジナルと言われています。 HKP (HTTP Keyserver Protocol) を備えている鍵サーバは大抵これのクローンか改良版です。 HKP 鍵サーバを RFC 化しようという動きもあったようですがドラフトのまま終わってるみたいです(なので明確な規格と言えるものはありません)。

https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00

上で紹介していただいた記事の最後でこっそり紹介しているのですが,新しい鍵サーバが提案されていて keys.openpgp.org として実装されています。こちらは鍵の持ち主であると証明できれば色々と操作できるっぽいです。また,メールソフト Thunderbird の最近のバージョンはこれを既定の鍵サーバとしているようです。

zundazunda

ありがとうございました!

MIT PGP Serverはバージョンの進んだものがOpenPGP Public Key Serverとして公開されていて、ざっくりソースコードを読んで公開鍵が追加された時の動作をなんとなく把握することができたように思います。

最近、公開鍵に添付する自己署名証明書のnotationを利用してオンラインでのアイデンティティを証明しようという https://keyoxide.org/ を試してみていて、公開鍵サーバにnotationを削除した公開鍵を追加した時の動作が直感とは異なっていたので調べてみた次第です。もうすこし理解を深めてから公開鍵サーバの動作を https://mitome.in/sns/keyoxide.html に追記します。

zundazunda

公開鍵サーバの実装から探してみる

https://softwarerecs.stackexchange.com/a/6905

hockeypuck/openpgp

https://code.launchpad.net/hockeypuck Goによる実装。規格への言及は見つけられなかったが、 https://github.com/hockeypuck/hockeypuck/tree/master/src/hockeypuck/openpgp にキーサーバー用の実装がありそうだ。

This package supports the unique concerns of a keyserver, which is not (and probably should not be) addressed in a typical OpenPGP implementation oriented toward user agents.

hockeypuck/pghkp/storage.goを眺めるとsubkeyやkeyの削除keyの更新やsubkeyの追加は実装されているようだ。このファイルにはsubpacketの操作は実装されていない。

OpenPGP Public Key Server (PKS)

http://pks.sourceforge.net/

http://pks.sourceforge.net/pks-current.tgz をダウンロードすると、configure.inAC_INITよりバージョンは0.9.6+cvsだった。

pks_help.jaファイルに下記のような記述が見られた。

各々の鍵サーバでの処理には所定の電子メールのメッセージ形式が必要です。サーバへの命令はSubject: 行に記述します。

(中略)

たとえば、あなたの鍵を鍵サーバへ加える、あるいは既に鍵サーバに登録している鍵を更新するためには以下のようなメッセージをいずれかの鍵サーバに送ります。

To: pgp-public-keys@keys.pgp.net
From: johndoe@some.site.edu
Subject: add

-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: 2.6

<blah blah blah>
-----END PGP PUBLIC KEY BLOCK-----

盗まれた鍵: 鍵廃棄証明書 (Key Revocation Certificate : PGPのドキュメントには、どのように行うかが書かれています)を作成し、ADDコマンドを使ってメールであなたの鍵をサーバにもう一度送ってください。

利用できるコマンド:

コマンド 結果
HELP このメッセージが送られます
HELP language 翻訳されたヘルプテキスト(DE, EN, ES, FI, FR, HR, NO, JA)
ADD 本文に含まれたPGP公開鍵を加えます。
INDEX userid ユーザIDを含む全てのPGP鍵リスト
VERBOSE INDEX userid ユーザIDを含む全てのPGP鍵冗長リスト
GET userid ユーザIDにマッチした鍵を得る
LAST days daysで指定した間に更新された鍵

kd_add.cに署名をマージする実装がありそうだ。全文を https://gist.github.com/zunda/ecda12811f27ebfe6701289d5c358571 にペーストした。

/* 前略 */

/* for all of these, the first element will come from the database,
   the second from the request */

typedef struct _merge_state
{
  /* statistics */
  int new_sigs;
  int repl_sigs;
  int new_userids;
  int changed_primary_userids;
  int new_revocations;
  int new_pubkeys;
  int not_changed_revocation_sig;
  /* added stuff */
  int save_add;
  llist add_keys;
  llist add_userids;
  llist add_sigs;
  xbuffer *add_xb;
  /* verbose printing state */
  int verbose;
  keys_elem *this_ke;
  unsigned char *this_userid;
  int this_userid_len;
}
merge_state;

/* 中略 */

/* this function has the potential problem that if there are two keys
   out there with the same keyid, if both of them have signatures,
   they are indistinguishable without access to the public keys.  Even
   then, the only way to tell is to attempt to verify the signature
   with each public key to see if that is, in fact, the correct public
   key.  And even then, you can't ever decide that a signature is
   invalid, because you could just be missing the public key.  This
   seems to be a fundamental flaw in the keyring packet formats. */

int
kd_sigs_elem_merge (llist * lout, void *e1, void *e2, void *c)
{
/* 中略 */
}

/* 中略 */

int
kd_userids_elem_merge (llist * lout, void *e1, void *e2, void *c)
{
/* 中略 */

  o = sigs_elem_order (e1, e2);

  /* if the sig keyids are different, move the smaller keyid
     to the output list */

/* 中略 */

  /* if the sig keyids are the same, move the more recent signature
     to the output list and delete the less recent.  If they are
     the same, take the database sig so the stats work out right */

  if (se1->sig_time >= se2->sig_time)
    {
      if (!llist_add (lout, e1))
        {
          dabort ();
          return (LLIST_MERGE_FAIL);
        }
      sigs_elem_free (se2, NULL);
    }
  else
    {
/* 中略 */

}

/* 後略 */

kd_sigs_elem_merge (llist * lout, void *e1, void *e2, void *c)

sigs_elem_order()e1e2keyidを比較する。keyidが異なる場合には小さい方を出力リストloutに移動する。e2の方が小さければ、署名も出力に追加する。keyidが同じ場合には、最近のものを出力リストに追加し、e2の方が最近ならば、署名も出力に追加する。keyidが同じ場合はe1e2の双方を入力リストから削除する。

kd_userids_elem_merge (llist * lout, void *e1, void *e2, void *c)

userids_elem_order()e1e2useridを比較する。useridが異なる場合には小さい方を出力リストloutに移動する。e2の方が小さければ、署名も出力に追加する。useridが同じ場合にはkd_sigs_elem_mergeで署名をマージする。

kd_userids_elem_merge()じたいはllist.cに実装されているllist_merge()を通して呼ばれる。llist_merge()は2つのリンクリストを、与えられた関数で最初の要素どうしを比較させ必要なら出力リストに追加させ、与えられた関数の返り値によって2つのリンクリストの最初の要素のうち削除の必要なものを削除する。この動作は両方のリンクリストが空になるまで繰り返される。

この関数の外ではkd_keys_elem_merge()db_key_merge_1()db_key_merge()kd_add_1()kd_add()と呼び出しが起きる。

PGP Public Key Server

Spiegelさんに教えていただいた、Marc HorowitzさんによるMIT PGP Server。ページの最終更新は2005年5月でコードのバージョンは0.9.4。

上記のOpenPGP Public Key Serverに続くものだろうか?例えばkd_add.cにはint kd_sigs_elem_merge(llist *lout, void *e1, void *e2, void *c)の定義があり上記と同じコメントが付いている。OpenPGP Public Key ServerがMIT PGP Serverの後継と思って間違いないだろう。

zundazunda

どのsignature packetが有効になるのか再確認する

Keyoxideの実装を追ったところ、鍵サーバから得られた自己署名証明書のうち一番最初のものを有効なものとしているようだ。

https://codeberg.org/keyoxide/web/src/tag/2.5.0/static/scripts.js#L390

            verifications = await doip.claims.verify(keyData, fingerprint, doipOpts)

keyDataに含まれるnotationを検証しているように見える。verifyメソッド内で呼んでいるkeys.getUserDataの返り値には有効なnotationのみが含まれ、無効なパケットは見られない。getUserDataが返却するのはconst keyData = await process(publicKey)よりkeyData.users

keys.jsprocessの実装

const process = (publicKey) => {
  return new Promise(async (resolve, reject) => {
    if (!publicKey) {
      reject('Invalid public key')
    }
    const fingerprint = await publicKey.primaryKey.getFingerprint()
    const primaryUser = await publicKey.getPrimaryUser()
    const users = publicKey.users
    let primaryUserIndex,
      usersOutput = []

    users.forEach((user, i) => {
      usersOutput[i] = {
        userData: {
          id: user.userId ? user.userId.userid : null,
          name: user.userId ? user.userId.name : null,
          email: user.userId ? user.userId.email : null,
          comment: user.userId ? user.userId.comment : null,
          isPrimary: primaryUser.index === i,
        },
      }

      if ('selfCertifications' in user && user.selfCertifications.length > 0) {
        const notations = user.selfCertifications[0].rawNotations 
        // このuser.selfCertificationsには無効になったものも含まれるが、そのうち最初のもののみを取り上げている
        usersOutput[i].notations = notations.map(
          ({ name, value, humanReadable }) => {
            if (humanReadable && name === 'proof@metacode.biz') {
              return openpgp.util.decode_utf8(value)
            }
          }
        )
      } else {
        usersOutput[i].notations = []
      }
    })

    resolve({
      fingerprint: fingerprint,
      users: usersOutput,
      primaryUserIndex: primaryUser.index,
    })
  })
}

openpgp.jsのコードに続く。selfCertificationsにパケットをpushする時点でパケットがcreatedの逆順に並んでいる。

  for (let i = 0; i < packetlist.length; i++) {
    switch (packetlist[i].tag) {
  :
      case enums.packet.signature:
        switch (packetlist[i].signatureType) {
          case enums.signature.cert_generic:
          case enums.signature.cert_persona:
          case enums.signature.cert_casual:
          case enums.signature.cert_positive:
            if (!user) {
              util.print_debug('Dropping certification signatures without preceding user packet');
              continue;
            }
            if (packetlist[i].issuerKeyId.equals(primaryKeyId)) {
              user.selfCertifications.push(packetlist[i]);
            } else {
              user.otherCertifications.push(packetlist[i]);
            }
            break;
  :

console.trace()すると、得られたarmored鍵をそのまま読んでいる。自己署名証明書を日付の逆順に並べているのはキーサーバーのようだ。

このスクラップは2021/03/24にクローズされました