s1r-Jの技術ブログ

とあるSEの技術ブログ

FIDO Metadata serviceのMetadata Statementのプロパティを勉強する

タイトルの通り、FIDO Metadata serviceのMetadata Statementのプロパティ(Metadata EntryのmetadataStatementプロパティの中身)を勉強する。 この記事は前の記事の続き、というか飛ばした部分について勉強した。

s1r-j.hatenablog.com

Metadata EntryのmetadataStatementプロパティ以外について知りたい場合は前の記事を確認してほしい。

おさらい

以前の記事で飛ばした部分、Metadata EntryのmetadataStatementプロパティの中身についてまとめていく。

おさらいとして、前の記事ではmetadataStatementプロパティについて以下のように記載した。


  • metadataStatement:非必須。認証器に関する情報。
    • 認証器に関する細かい情報が入っているようだ。後述のstatusReportsよりも細かい情報が欲しければこちらを使うのだろうか。非必須であるが、現時点で提供されている認証器データにはすべて含まれていた。
    • この項目は長いので、次回以降まとめる。
    • 参考:Metadata Statement - FIDO Metadata Statement

前回の振り返りおわり。

metadataStatementはMDS バージョン2のEntryにはなかった項目であり、バージョン3からより詳細な認証器情報が提供されるようになった。 参考として記載した定義を和訳しつつ、実データなどを交えて勉強していく。

Metadata Entryについて

MetadataEntryについての公式情報は以下のリンク先を参照されたし。

Metadata Statement

metadataStatementに含まれるプロパティを一気に紹介していく。

  • legalHeader:MDSを利用するための法的な合意事項。
    • 例としてhttps://fidoalliance.org/metadata/metadata-statement-legal-header/というURLが記載されているように、ここに文言を入れる必要はないようだ。下記の実際のFIDO2認証器のメタデータでもURLが入っている。
  • aaguid, aaid, attestationCertificateKeyIdentifiers:認証器のモデルを特定するための識別子。Entryにも存在する。
    • FIDO2の認証器は aaguidを持つ、FIDO UAFの認証器はaaidを持つ、FIDO U2Fの認証器はattestationCertificateKeyIdentifiersを持つ。なので、上記のいずれかのプロパティに値が入っている
  • description:必須。人間が読むことができる認証器の端的な説明。英語で記載する。
  • alternativeDescriptiondescriptionプロパティと似た内容だが、英語以外の言語で記載する。
    • {"ru-RU": "Пример UAF аутентификатора от FIDO Alliance", "fr-FR": "Exemple UAF authenticator de FIDO Alliance"}のように言語コードをキー、認証器の説明をバリューとする。
  • authenticatorVersion:必須。認証器のバージョン。符号なしの整数。
    • メタデータで指定された要件を満たす、最も古い(すなわち最も低い)信頼できるバージョン番号が入る。認証器のステータス(EntryのstatusReportsプロパティ参照)で認証器が信頼できない状態になってから修正されたらバージョンを変更する。
    • ここに記載されているバージョン番号のほうが利用している認証器のバージョン番号よりも大きい場合、リスクが高くなっていると考えられるので注意。
  • protocolFamily:必須。認証器がサポートしているFIDOプロトコル
    • uafu2ffido2のいずれか。
  • schema:必須。現在のMetadata Statementがどのメタデータスキーマのバージョンのものかを示す。MDSバージョン3なので値は3になる。
  • upv:必須。upvとはFIDOユニファイドプロトコルバージョン(おそらく認証器とクライアントの接続プロトコル)の略称。認証器がどのバージョンをサポートしているか示す配列。FIDO UAF、FIDO U2F、FIDO2で取る値が若干異なる。
    • FIDO UAFの場合、FIDO UAF Protocol SpecificationのOperationHeaderのupvに準拠するようにと書かれている。つまり、majorが1でminorは2ということだが、FIDO UAFの例とは異なっている。
    • FIDO U2Fの場合、majorが1でminorが0ならU2F v1.0を示す。同様にv1.1、v1.2(=CTAP1)が取りうる値。
    • FIDO2の場合、majorが2でminorが0ならCTAP2.0を示し、majorが2でminorが1ならCTAP2.1を示す。
  • authenticaionAlgorithms:必須(空配列も禁止)。認証器がサポートしている認証器アルゴリズムのリスト。
  • publicKeyAlgAndEncodings:必須(空配列も禁止)。認証器登録処理において認証器がサポートする公開鍵フォーマットのリスト。複数の値を取る場合には好ましいものをより先頭に並べる。
    • FIDO RegistryのPublic Key Representation Formatsで定義されているフォーマット名で記載する。
    • FIDO2ならば値としてはcoseが基本的に入っているはず?
  • attestationTypes:必須。認証器がどのアテステーションタイプに対応したアテステーションを生成するのかを示したリスト。
    • FIDO RegistryのAuthenticator Attestation Typesに定義された名称で記載する。
    • FIDO2のセルフコンフォーマンステストでもテスト項目になっている。このリストに無いアテステーションタイプのアテステーションだった場合には認証器登録を拒否しなければならない。
  • userVerificationDetails:必須。ユーザ認証(User Verification)方式を示す2重の配列。
    • 外側の配列の要素同士はOR関係で、内側の配列の要素同士はAND関係になっている。
    • 下の例だと、(1) 指紋認証 または(2) パスコード認証 または(3) 顔認証かつ声帯認証 が利用できるユーザ認証方式となる
[
    [
      { "userVerificationMethod": "fingerprint_internal" }
    ],
    [
      { "userVerificationMethod": "passcode_internal" }
    ],
    [
      { "userVerificationMethod": "faceprint_internal"},
      { "userVerificationMethod": "voiceprint_internal"}
    ]
  ]
  • keyProtection:必須(空配列も禁止)。認証器がサポートしているキープロテクションタイプ(作成した秘密鍵の保管、守り方)のリスト。
  • isKeyRestricted秘密鍵の利用用途が認証器によってFIDOのアサーションへの証明だけに制限されているかを示す。
  • trueの場合またはこの項目が存在しない場合、FIDOのアサーションへの証明だけに制限。falseの場合は制限されておらず、アプリケーションからFIDO以外での用途に利用される可能性がある。
  • isFreshUserVerificationRequired秘密鍵を利用する場合、必ず新鮮なユーザ認証(過去に実施されてキャッシュされているユーザ認証ではなく)を必要とするかを示す。
    • trueの場合またはこの項目が存在しない場合、必ず新鮮なユーザ認証が必要。falseの場合はキャッシュしたユーザ認証を利用する。
    • falseの場合、ユーザの関与がなくなるため、ユーザ同意の実施は呼び出し元のアプリの責任となる。
  • matcherProtection:空配列は禁止。認証器がサポートしているユーザ認証方式(matcher)の保護方式のリスト。
  • FIDO RegistryのMatcher Protection Typesで定義されている名称で記載する。
  • cryptoStrength:認証器の暗号強度の値。値が存在しない場合、暗号強度が不明ということを示す。
  • attachmentHint:空配列は禁止。認証器がサポートしている認証器とクライアントとの接続方法のヒントのリスト。
    • FIDO RegistryのAuthenticator Attachment Hintsで定義されている名称で記載する。
  • tcDisplay:認証器がサポートしているトランザクションコンファーメーション表示のリスト。認証器がトランザクションコンファーメーションをサポートしていない場合、空配列にしなければならない。
  • tcDisplayContentTypeトランザクションコンファーメーション表示に利用されるMIMEタイプ。
    • 認証器がトランザクションコンファーメーションをサポートしている場合(前述の tcDisplayプロパティの配列が空ではない場合)、このプロパティは存在しなければならない。
  • tcDisplayPNGCharacteristicsトランザクションコンファーメーション表示に利用するPNG画像の特徴(縦横の長さ、色など)のリスト。配列に複数要素がある場合は、代替となる画像の特徴となっている。
    • 認証器がトランザクションコンファーメーションをサポートしており(前述の tcDisplayプロパティの配列が空ではない場合)、かつ前述のtcDisplayContentTypeの値がimage/pngの場合、このプロパティは存在しなければならない。
  • attestationRootCertificates:アテステーションのルート証明書(X509形式、Base64エンコーディング)、つまりトラストアンカー。複数存在する場合はルート証明書が複数存在しており、同じモデルの認証器でも異なるルート証明書に基づいてることがあるということ。証明書チェーンではない。
    • FIDO2のセルフコンフォーマンステストで利用したことがある。利用しなくてもテストは通過する。
  • ecdaaTrustAnchors:ECDAAアテステーションに対して利用されるトラストアンカーのリスト。
    • このプロパティはUAF認証器だけが利用する。FIDO2だとECDAAは使っていない。
  • icon:認証器のアイコン画像。
  • supportedExtensions:サポートしている認証器拡張機能のリスト。
    • このプロパティはUAF認証器だけが利用する。FIDO2なら次のauthenticatorGetInfoを参照する。
  • authenticatorGetInfo:サポートしているバージョン(U2F、FIDO2、FIDO2.1Pre?)、拡張機能、AAGUIDやその機能(対応アルゴリズム等)のデータが入っている。認証器の外見なども含まれており、metadata statementと同じような内容が含まれている。

以上。 FIDOの認定試験の最初のステップであるセルフコンフォーマンステストで使った attestationTypes、一応利用したattestationRootCertificatesあたりがWebでFIDO認証を行なう上では最も重要だと思う。

それ以外の項目については、一定レベルのセキュリティや機能が存在する認証器以外は利用させないなどの用途に用いるのだろうか。

おわりに

前回から半年以上あけてしまい、もはや何のための記事なのかわからないが勉強してよかった。

この半年の間には、FIDO2サーバのためのNode.jsのモジュールを作成してセルフコンフォーマンステストが通ることを確認したりと勉強はしてきた。モジュールはnpmで公開している。

拡張機能の実装がまだまだできてなかったり、穴が多いモジュールなので今後も勉強して改善していきたい。

Appendix

FIDO2認証器のMetadata Entry

Yubico Bio Series

       {
            "aaguid": "d8522d9f-575b-4866-88a9-ba99fa02f35b",
            "metadataStatement": {
                "legalHeader": "https://fidoalliance.org/metadata/metadata-statement-legal-header/",
                "aaguid": "d8522d9f-575b-4866-88a9-ba99fa02f35b",
                "description": "YubiKey Bio Series",
                "authenticatorVersion": 328965,
                "protocolFamily": "fido2",
                "schema": 3,
                "upv": [
                    {
                        "major": 1,
                        "minor": 0
                    },
                    {
                        "major": 1,
                        "minor": 1
                    }
                ],
                "authenticationAlgorithms": [
                    "secp256r1_ecdsa_sha256_raw",
                    "ed25519_eddsa_sha512_raw"
                ],
                "publicKeyAlgAndEncodings": [
                    "cose"
                ],
                "attestationTypes": [
                    "basic_full"
                ],
                "userVerificationDetails": [
                    [
                        {
                            "userVerificationMethod": "none"
                        }
                    ],
                    [
                        {
                            "userVerificationMethod": "presence_internal"
                        },
                        {
                            "userVerificationMethod": "fingerprint_internal",
                            "baDesc": {
                                "selfAttestedFRR": 0,
                                "selfAttestedFAR": 0,
                                "maxTemplates": 5,
                                "maxRetries": 5,
                                "blockSlowdown": 0
                            }
                        }
                    ],
                    [
                        {
                            "userVerificationMethod": "passcode_external",
                            "caDesc": {
                                "base": 64,
                                "minLength": 4,
                                "maxRetries": 8,
                                "blockSlowdown": 0
                            }
                        },
                        {
                            "userVerificationMethod": "presence_internal"
                        }
                    ],
                    [
                        {
                            "userVerificationMethod": "presence_internal"
                        }
                    ],
                    [
                        {
                            "userVerificationMethod": "passcode_external",
                            "caDesc": {
                                "base": 64,
                                "minLength": 4,
                                "maxRetries": 8,
                                "blockSlowdown": 0
                            }
                        }
                    ],
                    [
                        {
                            "userVerificationMethod": "fingerprint_internal",
                            "baDesc": {
                                "selfAttestedFRR": 0,
                                "selfAttestedFAR": 0,
                                "maxTemplates": 5,
                                "maxRetries": 5,
                                "blockSlowdown": 0
                            }
                        }
                    ]
                ],
                "keyProtection": [
                    "hardware",
                    "secure_element"
                ],
                "matcherProtection": [
                    "on_chip"
                ],
                "cryptoStrength": 128,
                "attachmentHint": [
                    "external",
                    "wired"
                ],
                "tcDisplay": [],
                "attestationRootCertificates": [
                    "MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbwnebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXwLvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJhjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kthX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2kLVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1UsG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqcU9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw=="
                ],
                "icon": "data:image/png;base64,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",
                "authenticatorGetInfo": {
                    "versions": [
                        "FIDO_2_0",
                        "FIDO_2_1_PRE",
                        "FIDO_2_1"
                    ],
                    "extensions": [
                        "credProtect",
                        "hmac-secret",
                        "largeBlobKey",
                        "credBlob",
                        "minPinLength"
                    ],
                    "aaguid": "d8522d9f575b486688a9ba99fa02f35b",
                    "options": {
                        "plat": false,
                        "rk": true,
                        "clientPin": false,
                        "up": true,
                        "uv": false,
                        "pinUvAuthToken": true,
                        "largeBlobs": true,
                        "bioEnroll": false,
                        "userVerificationMgmtPreview": false,
                        "authnrCfg": true,
                        "credMgmt": true,
                        "credentialMgmtPreview": true,
                        "setMinPINLength": true,
                        "makeCredUvNotRqd": false,
                        "alwaysUv": true
                    },
                    "maxMsgSize": 1200,
                    "pinUvAuthProtocols": [
                        2,
                        1
                    ],
                    "maxCredentialCountInList": 8,
                    "maxCredentialIdLength": 128,
                    "transports": [
                        "usb"
                    ],
                    "algorithms": [
                        {
                            "type": "public-key",
                            "alg": -7
                        },
                        {
                            "type": "public-key",
                            "alg": -8
                        }
                    ],
                    "maxSerializedLargeBlobArray": 1024,
                    "minPINLength": 4,
                    "firmwareVersion": 328965,
                    "maxCredBlobLength": 32,
                    "maxRPIDsForSetMinPINLength": 1,
                    "preferredPlatformUvAttempts": 3,
                    "uvModality": 2,
                    "remainingDiscoverableCredentials": 25
                }
            },
            "statusReports": [
                {
                    "status": "FIDO_CERTIFIED",
                    "effectiveDate": "2021-08-06"
                },
                {
                    "status": "FIDO_CERTIFIED_L1",
                    "effectiveDate": "2021-08-06",
                    "url": "www.yubico.com",
                    "certificationDescriptor": "YubiKey Bio",
                    "certificateNumber": "FIDO20020210806001",
                    "certificationPolicyVersion": "1.3",
                    "certificationRequirementsVersion": "1.4"
                }
            ],
            "timeOfLastStatusChange": "2021-08-10"
        },

FIDO UAF認証器のMetadata Entry

       {
            "aaid": "4e4e#4005",
            "metadataStatement": {
                "legalHeader": "https://fidoalliance.org/metadata/metadata-statement-legal-header/",
                "aaid": "4e4e#4005",
                "description": "Touch ID, Face ID, or Passcode",
                "authenticatorVersion": 256,
                "protocolFamily": "uaf",
                "schema": 3,
                "upv": [
                    {
                        "major": 1,
                        "minor": 0
                    },
                    {
                        "major": 1,
                        "minor": 1
                    }
                ],
                "authenticationAlgorithms": [
                    "rsa_emsa_pkcs1_sha256_raw"
                ],
                "publicKeyAlgAndEncodings": [
                    "rsa_2048_raw"
                ],
                "attestationTypes": [
                    "basic_surrogate"
                ],
                "userVerificationDetails": [
                    [
                        {
                            "userVerificationMethod": "passcode_internal",
                            "caDesc": {
                                "base": 10,
                                "minLength": 4,
                                "maxRetries": 5,
                                "blockSlowdown": 60
                            }
                        }
                    ],
                    [
                        {
                            "userVerificationMethod": "fingerprint_internal",
                            "baDesc": {
                                "selfAttestedFRR": 0,
                                "selfAttestedFAR": 0,
                                "maxTemplates": 0,
                                "maxRetries": 5,
                                "blockSlowdown": 0
                            }
                        }
                    ]
                ],
                "keyProtection": [
                    "hardware",
                    "tee"
                ],
                "matcherProtection": [
                    "tee"
                ],
                "attachmentHint": [
                    "internal"
                ],
                "tcDisplay": [
                    "any"
                ],
                "tcDisplayContentType": "text/plain",
                "attestationRootCertificates": [],
                "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAAAXNSR0IArs4c6QAAABxpRE9UAAAAAgAAAAAAAAAkAAAAKAAAACQAAAAkAAAFJbuJ2EkAAATxSURBVHgB7JYxbiNHEEUFJ14YC5jJAgsnHsOOHPEAC2hyB+INNKEzMnSmucBieQPyBmLgnLyBeAPSJ1jegH6f6hqUy9PaXg0JO+AAX91dVV39/5/mQDfH4/HmirwHV3O+cEGuBl0Nyv98Sj4t1xt0vUHXG3TRX8Gg5jcDn59/rL4DH8AMbBxWzFvwG3g/8JhhGks+VLma1xJH9ATIhGMhZF7z2vNy/Eviw9z9SsaIrMG+0JQ+87R38pXHDtNY4mKuppQookZgHoxZs/4EpuD2BSivOtWbabp9o9Lzc/xL4sPcLWCIkApswWcgobd924irrnYYxzpyMvoOLMBf4F81cY/WJUbkaoZt7mPjYhIA/gR3LnzDWmbMwArsgd2MvlH5DWhBZwhzmfU7+NX37pvnxJfEL2YQxN+DD0aYuQTJlC3oM6I0dmD/HFSu9zub940lRuRqLmIQ5L81ohIC9PYlrNSE0jrdrFpnMX5jZ8YxJ74kfhGDjCCkZyBnzI7cAkzBLahsn40prm+Ovl1PIGfcitwPti+OJUbkai5iEGTHYNsj6DMxie2+JVHMS2v26TZOgcyNZulF9PbNiS+Jn90gSOo/Y5H1AmTMAxh5A7QGNZiBFszBBqzSWrEJqPw+zYndgx04BvwUa0uMyNWc1SCIypxI+JFYZaSZj0ADZESsfWm9p34JauulkbVulF6A7d34vOY58SXxYZsdEwi+dSRFVqQbVyIxLTgAE/Pace97M6/Ak+tb+3NLjMjVnNOgpSMoc7rvgeZg6/LRmDU54cHhMcXU65iBjOrMYP4p1W3+VwZB6vtETEIkyJvTsI63RjUL0PtftRenufqBKXgCfWbNiZ++b4w6TzW19cndjpL4WW4QZGaJVJ85UZCM+cfH2oRolDCDj9ucnMxag9h3S8ybtLQ9JUbkas5lkMiJcGOkNE8xEyLzastrZD1KdSvGPbBaPx6IK69+nbHMa7ADsXacenf1OfEl8cEGQXCcSD6aeNYi54nHm1WRX4YaX5+byyztq5IJI+aL0Ec1ZtIvqisxIlczbDOHQ2YG9G2w6z1m7gVGc1QvEb7mNfNW4vXQ6yH027PubltOfEn8HAbNjQyjzPHiozl6+9EM1SzAHTi9+WfZJ+FViiuvurh3Q8xeTBPyG+tTYkSuZrBBRkJEwTaQ7AQTlxgvUILvQfemmcvgGWgTauuvkZjqo1E600xaMPdntNqXE18SH7ZZp6cHYtGcxuWWgfijiVIN8wnYhxovVPurVDtirv0+701ah9zbEiNyNWcxCELRgFZi9JCbBcKL58zz3569Xnicz20v+6ah70Y5YjLQ37ImJ74kPsggiLwBK+CFdAYQb0LuibX9HCRkG/Lqo5p1ghdqZ2iP9Yj9TwaS9/FNiRG5mqEGfYSMEdfozRmH3JfMUX5sN8RGYvdgF3p5kxYhd+pBbJ3i/6lBG0cumnNwOd2ETjxzCTw6+L0V8SVQ7znQegSiEVtnosy1fqc467HFcrejJD70BkmEiD04siJ2MHKM0RyJNzEavTltyFldo/6qDfl5indmpLzVr7UuMSJXM9SgPyBQiaQe5g3w5khgc0o+55esTbRGb07M+bquj/aEHrX6E/P79ylWqzYnviQ+yCCRsAci80BcN2fi8l5ANKcNe/WTeQC7EB+rH7G+n1QVak9nq7bEiFzN3wAAAP//X9LlPwAABPNJREFU7Vq7jiNVFBwkJBCstB0QEey2IGSDzpaMDsnWMcl2SLCSHRBsNv4AxDgiQnL/wXRAPi3xATb8gP0H238wVLVPmdorz4Nx0G3JV6o599Y55/pUzZ1ZaTUXt7e3F0/FBda3L/MCWAO3hg/kmefCfmY51q2ALHLVPbksanX3ln1AkfRUcVdtfBPc7Kn62Pdkc9iMYd7ZQBJB8TmH48Leh07NodDO7tgbt+vefwNouO5fHLh3G1xqXI6+fEiDWhucAq6A/mUcEPGQOTSBgiYA7yXmQBVRBjHmAecm8Zk0WfyM3JAGNTHMBrHkMFzYZ0AbOQ3LwXvzEPmd7pJ8Gb2qvy/WUVvbHU1wM+NackMa9B7DXHIILZxLIBXv5lQH8pX18yXdZ45yeXyWzowZUCT9z4Y06DMTxoGbZDgOvQT0cmiOC6IZE93BiDPvmQKXwBWwAbxH+0XUe76/K+l5PZhBJqjGUOmroZApa7iwZ43EMdKcYpe9/yvqSmAFeP+WXeD8XpnmXDmYQRjuy2RoCaCYXjxiDqyTuo/MQW4CUFRr4GusgEz2Yb8E9Bn4N7g3iDXi1sHNjCsGMyiG2dgwFP6WPBf2HLSzPIXQvF40YglsAQm8Ky6sZxn1q/iM3PuD4726KxvaIA6/AdwYDtjakBr2igK4kGOf+MfENer7V7m74b+vyT19TXC9iUMb9FyjYqi7jOHLmlhdnYjqDQaXswYxA94AS8DN65jTPYrg+CpVV5IPbsH9oAbFMD9hIH6HNaTHJfi9KOxTc/avinelC/UlQIN1Z3ugprV8yTzO5Arux2BQbQNKyA24kgNyYc9XwaGVZ6z65C5f4dxEDePEcgXObtK+jzXRo3tnwfWR+zEYVGJIDXiNfcnBtHCeAJ3V7M0BlwGpcbqrYZ73IPIO8VvdHTnvnwdXMnINbhCHwPC/ADn3WjiXgA9PgXwJFWsQac4akPBDsWYtF+purNZfmH9GFbXPGLlGYdBulF5EARELYGtiJHwFrmAtYmoOjZsCeUT1MJbRU2EvfkGOC1xrfNmT9mU0BmHIf2xQCWHsxWtmnGni2mqZ742zmpnlG/I458a1Vrs1vhSvOCaDShuUxmwAvopMw2I/ATpABu7NAcd+r2Wur7N+9XUHOOY+F684GoM4EAb8DbgCCg0YPMW3gAQyujl15Fy41+dxz77f7hX3N7l0jcogHw6CC4A/KusQLyGMKyBnPSJrPNe/InBuUIYzobo2eufGvSKXrtEZhIFfAVsbXKIY+WqmEoF9ldTNmQPnZnwIbmK1TXDr4BY8H1qjM4hDYuhU+AbcJdC/jqiZhTgaRywlEPu55eqor41jbx7na/UdiqM0KAT9DAH8ffTGB8c5AxpAxqTmFEmujJ7OeJozB/ijujfdP0f70RqkARUpJES50NQc1mwBmde/DpwXxjXYs+5PRt1/Vxy9QRDxAvgd6AAJV5xKGHIUvbaaTXCFcezji/pRfQ/F0RtEARCUAzeAjOE+lzjsaUJnef4yJ5cBa+N/xf4L9T0mnoRBEgJxr4HvdWbEeQbIOEY3p40cuek3L15+4r2P2Z+UQS4Igr8C/ggDZNAGZ72cv7C/Bt4Cz733/+xP1iCJhHj+GP0AfAd8Gvha+WPjYAYd88Gn0nvU/5Wcishj5jwb9MCf/5wNOhv09D8Q44/m+QWdX9BxL+hfUwTYyRCarZ8AAAAASUVORK5CYII="
            },
            "statusReports": [
                {
                    "status": "NOT_FIDO_CERTIFIED",
                    "effectiveDate": "2018-05-19"
                }
            ],
            "timeOfLastStatusChange": "2018-05-19"
        },

FIDO U2F認証器のMetadata Entry

       {
            "attestationCertificateKeyIdentifiers": [
                "1434d2f277fe479c35ddf6aa4d08a07cbce99dd7"
            ],
            "metadataStatement": {
                "legalHeader": "https://fidoalliance.org/metadata/metadata-statement-legal-header/",
                "attestationCertificateKeyIdentifiers": [
                    "1434d2f277fe479c35ddf6aa4d08a07cbce99dd7"
                ],
                "description": "NEOWAVE Winkeo FIDO2",
                "authenticatorVersion": 2,
                "protocolFamily": "u2f",
                "schema": 3,
                "upv": [
                    {
                        "major": 1,
                        "minor": 1
                    }
                ],
                "authenticationAlgorithms": [
                    "secp256r1_ecdsa_sha256_raw"
                ],
                "publicKeyAlgAndEncodings": [
                    "ecc_x962_raw"
                ],
                "attestationTypes": [
                    "basic_full"
                ],
                "userVerificationDetails": [
                    [
                        {
                            "userVerificationMethod": "presence_internal"
                        }
                    ]
                ],
                "keyProtection": [
                    "hardware",
                    "secure_element"
                ],
                "matcherProtection": [
                    "on_chip"
                ],
                "cryptoStrength": 128,
                "attachmentHint": [
                    "external",
                    "wired"
                ],
                "tcDisplay": [],
                "attestationRootCertificates": [
                    "\n\nMIICHTCCAcKgAwIBAgICddUwCgYIKoZIzj0EAwIwezELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNlcnRFdXJvcGUxFzAVBgNVBAsTDjAwMDIgNDM0MjAyMTgwMSQwIgYDVQQDExtDZXJ0RXVyb3BlIEVsbGlwdGljIFJvb3QgQ0ExGDAWBgNVBGETD05UUkZSLTQzNDIwMjE4MDAeFw0xODAxMjIyMzAwMDBaFw0yODAxMjIyMzAwMDBaMHsxCzAJBgNVBAYTAkZSMRMwEQYDVQQKEwpDZXJ0RXVyb3BlMRcwFQYDVQQLEw4wMDAyIDQzNDIwMjE4MDEkMCIGA1UEAxMbQ2VydEV1cm9wZSBFbGxpcHRpYyBSb290IENBMRgwFgYDVQRhEw9OVFJGUi00MzQyMDIxODAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATz2jNaKOK/MKdW2fme1tq6GREuPuuKW9HgWYgMRrjvZUTOqLANJ3Md5Hqv1EN1zMd4lWtyfzRla7rv5ARBoOoTozYwNDAPBgNVHRMBAf8EBTADAQH/MBEGA1UdDgQKBAhNnTW0a4E8ujAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwIDSQAwRgIhAMrhb8SmfNLeLNgaAVmQ6AOMiLNLVHX0kFUO80CnT38EAiEAzNAgv4dH+HDhZSgZWJiaPu/nfZTeuGy4MydPMq5urs4=",
                    "\nMIIEODCCA92gAwIBAgIDAInBMAoGCCqGSM49BAMCMHsxCzAJBgNVBAYTAkZSMRMwEQYDVQQKEwpDZXJ0RXVyb3BlMRcwFQYDVQQLEw4wMDAyIDQzNDIwMjE4MDEkMCIGA1UEAxMbQ2VydEV1cm9wZSBFbGxpcHRpYyBSb290IENBMRgwFgYDVQRhEw9OVFJGUi00MzQyMDIxODAwHhcNMTgwMjIyMjMwMDAwWhcNMjgwMTIxMjMwMDAwWjB0MQswCQYDVQQGEwJGUjETMBEGA1UEChMKQ2VydEV1cm9wZTEXMBUGA1UECxMOMDAwMiA0MzQyMDIxODAxHTAbBgNVBAMTFENlcnRFdXJvcGUgSWRlY3lzIENBMRgwFgYDVQRhEw9OVFJGUi00MzQyMDIxODAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASLVL+1STJvaERO5WCR+jGcAxLvmPBDiZY1NgFFIhpX6OAZApQYmt6xSh74SwM+mjgnsSEcc4A2Uf139FgZ4rpYo4ICVTCCAlEwEwYDVR0jBAwwCoAITZ01tGuBPLowSgYIKwYBBQUHAQEEPjA8MDoGCCsGAQUFBzAChi5odHRwOi8vd3d3LmNlcnRldXJvcGUuZnIvcmVmZXJlbmNlL2VjX3Jvb3QuY3J0MFMGA1UdIARMMEowSAYJKoF6AWkpAQEAMDswOQYIKwYBBQUHAgEWLWh0dHBzOi8vd3d3LmNlcnRldXJvcGUuZnIvY2hhaW5lLWRlLWNvbmZpYW5jZTCCAWAGA1UdHwSCAVcwggFTMD+gPaA7hjlodHRwOi8vd3d3LmNlcnRldXJvcGUuZnIvcmVmZXJlbmNlL2NlcnRldXJvcGVfZWNfcm9vdC5jcmwwgYaggYOggYCGfmxkYXA6Ly9sY3IxLmNlcnRldXJvcGUuZnIvY249Q2VydEV1cm9wZSUyMEVsbGlwdGljJTIwUm9vdCUyMENBLG91PTAwMDIlMjA0MzQyMDIxODAsbz1DZXJ0RXVyb3BlLGM9RlI/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDCBhqCBg6CBgIZ+bGRhcDovL2xjcjIuY2VydGV1cm9wZS5mci9jbj1DZXJ0RXVyb3BlJTIwRWxsaXB0aWMlMjBSb290JTIwQ0Esb3U9MDAwMiUyMDQzNDIwMjE4MCxvPUNlcnRFdXJvcGUsYz1GUj9jZXJ0aWZpY2F0ZVJldm9jYXRpb25MaXN0MBEGA1UdDgQKBAhDaQbhTFtjcjAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQDAgNJADBGAiEAoEepHMC5X9jBKaGphcKjidhiN+Znz7v3S3hc31/AunsCIQDKqogK2SZOXZcvvHCB6UQSaA0nLn4RUwy1guDivbZbwg==",
                    "MIIEODCCA92gAwIBAgIDAInBMAoGCCqGSM49BAMCMHsxCzAJBgNVBAYTAkZSMRMwEQYDVQQKEwpDZXJ0RXVyb3BlMRcwFQYDVQQLEw4wMDAyIDQzNDIwMjE4MDEkMCIGA1UEAxMbQ2VydEV1cm9wZSBFbGxpcHRpYyBSb290IENBMRgwFgYDVQRhEw9OVFJGUi00MzQyMDIxODAwHhcNMTgwMjIyMjMwMDAwWhcNMjgwMTIxMjMwMDAwWjB0MQswCQYDVQQGEwJGUjETMBEGA1UEChMKQ2VydEV1cm9wZTEXMBUGA1UECxMOMDAwMiA0MzQyMDIxODAxHTAbBgNVBAMTFENlcnRFdXJvcGUgSWRlY3lzIENBMRgwFgYDVQRhEw9OVFJGUi00MzQyMDIxODAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASLVL+1STJvaERO5WCR+jGcAxLvmPBDiZY1NgFFIhpX6OAZApQYmt6xSh74SwM+mjgnsSEcc4A2Uf139FgZ4rpYo4ICVTCCAlEwEwYDVR0jBAwwCoAITZ01tGuBPLowSgYIKwYBBQUHAQEEPjA8MDoGCCsGAQUFBzAChi5odHRwOi8vd3d3LmNlcnRldXJvcGUuZnIvcmVmZXJlbmNlL2VjX3Jvb3QuY3J0MFMGA1UdIARMMEowSAYJKoF6AWkpAQEAMDswOQYIKwYBBQUHAgEWLWh0dHBzOi8vd3d3LmNlcnRldXJvcGUuZnIvY2hhaW5lLWRlLWNvbmZpYW5jZTCCAWAGA1UdHwSCAVcwggFTMD+gPaA7hjlodHRwOi8vd3d3LmNlcnRldXJvcGUuZnIvcmVmZXJlbmNlL2NlcnRldXJvcGVfZWNfcm9vdC5jcmwwgYaggYOggYCGfmxkYXA6Ly9sY3IxLmNlcnRldXJvcGUuZnIvY249Q2VydEV1cm9wZSUyMEVsbGlwdGljJTIwUm9vdCUyMENBLG91PTAwMDIlMjA0MzQyMDIxODAsbz1DZXJ0RXVyb3BlLGM9RlI/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDCBhqCBg6CBgIZ+bGRhcDovL2xjcjIuY2VydGV1cm9wZS5mci9jbj1DZXJ0RXVyb3BlJTIwRWxsaXB0aWMlMjBSb290JTIwQ0Esb3U9MDAwMiUyMDQzNDIwMjE4MCxvPUNlcnRFdXJvcGUsYz1GUj9jZXJ0aWZpY2F0ZVJldm9jYXRpb25MaXN0MBEGA1UdDgQKBAhDaQbhTFtjcjAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQDAgNJADBGAiEAoEepHMC5X9jBKaGphcKjidhiN+Znz7v3S3hc31/AunsCIQDKqogK2SZOXZcvvHCB6UQSaA0nLn4RUwy1guDivbZbwg==",
                    "MIICHTCCAcKgAwIBAgICddUwCgYIKoZIzj0EAwIwezELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNlcnRFdXJvcGUxFzAVBgNVBAsTDjAwMDIgNDM0MjAyMTgwMSQwIgYDVQQDExtDZXJ0RXVyb3BlIEVsbGlwdGljIFJvb3QgQ0ExGDAWBgNVBGETD05UUkZSLTQzNDIwMjE4MDAeFw0xODAxMjIyMzAwMDBaFw0yODAxMjIyMzAwMDBaMHsxCzAJBgNVBAYTAkZSMRMwEQYDVQQKEwpDZXJ0RXVyb3BlMRcwFQYDVQQLEw4wMDAyIDQzNDIwMjE4MDEkMCIGA1UEAxMbQ2VydEV1cm9wZSBFbGxpcHRpYyBSb290IENBMRgwFgYDVQRhEw9OVFJGUi00MzQyMDIxODAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATz2jNaKOK/MKdW2fme1tq6GREuPuuKW9HgWYgMRrjvZUTOqLANJ3Md5Hqv1EN1zMd4lWtyfzRla7rv5ARBoOoTozYwNDAPBgNVHRMBAf8EBTADAQH/MBEGA1UdDgQKBAhNnTW0a4E8ujAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwIDSQAwRgIhAMrhb8SmfNLeLNgaAVmQ6AOMiLNLVHX0kFUO80CnT38EAiEAzNAgv4dH+HDhZSgZWJiaPu/nfZTeuGy4MydPMq5urs4="
                ],
                "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAACqUlEQVRIx2P8//8/Ay0BEwONwagFpFlw8cKFirIyR3t7S1Oz0KDgBfPm//z5k3izvn39lp+Ta2tltWTRIoTofxhYtXKllpq6srwCAikoRIVHvH379j9x4NSpU0AtQI1W5hZwQagPzp87V11ZiXAvIxj9Zzh54kRNZRWRPvj96xcDOM0zMTKiB9G8uXP//fsHNFRASLC+sXHm7Nlubu4Qm3bt3Llu7VpiLGCEmcuIacGZU6fB4cWQX1AQGx/n7OIyaeoUbV0diIvamluePXtGUST/+g32HSODhoYGRISFhaWppYWVlRUo+OHjh6b6BoosgHvqz58/cDl9ff3M7CwIe8+e3atXrqQgmeIokDKzs/X19EGy/xk6OzofP3pEWUbDsAYYRC3tbRwcHED2h/fv62pqCReOjCTmZE0trZy8XAj78KFDy5YuJd50VAsYcepKTU83NjWBqOnu7Hxw/wE+O/7jsgC315mZmRubm9nZ2YFqvnz+0lBfhzOg/qO7lQm/B+EAmHwLioogCo4cOrxk0WIiPUEgkpFBUnKymZk5hN3T1XX3zh1iYoKJcDTBA4qFubmtlYubC8j++vVrTVU1qHQhzQeMBHyhrKxcWFwMUXn61Kn5c+dSv8JJSEy0trGGsCf099+6dQsuxcLCCrH7P5IrSYgDeKFS39TEx8sHZH//9r2uGhFQN65fh2VPNoqqTCUlpeKyUmgxfPpMSWERMAMuX7asv7cXIqilrYXwFrxeg/qOuGZSdEzM3t17Dh06CPT0pk0bN23cCI9FYKZJz8hE98Hff38hDDY2diL90dHdpaurixawrCysre3tunq6iLTX0NAAToIsTx4/tndwiIyOAtYExFjAzc3t4+sLJL99/QosE0VFRe3s7RtbmoGVFUqcjTYdh78FAIhBLlNd7ju1AAAAAElFTkSuQmCC"
            },
            "statusReports": [
                {
                    "status": "NOT_FIDO_CERTIFIED",
                    "effectiveDate": "2021-09-21"
                }
            ],
            "timeOfLastStatusChange": "2021-09-21"
        },

Googleスプレッドシートの内容をSlackに通知する

Googleフォームを使うと簡単にアンケートや問い合わせの入力フォームを作成することができる。 フォームの入力内容はGoogleドライブに作成されるGoogleスプレッドシートに自動的に保存される。 さらに、Googleフォームを使った投稿があったとき、Gmailだけでなくその他のWebhookが使えるツール(今回はSlack)に通知させることができる。

通知の連携にはGoogle App Script(以下、GAS)というJavaScriptに似た言語でスクリプトを書く必要があるが、Slackへの投稿方法とスクリプトは参考になる記事があったので後ほど紹介しておく(自分の備忘録を兼ねて)。

この記事では、Googleフォームで投稿された情報が保存されているGoogleスプレッドシートから最新の投稿を、1日1回Slackに通知させるGASについて書いておく。

Google App Scriptの参考記事

先人達の書いてくれているGoogleフォームの使い方、SlackのWebhook作成方法、Slackへの通知スクリプトについての記事を紹介する。

スプレッドシートの内容を通知する

スプレッドシートについて

スプレッドシートにはGoogleフォームでの入力内容が以下の画像のように蓄積されている(一部画像編集)。 新しい投稿があると、1枚目のシートのデータのある最下行の下に新規に追加される。

A列には自動的に収集されるタイムスタンプ、つまりGoogleフォームで投稿があった時刻が記録される。 B列以降はフォームに存在する質問が入っていく。 ちなみに質問1(B列)は数値、質問2(C列)は日付、質問3(D列)はプルダウンである。

スプレッドシートの情報を読み取って通知する

先述のように新しい投稿はスプレッドシートのデータのある最下行の下に追加されるため、最下行が最新データとなる。 今回の通知では、1日1回スプレッドシートから最新データを取り出してSlackに通知する。 利用方法としては、体重測定や薬の飲み忘れ防止として前日の結果を含めて投稿させるなどである。

Slackへの投稿は、前述の記事を参考にスクリプトを作成してほしい。

スプレッドシートの情報を読み取って通知するスクリプトは以下のとおりだ。

function trigger() {
  var ss = SpreadsheetApp.openById("1aVzXpskb_wRTIOvWYjuxGh1r4b1jgcHmZzbeWnquyFM");
  var sheet = ss.getSheetByName("フォームの回答 1");

  var lastRowNumber = sheet.getLastRow();

  var timestampRaw = sheet.getRange(lastRowNumber, 1).getValue();
  var timestamp = Utilities.formatDate(timestampRaw,"JST", "yyyy/MM/dd HH:mm:ss");
  var num = sheet.getRange(lastRowNumber, 2).getValue();
  var dateRaw = sheet.getRange(lastRowNumber, 3).getValue();
  var date = Utilities.formatDate(dateRaw,"JST", "yyyy/MM/dd");
  var drink = sheet.getRange(lastRowNumber, 4).getValue();

  var body = "<@somebody> \n\n" + "前回の記録は以下のとおりです。\n";
  var data = "タイムスタンプ:" + timestamp + "\n質問1:" + num + "\n質問2:" + date + "\n質問3:" + drink;
  var form = "\n本日の記録をしてください:" + "https://docs.google.com/forms/d/e/formformform/viewform"
  var publish = body + data + msg + form;
  sendToSlack(publish, "#channel");
}

L.1 trigger関数

この関数をGASの設定で1日1回指定した時刻に実行させる。

L.2 SpreadsheetApp.openById

引数で指定したスプレッドシートを開く関数。 引数のIDは、スプレッドシートを開いたときのURLhttps://docs.google.com/spreadsheets/d/<スプレッドシートのID>/edit#abcdefからわかる(https://docs.google.com/spreadsheets/d/test001spreadsheet/edit#abcdefならIDはtest001spreadsheet)。

SpreadsheetAppはGASに組み込まれているクラスなので何もせずとも使うことができる。

L.3 getSheetByName

取得したスプレッドシートのシートをシート名で取得する。

L.5 getLastRow

取得したシートのデータが存在する最下行、最新データの行番号を取得する。 あとで、行番号と列番号をつかって特定のセルのデータを取得するために使う。

LL.7-12 データ取得、日付変換

getRangeは第一引数に行番号、第二引数に列番号を入れることで特定のセルを取得できる。 取得したセルに対してgetValueを使うことでセルのデータを取得できる。

日付データの場合(L.8とL.11)、Utilities.formatDateを使うことで特定のフォーマットの文字列に変換する。 第一引数が日付データ、第二引数にタイムゾーン(日本なのでJST指定)、第三引数にフォーマット形式を指定する。

LL.14-17 整形

データをSlack通知のために整形している。 特に面白いことはないが、Slackで誰か宛に投稿する場合は<@somebody>のようにする必要がある。

L.18 sendToSlack関数

紹介した記事のsendToSlack関数を使って通知をおこなう。

おわり

今回のスクリプトは、毎日忘れずに何かをする場合に、Googleフォームとスプレッドシート、Slackを使って記録・保存・通知をさせる方法として結構便利に使っている。 投稿を忘れても前日の記録がないことを教えるような投稿をさせるともっと使いやすいかもしれない。

最後に、Googleスプレッドシートを扱うGAS実装で参考にした記事を紹介しておく。

Expressで非同期処理をおこなうミドルウェアの動作を確認する

Expressミドルウェアの挙動、特に非同期処理をおこなうにエラーが発生した場合の挙動が気になったので試してみた。 ミドルウェアで非同期処理をするのは前処理でデータベースに登録するケース等を想定している。

挙動を確認した実装はGitHubにおいた:nodejs-module-labo/express-middleware at main · s1r-J/nodejs-module-labo

実装で気をつけるポイント

先にポイントをまとめておく。

  • next()は複数回呼び出しても問題ない
  • ミドルウェアでエラーを発生されるときはnext(err);のように呼び出す
    • 呼び出さずにthrowするだけだとExpressがハングしてタイムアウトのエラーが発生する
  • ミドルウェアで発生したエラーはエラーハンドリングでキャッチできる
  • (エラーハンドリングでキャッチした場合など)レスポンスを複数回返すとエラーになるのでres.headersSentで確認する

実装例と挙動

非同期処理が正常に実行される

ミドルウェアで非同期処理が正常に実行される場合の実装例(抜粋、全体はGitHubにある)は以下のとおり。

app.use('/no-async', (req, res, next) => {
    // 非同期的に前処理をするミドルウェア
    console.log('no-async middleware');

    setTimeout(() => {
        console.log('no-async sleep');
        next(); // ①
    }, 3000);

    next(); // ②
});

app.get('/no-async', function (req, res) { // ③
    console.log('Call no-async.');
    res.send('Express response: no-async');
    console.log('---');
});

挙動を解説する。

  1. 非同期なのでsetTimeoutのコールバック処理は飛ばされて3000ミリ秒待つことなく、②のnext()が実行される。
  2. ②のnext()によってパスの処理(③)が実行され、サーバからレスポンスが返される。
  3. 3000ミリ秒後にコンソールに「no-async sleep」と表示される。
    • ①のnext()は実行されても再度③が呼ばれることはない。

非同期処理でエラーが発生する

ミドルウェアでの非同期処理でエラーが発生する場合の実装例(抜粋、全体はGitHubにある)は以下のとおり。

app.use('/no-async-error', (req, res, next) => {
    // 非同期的に前処理をするときにエラーが発生するミドルウェア
    console.log('no-async-error middleware');

    setTimeout(() => {
        try {
            if (true) {
                throw new Error('no-async-error');
            } else {
                // エラーが発生しなかったらnextを呼ぶ
                next();
            }
        } catch (err) {
            next(err); // ④
        }
}, 3000);

    next();  // ⑤
});

app.get('/no-async-error', function (req, res) { // ⑥
    console.log('Call error.');
    res.send('Express response: no-async-error');
    console.log('---');
});

// ===エラーハンドリング===
app.use((err, req, res, next) => {  // ⑦
    console.log(`Error: ${err.name}`);

    if (res.headersSent) {  // ⑧
        // 非同期的に処理が実施されるとレスポンスが返却されている可能性がある
        console.log('response is already sent.')
        // レスポンスを複数回返すとエラーになる
        // res.status(500).send('Express response: error');  // ⑨
    } else {
        res.status(500).send('Express response: error');
    }
    console.log('---');
});

挙動を解説する。

  1. 非同期なのでsetTimeoutのコールバック処理は飛ばされて3000ミリ秒待つことなく、⑤のnext()が実行される。
  2. ⑤のnext()によってパスの処理(⑥)が実行され、サーバからレスポンスが返される。
  3. 3000ミリ秒後にsetTimeoutのコールバックでエラーが発生し、④のnext(err)が呼び出される。
  4. ④のnext(err)は⑦のエラーハンドリングでキャッチされる。
  5. ⑧ではレスポンスが既に返却されているかを確認している。
    • レスポンスを複数回返すとエラー(Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client)が発生する。(⑨のコメントアウトを戻すとエラーになる)
    • res.headersSentはレスポンスが返却済の場合にはtrue、返却していない場合にはfalseを返却するう
    • 参考:Express 4.x - API リファレンス

おわりに

非同期のときに気になる挙動については確認できた。

レスポンスを複数回返すとエラーになるので注意が必要だ。 今回エラーハンドリングにres.headersSentを使った確認を実装した。パスのほうで時間がかかる処理があるならば、レスポンスを返却済でないのかを確認する処理を入れる、またはミドルウェアのエラーを握りつぶす(ログにだけは出しておく)ような実装をおこなう必要があるだろう。

Web Authentication API 9章 WebAuthn拡張機能の日本語訳

W3CのWebAuthnの仕様書9章 拡張機能について書かれている部分について、読んだので残しておく。

前にWeb Authentication API 7章 Relying Party処理の日本語訳 - s1r-Jの技術ブログについては日本語訳した。

9 WebAuthn拡張機能

公開鍵クレデンシャルを生成する機構は、認証アサーションの要求・生成と同じく5章 Web Authentication APIで定義されており、特定のユースケースに沿うように拡張することができる。それぞれのケースは 登録拡張機能registration extension )およびまたは 認証拡張機能authentication extension )を定義することで対応されている。

すべての拡張機能クライアント拡張機能client extension )であり、つまり拡張機能はクライアントと通信し、クライアントによって処理される。クライアント拡張機能は以下のステップとデータを定義している:

公開鍵クレデンシャルの生成または認証アサーションの要求の際、WebAuthnリライングパーティは一連の拡張機能の利用を要求することができる。これらの拡張機能はクライアントおよびまたはWebAuthn認証器によってサポートされている場合、要求処理中に呼び出される。リライングパーティは、get()認証拡張機能のため)の呼び出し中またはcreate()登録拡張機能のため)の呼び出し中に、各拡張機能のためのクライアント拡張機能インプットクライアントに送る。クライアントは、クライアントプラットフォームがサポートする各拡張機能に対してクライアント拡張機能処理を実行し、拡張機能識別子およびクライアント拡張機能アウトプット値を含めた各拡張機能で定義されているクライアントデータを拡張します。

また、拡張機能認証拡張機能authenticator extension )でもあり、つまり拡張機能は認証器と通信し、認証器によって処理される。認証拡張機能は以下のステップとデータを定義している:

認証器拡張機能に対して、認証器拡張機能処理の一部としてクライアントも各拡張機能のためのCBOR 認証器拡張機能インプット値(しばしば対応しているクライアント拡張機能インプット値に基づいて)を作成し、get()認証拡張機能のため)の呼び出し中またはcreate()登録拡張機能のため)の呼び出し中に認証機にそれらを受け渡す。これら認証器拡張機能インプット値はCBORで表され、ネーム・バリューのペアで受け渡される。拡張機能識別子はネームとして、対応する認証器拡張機能インプットはバリューとして扱われる。次に、認証器はサポートしている拡張機能にたいして追加の処理をおこない、格納機能によって定義されるCBOR 認証器拡張機能アウトプットを返す。 認証器拡張機能に対するクライアント拡張機能処理の一部は、クライアント拡張機能アウトプットを作成する入力値として認証器拡張機能アウトプットを利用する。

すべてのWebAuthn拡張機能はクライアントおよび認証器にとってOPTIONALである。つまり、リライングパーティから要求されたいかなる拡張機能でもクライアントブラウザまたはOSによって無視されたり、認証器に渡されなかったりすることがあり(MAY)、また認証器によって無視されることがある(MAY)。拡張機能を無視することはWebAuthn APIの処理では失敗として扱われることは決してなく、リライングパーティがどのAPI呼び出しでも拡張機能を含めたときでも、一部もしくは全ての拡張機能が無視されるケースについてハンドリングする準備しなければならない(MUST)。

可能なかぎり多くの拡張機能にサポートしたいクライアントは、認識できない拡張機能を認証器に渡すことを選んでもよく(MAY)、シンプルにクライアント拡張機能インプットをCBORにエンコードした認証器拡張機能インプットを生成する。すべてのWebAuthn拡張機能はこの実装選択がユーザのセキュリティやプライバシーを危険に晒さない方法で定義されていなければならない(MUST)。例えば、クライアント処理を要求する拡張機能の場合、意味のない認証器拡張機能インプット値を生成するようなナイーブな受け渡しを保証するような方法で定義されると、結果としてその拡張機能は認証器によって無視される。すべての拡張機能はOPTIONALであることから、API処理中の機能的失敗を発生させることはない。同様にクライアントは、CBOR出力がJSONだけに存在する型を使用する場合、認証器拡張機能アウトプット値をJSONエンコードしたことで解釈できない拡張機能クライアント拡張機能アウトプット値の処理を選ぶことができる。

クライアントは認識できない拡張機能を受け渡すと選択したとき、クライアント拡張機能インプットJavaScriptの値を認証器拡張機能インプットCBORの値に変換する。JavaScriptの値が%ArrayBuffer%の場合、CBOR byte arrayに変換される。JavaScriptの値が非整数値の場合、64ビットCBOR浮動小数点数に変換される。一方、JSONの型に対応したJavaScriptの型の場合、変換は[RFC8949]の6.2節(JSONからCBORへの変換)で定義されているルールに従って実施されますが、JSONの型の値のインプットに対してではなくJavaScriptの型の値のインプットに処理をおこないます。これらの変換が実施された場合、変換結果のCBORの正規化はCTAP2 カノニカルCBORエンコード形式を使って実行しなければならない(MUST)。

JavaScriptの数値変換ルールは、クライアントが認識していない拡張機能を受け渡すときに拡張機能浮動小数点数を利用していた場合、認証器CBOR整数としてそれらの値を受け取る準備が必要であり、認証器は実際のクライアントの助けなしに常に動作すべきということに注意する。これは使われている浮動小数点数がたまたま整数である場合に発生する。

同様に、クライアントが認識していない拡張機能からアウトプットを受け取ったとき、認証器拡張機能アウトプットCBORの値はクライアント拡張機能アウトプットJavaScriptの値に変換される。CBORの値はバイト文字列のとき、JavaScript %ArrayBuffer%(base64urlエンコードされた文字列ではなく)に変換される。一方、CBORの型はJSONの型に対応しているとき、変換は[RFC8949]の6.1節(CBORからJSONへの変換)に定義されているルールに従って実施されるが、JSONの型の値ではなくJavaScriptの型の値のアウトプットが生成される。

一部のクライアントは機能フラグのもとでこの受け渡し機能を実装することを選択する可能性があることに注意する。認証器が新しい拡張機能を実験することを許可し、クライアントで明示的にサポートする前にリライングパーティがそれらを利用できるようになり、この機能をサポートすることでイノベーションを促進する。

[RFC8809]によって設立されたIANA「WebAuthn Extension Identifiers」レジストリ [IANA-WebAuthn-Registries] は登録済WebAuthn拡張機能の最新のリストについて協議する。

9.1 拡張機能識別子

拡張機能は、拡張機能の作成者が決定した 拡張機能識別子extension identifier )と呼ばれる文字列によって識別されています。

拡張機能識別子は、[RFC8809]によって設立されたIANA 「WebAuthn Extension Identifiers」レジストリ[IANA-WebAuthn-Registries]に登録すべきである(SHOULD)。当然ではあるが登録されている拡張機能識別子はユニークである。

すべての拡張機能識別子は最大32オクテット長でなければならず(MUST)、バックスラッシュおよびダブルクォート、つまり%x22および%x5cを除いた[RFC5234]で定義されているVCHARを除く印字可能USASCII文字だけで構成されなければならない(MUST)。実装はケースセンシティブにWebAuthn拡張機能識別子に一致しなければならない(MUST)。

複数のバージョンが存在する可能性がある拡張機能は、それらの識別子にバージョンが含まれていることに注意するべきである。実際、つまり異なるバージョンは異なる拡張機能として扱われる。例:myCompany_extension_01

10章 定義済拡張機能は追加の拡張機能および拡張機能識別子の一覧を定義する。登録済のWebAuthn拡張機能識別子の最新リストのための[RFC8809]によって設立されたIANA 「WebAuthn拡張機能識別子」レジストリ [IANA-WebAuthn-Registries]を参照する。

9.2 拡張機能の定義

拡張機能の定義では、拡張機能識別子get()またはcreate()を介して送られたクライアント拡張機能インプット引数、クライアント拡張機能処理のルール、そしてクライアント拡張機能アウトプットを定義する。認証器と連携する拡張機能(つまり認証器拡張機能)の場合、authenticatorGetAssertionまたはauthenticatorMakeCredentialの呼び出しによってCBOR 認証器拡張機能インプット引数、認証器拡張機能処理のルールおよびCBOR 認証器拡張機能アウトプットの値も定義しなければならない(MUST)。

クライアントによって処理されたすべてのクライアント拡張機能クライアント拡張機能アウトプットを返却しなればならず(MUST)、WebAuthnリライングパーティはクライアントによって利用された拡張機能を知ることになる。同じように、認証器での処理を必須とするすべての拡張機能は、リライングパーティが認証器によって利用された拡張機能を知るために認証器拡張機能アウトプットを返却しなければならない(MUST)。拡張機能が如何なる結果の値も必須としない場合、JSON Boolean クライアント拡張機能アウトプットの結果をその拡張機能が理解されて処理されたことを知らせるためにtrueをセットして返却するように定義すべきである(SHOULD)。同様に如何なる結果の値も必須としない認証器拡張機能は値を返却しなくてはならず(MUST)、CBOR Boolean 認証器拡張機能アウトプットの結果をその拡張機能が理解されて処理されたことを知らせるためにtrueをセットして返すべきである(SHOULD)。

9.3 リクエストパラメータの拡張

拡張機能は1つか2つのリクエスト引数を定義する。値がJSONエンコードされている クライアント拡張機能インプットclient extension input )は、get()またはcreate()の呼び出しにおいてWebAuthnリライングパーティからクライアントに渡され、一方で 認証器拡張機能authenticator extension input向けのCBOR形式の認証器拡張機能インプットはそれらの呼び出し時にクライアントから認証器に渡される。

リライングパーティは同時に拡張機能の利用を要求し、create()またはget()の呼び出しに対してextensionsのオプションにエントリを含めることでそのクライアント拡張機能インプットをセットする。

注意:他のドキュメントでは、拡張機能インプットがエントリキーとして拡張機能識別子を常に利用しているとは限らないと定義していた。新しい拡張機能には上述の変換に従うべきである(SHOULD)。

EXAMPLE 10

var assertionPromise = navigator.credentials.get({
    publicKey: {
        // Other members omitted for brevity
        extensions: {
            // An "entry key" identifying the "webauthnExample_foobar" extension, 
            // whose value is a map with two input parameters:
            "webauthnExample_foobar": {
              foo: 42,
              bar: "barfoo"
            }
        }
    }
});

拡張機能の定義は、クライアント拡張機能インプットとして正当な値を定義しなくてはならない(MUST)。クライアントは不正なクライアント拡張機能インプットを持つ拡張機能を無視すべきである(SHOULD)。拡張機能リライングパーティからのパラメータを一切必要としない場合、リライングパーティから要求された拡張機能が処理されたことを示すためにtrueを設定するBoolean型のクライアント引数を取るように定義すべきである(SHOULD)。

クライアントでの処理に対してだけ影響する拡張機能は、認証器拡張機能インプットを定義する必要がない。認証器で処理をおこなう拡張機能は、クライアント拡張機能インプットから認証器拡張機能インプットの算出方法を定義しなければならず(MUST)、AuthenticationExtensionsAuthenticatorInputsおよびAuthenticationExtensionsAuthenticatorOutputsCDDLを、エントリキーとして拡張機能識別子を利用して$$extensionInputおよび$$extensionOutput グループソケットに対する追加選択を定義することで定義しなければならない(MUST)。インプットパラメータを必須としない、つまりBoolean型のクライアント拡張機能インプットの値をtrueに設定する拡張機能は、認証器拡張機能インプットもBoolean値true(CBOR major type 7, value 21)として定義するべきである(SHOULD)。

以下の例では、識別子 WebauthnExample_foobarを持つ拡張機能認証器拡張機能インプットとして符号なし整数を取り、認証器拡張機能アウトプットとして最小1バイト列の配列を返すことを定義している:

EXAMPLE 11

$$extensionInput //= (
  webauthnExample_foobar: uint
)
$$extensionOutput //= (
  webauthnExample_foobar: [+ bytes]
)

注意:拡張機能は認証器引数をできる限り小さくなることを目指して定義すべきである。一部の認証器はBluetooth Low-EnergyやNFCのような低帯域接続による通信をおこなっている。

9.4 クライアント拡張機能処理

拡張機能は、クレデンシャルの作成またはアサーションの生成のあいだにクライアントでの追加処理の要求を定義しているかもしれない(MAY)。拡張機能に対するクライアント拡張機能インプットはクライアント処理に対する入力として利用される。サポートされている各クライアント拡張機能に対し、クライアントは clientExtensions mapに対して拡張機能識別子をキー、クライアント拡張機能インプットをバリューとして拡張機能のエントリを追加する。

同様に、クライアント拡張機能アウトプットは、拡張機能識別子をキー、各拡張機能クライアント拡張機能アウトプットclient extension output )の値をバリューとしてgetClientExtensionResults()の結果のディクショナリとして表現される。クライアント拡張機能インプットと同じく、クライアント拡張機能アウトプットJSONエンコードされた値である。無視した拡張機能に対しては如何なる値も返却してはならない(MUST NOT)。

認証器処理を必須とする拡張機能は、CBOR 認証器拡張機能インプットを決定するためのクライアント拡張機能インプットを利用する処理およびクライアント拡張機能アウトプットを決定するためのCBOR 認証器拡張機能インプットを利用する処理を定義しなければならない(MUST)。

9.5 認証器拡張機能処理

処理された各認証器拡張機能CBOR 認証器拡張機能インプットの値は、authenticatorMakeCredentialおよびauthenticatorGetAssertionの処理の拡張機能パラメータに含まれる。拡張機能パラメータは各キーが拡張機能識別子で対応する値が認証器拡張機能インプットとなっているCBORマップである。

同様に、拡張機能アウトプットはauthenticator dataextensions部分に表記されている。authenticator dataextensions部分は、各キーが拡張機能識別子で対応する値が 認証器拡張機能アウトプットauthenticator extension output )となっているCBORマップである。

サポートされている各拡張機能に対し、認証器拡張機能処理ルールは認証器拡張機能インプットから認証器拡張機能アウトプットを生成するために利用され、その他入力値に対しても可能な限り同様である。無視する拡張機能に対しては如何なる値も返却してはならない(MUST NOT)。

mkcertを使ってlocalhostをHTTPS化して開発する

以前も同じ内容の記事を書きましたが、auth0のブログにあった方法のほうが便利そうなので、試してみました。 auth0のブログの内容をちょこちょこ翻訳しながら紹介します。

元になったブログ記事はこちら

Why and How to Use HTTPS in Your Local Development Environment

auth0はOAuth・OIDCなどの認証認可基盤とよばれるようなシステムの開発会社です。認証認可(サインインやログインなど)に関するブラウザ等の機能(例:FIDO2認証)にはHTTPSが必要とされることがあり、ローカル開発環境にもHTTPSが必要になのでしょう。

試した環境

  • Windows 10
  • Chocolatey v0.12.1
    • mkcertをインストールするために必要
  • mkcert v1.4.3
    • HTTPSに必要な証明書などを作成するツール
  • Node.js v12.22.5

手順

mkcertのインストール

HTTPSに必要な証明書などを作成するツールmkcertをインストールします。 OpenSSLが最もよく知られていて最も強力ですが、公開鍵基盤について充分な知識が必要なため、今回はmkcertを使うとのことです。

mkcertのインストール方法はmkcertのGitHubに記載されているとおりですが、WindowsなのでChocolateyを使います。WSLを使ってLinux版のインストール手順で実行することもできそうですね。

Chocolateyのインストール方法も公式サイトに記載されているとおりで出来ました。

Chocolatey Software | Installing Chocolatey

PowerShellを管理者権限で起動して以下のコマンドを実行

$ Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

完了するまで待ち、完了したら以下のコマンドで確認します。

$ choco

バージョン情報が帰ってくれば完了です。 改めてmkcertのインストールをします。

$ choco install mkcert

インストールが完了すると、インストール成功という情報とバージョンの情報が返ってきました。

mkcertでローカル認証局を作成する

以下のコマンドを実行します。

$ mkcert -install

ポップアップ画面で以下の画像のようなセキュリティ警告が表示されました。 どうやら、Firefoxの機能・設定に関する警告(Setting Up Certificate Authorities (CAs) in Firefox | Firefox for Enterprise Help)であり、Firefoxを使わないのであれば無視してよいそうです。

mkcert

今回は「はい」を選ぶことにしました。

証明書の作成

まず、Node.js上に構築するアプリのフォルダを作成します。フォルダ名はブログ記事の内容と同じにしてmy-secure-appとします。 作成したフォルダ内に入ります。

フォルダ内で以下のコマンドを実行し、localhostというホスト名に対する証明書(localhost.pem)と秘密鍵localhost-key.pem)を作成します。

$ mkcert localhost

サーバのソースコード

先のmy-secure-appフォルダ内にserver.jsというファイルを作成し、以下のコードを書いて保存します。

const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('localhost-key.pem'),
  cert: fs.readFileSync('localhost.pem')
};

https.createServer(options, function (req, res) {
  res.writeHead(200);
  res.end("I'm HTTPS-enabled!");
}).listen(8080);

console.log("The server is listening to port 8080 with HTTPS enabled.");

実行

my-secure-appフォルダ内でPowerShellコマンドプロンプトを立ち上げ、以下のコマンドを入力します。

$ node server.js

これによってhttps://localhost:8080でサーバがアクセスを待っています。 実際に上記のURLをChromeに入力すると、「I'm HTTPS-enabled!」というメッセージがブラウザに表示されます。

アドレスを変更してhttp://localhost:8080にしてHTTPで接続を試みると、「ページは動作していません」というブラウザのエラー画面になります。

また、Firefoxhttps://localhost:8080にアクセスを試みると、以下の画像のように警告が表示されます。証明書が無効になっているHTTPSのサイトにアクセスしたときに表示される画面と同じです。 mkcertでローカル認証局を作成したときの警告と関連しており、Firefoxがこの認証局を信頼していないということが原因です。

Firefox

対策としては、Setting Up Certificate Authorities (CAs) in Firefox | Firefox for Enterprise Helpに記載されている指定のいちにルート証明書を配置することらしい(ちゃんと読んでいない)。

証明書の情報

すこし証明書のなかみのを確認

certificate

発行先は自分のPCの自分のアカウントで、発行元のは自分のPCの自分のアカウントのmkcertとのこと。 有効期限は2年3ヶ月(27ヶ月)ですね。Let's Encryptoよりも随分長い。こちらのほうが便利かもしれないですね。

詳細もスクショしようと思いましたが消すのめんどくさくなってきたので、文字情報だけで。

おわりに

今回の方法はmkcertを使うことでlocalhost127.0.0.1)を27ヶ月間更新する手間なくHTTPS化して開発することができます。また、WSLがなくてもWindowsLinuxMacで実行できます。

以前、書いた記事ではFreenomでドメインを取得して、certbotを使ってLet's Encryptoの証明書を作成し、3ヶ月ごとに更新する必要がありました。

今回の記事のほうが更新頻度も少なくて楽。また、環境を他の開発者に共有したり、配布するときにこちらのほうが良さそう(Dockerで配布とかもできる??)なので、こちらの方法に切り替えていこうかなと思っています。

参考文献

AWS EC2でcurl: (60) SSL certificate problem: certificate has expired

現象

EC2でcurlを実行したところ、以下のようなエラーが発生した(アドレスはダミー)。

$ curl https://example.com

curl: (60) SSL certificate problem: certificate has expired

証明書のエラーだったので、サーバ側(上の例だとhttps://example.com)に原因があると思っていたが、今回は違った。 エラーについて詳しく見るため、-vのコマンドを付けて確認した。アドレスなどダミーにして隠しておく。

$ curl https://example.com -v
* Rebuilt URL to: https://example.com
*   Trying 192.168.0.1...
* TCP_NODELAY set
* Connected to example.com (192.168.0.1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOE:!RC4:@STRENGTH
* successfully set certificate verify locations:
*   CAfile: /etc/pki/tls/certs/ca-bundle.crt
  CApath: none
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS alert, certificate expired (557):
* SSL certificate problem: certificate has expired
* Closing connection 0
curl: (60) SSL certificate problem: certificate has expired
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

解決

原因はクライアント側(EC2インスタンス)のルート証明書の期限が切れていたことだった。 2021年1月あたりに作成したEC2インスタンスだったので、期限切れのままつかっていたのかもしれない。

解決方法については、AWS公式サイトに記載されていた。

EC2 インスタンスにある期限切れの Let’s Encrypt 証明書を修正する

今回使っていたEC2インスタンスAmazon Linux2だったので、以下のコマンドを実行。

sudo yum install https://cdn.amazonlinux.com/patch/ca-certificates-update-2021-09-30/ca-certificates-2021.2.50-72.amzn2.0.1.noarch.rpm

証明書のインストールが完了したら、再度curlを実行したところ問題なく通信ができた。

Qiitaで同じことをしている人がいたので、こちらも記載しておく。

【AWS/EC2/Amazon Linux2】curl: (60) SSL certificate problem: certificate has expired - Qiita