小野マトペの納豆ペペロンチーノ日記

2024年衆議院選挙で私が考えていること(刑事司法・裏金・民主主義)

衆議院選挙の党開票日が27日に迫る中、ふと、今回の投票にあたって自分が考えていることを書き起こしておこうという気持ちが湧いてきました。大上段に政治を語るのは少し気恥ずかしいですが、いい歳をした大人が政治についての考えをきちんと語れないほうがよほど恥ずかしいとの信念に基づいて、やってみようと思います。

今回の選挙で私が重視しているのは、個別の政策でいえば「刑事司法」、全体的な考え方で言えば「民主主義と経済」です。

刑事司法制度

いま、冤罪に関する再審無罪判決や再審開始の決定が相次いでいます。

袴田事件が、「5点の着衣」が警察による捏造証拠であったことを認定して再審無罪となったことはみなさんもご存じの通りですが、今週、1986年に発生した福井女子中学生殺害事件で7年の懲役判決が確定し服役した男性の再審の開始も決定されました。

福井中3事件では、新たに開示された検察側証拠から、検察が男性を有罪にするために事実に反する主張をし、証人に利益を供与して唯一の証拠である目撃証言を誘導していた疑いが明らかになりました。裁判所は「不誠実で罪深い不正の所為」と検察の不正義を糾弾しています。

どちらの事件でも、裁判官が裁量で提出を命じた検察側の証拠によって真実の究明につながりました。冤罪の原因の検証とともに、検察に全ての証拠の提出を義務付ける再審法の改正、さらには人質司法や死刑制度の見直しが急務です。

実は、あまり一般には知られていませんが、この分野に関しては民主党政権に実績があります。

14年前、検察官が証拠のフロッピーディスクを改竄した村木厚子さんの冤罪事件が明らかになった時は、当時の民主党政権柳田稔法務大臣が私的諮問機関「検察の在り方検討会議」を開きました。そこで「検察の再生に向けて」という提言がまとめられ、その提言から法制審議会「新時代の刑事司法制度特別部会」が開かれ、一部事件の取り調べ可視化や国選弁護人制度の拡大など、わずかではあるものの刑事司法改革は前進しました。

この法制審議会には映画監督の周防正行氏も参加し、著作『それでもボクは会議で戦う』で、改革に激しく抵抗する法務省検察庁との攻防の様子を読むことができます。

では、今回はどうでしょうか。残念ながら、袴田事件という前回よりもさらに根深い事件に直面してもなお、現在の自民党政権に、民主党政権の時のような積極的な改革は期待できる状況にありません。

とりわけ現内閣の牧原秀樹法務大臣は、弁護士資格を有していながら、SNSで「オリンピック選手への誹謗中傷は全員逮捕すべき」と捜査の謙抑主義を定めた刑事訴訟規則に反する投稿をしたり、煽り運転や性犯罪の法定刑に死刑がないことを残念がり、上川元法務大臣が3週間で13人もの死刑を執行したことを「なんて凄い方だ」と称賛するなど、逮捕や死刑の執行に異様な執着心を見せている法務大臣でもあります。

牧原法相は袴田事件の無罪確定後も、畝本直美検事総長「長期間に渡って法的地位が不安定な状況にして申し訳ない」という曖昧な謝罪の文言を踏襲するのみで、再審制度の見直しや再発防止についてて問われても、法務省議員連盟での議論を期待すると述べるにとどまりました。

再審法の改正については日弁連の旗振りで超党派議員連盟が組織され、自民党の大物議員も多数参加しているとはいえ、このような法務大臣を任命する自民党の政権において、冤罪・人質司法の前進が期待できるとは期待できません。

では、袴田事件の反省を生かし、日本の刑事司法の改善に繋げられる政党はどこでしょうか。主要政党の中で、政策集に再審法改正のある政党を「○」、再審法改正以外の冤罪防止の取り組みのある政党を「△」、冤罪防止の取り組みの見られない党を「×」として、私が調べた限りでまとめてみました。

立憲民主党

立憲民主党は「2024年衆院選 主な政策項目 - 立憲民主党」にこう書いています。

  • えん罪被害者の速やかな救済のため、再審法を全面的に見直します。
  • えん罪をなくすため、「取り調べ等の録音・録画(可視化)制度」の対象事件をさらに拡大し、取り調べ等の開始から終了までの録音・録画を実現します。

立憲民主党 政策集2024」でも「人権尊重の刑事司法制度」を掲げ、再審法の他にも取り調べ録音録画の対象の拡大、人質司法の改善などさらに多くの取り組みを提案しています。

日本共産党

共産党の「2024年総選挙政策」にはこうあります。

――再審法を改正します。

 袴田さん無罪判決では、捜査機関による自白強要と証拠捏造を断罪しました。二度とこのような国家権力による重大な人権侵害を引き起こさないために、全面的な証拠開示と、再審開始決定に対する検察による不服申し立ての禁止を制度化するなど再審法改正を行います。

さらに、分野ごとの政策ページでは広範な改善政策を提案しています。

75、共謀罪廃止・盗聴法拡大・刑訴法「改正」問題

77、司法・警察│総選挙政策

社民党

再審法についての言及はないものの、社民党の「統一自治体選挙2023政策集(PDF)」の中に、参考人を含む取り調べの全過程の可視化や検察側全証拠の開示義務付け、代用監獄の廃止などの政策提言が確認できます。

日本維新の会

日本維新の会は、再審法改正に関する記述はありませんが、「維新八策2024 個別政策集|日本維新の会」に、「347. 冤罪根絶のため、参考人も含めてすべての捜査において取り調べの全面可視化を行うとともに、国際基準である取り調べ時の弁護人立ち合いの制度化に努めます。」としています。

人質司法や冤罪といった問題は、政治と(もっといえば政治思想と)強く結びついた問題です。袴田巌さんのような冤罪事件を減らすためには制度を変革する必要があり、それを効果的に実現できるのは保守政党ではなくリベラル政党であり、リベラル政権がいま必要とされていると考えています。

裏金・民主主義・経済

裏金問題ー自民党政治資金パーティー収入不記載問題について。

世の中は裏金許すまじとヒートアップしています。しかし、なかには本当にそんなに悪いことなの?もっと大事なことを話し合った方がいいんじゃないの?という思いの人もいるんじゃないかと思うので、ここではあえて、なぜ許されないのか、ざっくりと自分が考えていることを振り返って整理していこうと思います。

私は、この問題は民主主義の働きを歪めているために許されないと考えています。

自民党の一部の議員は、政治資金パーティーで支持者から集めた政治資金のうち、ノルマを超えた分を派閥から自らに環流(キックバック)させたり、派閥に納めなかったりして、政治資金収支報告書に記載していなかったことが明らかになっています。周知の通り、これは明らかに組織的な動きでした。

政治資金規制法に捕捉されない現金ができること。これは、政治家にどこから金が入り、どこに金が流れたかを国民が知ることのできないお金ができることを意味します。裏金ですね。

そもそも我々の政治システムが前提とする民主主義というのは、「国民ぜんたいの議論を政治に反映させれば、国家が最も繁栄する」という考え方で成り立っているわけですよね。いっぽう、国民全員が直接議論することは現実的にできないので、かわりに幅広い国民の利益を代表する代議士を選出する、代議制民主主義というシステムによって便宜上それを実現しているわけです。

そこにカネが介在すると、「国民の代表者」という代議制の建前が失われ、民主主義が本来予定していた機能を喪失してしまいます。だからこそ政治資金規制法や公職選挙法といった法律は、政治家や政治団体のお金の流れを制限したり、市民が監視できるように公開を義務付けています。

今回の裏金問題では、自民党が少なくとも20年以上にわたり、組織的に国民の監視の届かないカネの流れを作ろうと腐心していたことが明らかになりました。つまり、組織的な意思を持って民主主義のシステムに抜け穴が作られ、本来の民主主義の予定する働きが妨げられていたわけです。

私は正直なところ、日本が30年間経済的に発展しなかったのは、そのことと無関係ではないだろうと考えています。

今年のノーベル経済学賞を受賞して話題になったダロン・アセモグルらは、二〇一三年の『国家はなぜ衰退するのか』において、一部のエリートが権力を独占する収奪的政治体制のもとでは、経済発展はどこかで頭打ちになり、持続的に成長しないと主張しています。アセモグルらは、国家の経済が発展するかどうかは、その政治体制が多元的で包括的かどうかによって決定づけられると主張しています。

実際、日本が過去に経験した経済発展である近代化と戦後の経済成長は、いずれも明治維新第二次世界大戦の敗戦という一種の政治革命に続くものでした。日本は民主的な多元的政治体制をインストールするたびに飛躍を遂げてきた国ということもできるかもしれません。

いまの日本はどうでしょうか。

多くの被害者を出してきたカルト教団宗教右派の票を得るために、ただカップルの両名が結婚してからも同じ名前で働けるようにするというだけの自由が世界中で日本でだけ禁止され、同性愛者が異性愛者と同様に家庭を築く自由もありません。

経済エリートの意向を汲み、雇用緩和により非正規労働が常態化し、法人税は下がりつづけ、消費税は上がり続けています。異次元の金融緩和を続けても弱体化した国内消費をカバーできず、約束されたトリクルダウンは起きず、格差は一層拡大して一般消費者には生活を圧迫する物価高だけが残りました。

政府の教育への支出はOECD加盟国で最低レベルですが、軍事費は野放図に拡大しています。中小企業の経理事務は強行されたインボイスで混乱し、保険証の廃止は決定経緯の文書すら存在しません。

このような政治体制のもと、我々の経済はすでに30年の停滞を続けています。次の4年間で突然イノベーションが刺激されて経済が発展するとは、アセモグルを持ち出すまでもなく想像できません。

そういうわけで、私は、日本の経済が再び豊かになるためにも、民主的で多元的な政治体制を指向するリベラル政権への転換が必要だと考えています。

足元に目を戻しても、代議制民主主義を本来の働きに戻すためには「企業・団体献金の廃止」を実現する必要があると私は考えますが、自民党にその能力がないことはすでに明らかです。

だいたいそんな感じです。ここまで書いておいてなんですが、まだ投票先は決めてません。

では。

映画『福田村事件』感想

9月1日、ユーロスペースで『福田村事件』を観た。

www.youtube.com

関東大震災直後の千葉で香川からの行商15名のうち子供と妊婦を含む9名が、地元の自警団に朝鮮人と間違えられて殺害された実在の事件を基にした劇映画。

事件が起きたのは1923年(大正12年)9月6日、つまり100年前の今日だ。

監督は著名なドキュメンタリー監督の森達也氏。失礼ながらドキュメンタリー作家の初劇映画ということで正直期待していなかったが、観てみると意外にもしっかりした映画で驚いてしまった。

前半は狭い村でのドロドロとした愛憎が組み立てられていき、その情念が後半での虐殺事件に流れ込んでいく。冒頭から鍬や包丁など、日常の中に暴力の予感を忍ばせるのも上手いし、豆腐、白磁の指輪、朝鮮飴といった純白モチーフの使い方も手慣れている。

実はこの作品、森達也監督と同時期にたまたま劇映画畑の脚本スタッフ3名も「福田村事件」の映画化企画を走らせていて、じゃあ一緒にやろうと合流したという経緯がある。その結果、じっとりとした人間ドラマとジャーナリスト的な考証の両輪が互いに作品の強度を強め合っていると感じた。

国粋主義を背景にした民衆の暴走を描く都合上、リベラルな立場から当時の軍国主義を一方的に断罪する話になるのを危惧していたが、登場する全ての人物が虚栄心や不安、情欲、臆病さといった"人間としての弱さ"を抱えており、(味付けに賛否はあるだろうが)臭みは感じなかった。被害者の行商たちも無垢には描かれず、おのおのが複層的な差別構造の中にあることが提示される。

大正時代を描いた日本のフィクションにおいて、韓国併合によって日本人となっていた朝鮮人は通常透明化されるため、日本社会における生活者としての彼らが描かれている時点ですでに興味深い。当時はまだ日本の地域ごとの違いが激しく、香川と千葉の風俗や方言の違いも事件の背景になったという物語上の要請からも「ジーヤン(じゃんけん)」など地域の風俗の違いが多数紹介されるのだが、これも楽しめる。

俳優陣も、『Winny(2023)』で金子勇氏を主演した東出昌大、『怪物(2023)』で教師を演じた永山瑛太在郷軍人会の分会長を怪演する水道橋博士など、みんな上手くて緊張感が途切れない。鈴木慶一の音楽も良い。

映画は、結局震災のショックの中で不法な暴力を振るったのは朝鮮人ではなく、アンオフィシャルな「自警団」だったことを描く。当初は内務省の要請した自警活動であったが、彼らはやがて政府のコントロールを離れて暴走していく。

ではなぜ暴走してしまったのか。韓国併合以降の民族独立運動の高まりと国内での差別的扱いから、誰もが「仕返しに暴動を起こしてもおかしくない」と疑心暗鬼に陥ったから。事件の背景も、映画はそうやって紐解いていく。

本作はアイデンティティの物語だ。映画の最後、ある生存者が殺害された胎児を含む10人のフルネームを読み上げるシーンがあるのだが*1、その「名前」の扱い方が強く印象に残った。

各地を巡業する行商団は地域社会におけるマージナルな存在として描かれるが、しかし最後に読み上げられた彼らの本名を通じて、われわれは否応なく彼らと一種の繋がりを感じてしまう。名前には拭いがたく個人と国家を接続する力があるということに気付かされる。「みんな名前があったんです」からのスタッフロール(一つの映画を世に届けるために力を合わせた、たくさんの人の「名前」)は胸にこみ上げるものがあった。

映画を見終えて帰り道に想起したのは、最近流行りの自警団「私人逮捕系YouTuber」である。彼らは「警察は頼りにならねえ」とばかりに"不逞の輩"を私的に取り締まろうとする。これは、暴力は法と適正手続の範囲内でのみ正当化されるという近代国家の原則からはみ出す、血生臭い治安維持への欲望の顕れだ。

そしてユダヤ陰謀論者が経営するSNSでは、今日もアジア系移民への反感を煽る流言飛語が飛び交っている。2011年の東日本大震災ではSNSはここまで普及していなかったが、今また同じような大震災が起きたら…と思うと、本当に真剣に対処しなければいけない問題だと思う。

www.sankei.com

ところで、『福田村事件』を観たら、是非パンフレットも買ってほしい。監督・脚本・キャストインタビューから、関東大震災前後の近代史年表、全編の脚本(!)までついた大ボリュームで、時代背景に興味を持ったら、この冊子を起点に調べていけるようになってる。作中のあるセリフは朝鮮語で語られ字幕もつかないのだが、何と言ったのか知りたい人は脚本の該当箇所を読むといいだろう。

脚本家や研究者がこの事件が郷土史や防災史の資料からも削られていると語る部分では先日の松野官房長官の「朝鮮人虐殺の事実を把握できる証拠はない」発言を思い出し、うすら寒い思いがする。

このパンフレットには9月1日に朝鮮人虐殺を題材にした漫画『追燈』を無料公開した岡田索雲先生の寄稿文も掲載されており、その中で岡田氏自身による『追燈』の「ネタばらし」がさらっと触れられている。大したことではないのだが、わたしは全然その解釈に思い至っていなくて、創作物を読み解く能力がないなあとちょっと落胆した。ぜひ、『追燈』と合わせてお楽しみを。

『福田村事件』パンフレット

*1:事件関係者の名前は映画の中では変更されている

公式GoクライアントindigoでBlueskyのAPIを使ってみた

みなさんはまだXで消耗してますか?Blueskyに移住中の小野マトペです。

Go言語でBlueskyにポストを投稿するコードを書いたのですが、ベータ版とあってドキュメントも少なくやや難儀したので、メモとして残します。記事を通じて、Blueskyのアーキテクチャのユニークさも少しだけ伝われば良いなと思います。

クライアントには、Blueskyの公式Goリポジトリ github.com/bluesky-social/indigo のクライアント実装を使います。ただし、開発中で、今後使い方が変わる可能性もあるので気をつけてください。

github.com

概要

Bluesky は、大規模分散ソーシャルアプリケーションのための汎用連合プロトコル AT Protocol 上に構築されるアプリケーション実装であるという建て付けです。AT Protocolでは、クライアントやサーバーは XRPC というHTTPベースの独自のRPCによって相互に通信します。

https://atproto.com/guides/overview

そのため、indigo 内のパッケージも、xrpc api/atproto api/bsky の階層に応じて分割されています。(AT Protocolはatproto、Blueskyはbskyと略されることが一般的です)

  • github.com/bluesky-social/indigo/xrpc
    • XRPC関連実装
  • github.com/bluesky-social/indigo/api/atproto
  • github.com/bluesky-social/indigo/api/bsky

XRPCのスキーマLexiconというスキーマ定義言語によって定義されています。github.com/bluesky-social/atproto リポジトリにあるスキーマ定義ファイルから、atprotoとbskyの各種プログラミング言語用の実装がジェネレートされます。そのGo版実装が indigoリポジトリの/apiディレクトリ にあるので、今回はこれを使うことにします。

atproto.com

install

とりあえず始めましょう。

$ go get github.com/bluesky-social/indigo

認証

なにはなくともまずは認証です。

ユーザー認証はAT Protocolレベルの操作です。 ハンドル名とパスワードをInputパラメータとして AT ProtocolレベルLexiconの atproto.ServerCreateSession を呼び出すと認証セッションが生成され、JWTなどのセッション情報が返却されます。このセッション情報を xrpc.Client 構造体の Auth フィールドに設定することで、そのClientを認証セッションと共に使うことができるようになります。

package main

import (
    "context"

    "github.com/bluesky-social/indigo/api/atproto"
    "github.com/bluesky-social/indigo/xrpc"
)

func main() {
    cli := &xrpc.Client{
        Host: "https://bsky.social",
    }

    input := &atproto.ServerCreateSession_Input{
        Identifier: "[your-bluesky-handle]",
        Password:   "[your-password]",
    }
    output, err := atproto.ServerCreateSession(context.TODO(), cli, input)
    if err != nil {
        log.Fatal(err)
    }
    cli.Auth = &xrpc.AuthInfo{
        AccessJwt:  output.AccessJwt,
        RefreshJwt: output.RefreshJwt,
        Handle:     output.Handle,
        Did:        output.Did,
    }
    _ = cli
}

プロフィール取得

セッションを開始したら、さっそくユーザーのプロフィールを取得してみましょう。

先ほどのセッション作成は AT Protocol レベルのLexiconでしたが、プロフィール取得はBlueskyレベルのLexiconなので、 github.com/bluesky-social/indigo/api/bsky パッケージをimportします。引数に私のハンドルの "matope.bsky.social" を指定します。

profile, err := bsky.ActorGetProfile(context.TODO(), cli, "matope.bsky.social")
if err != nil {
    log.Fatal(err)
}
pp.Print(profile)

profile をお好きなpretty printerでダンプすると、こんな感じの詳細プロファイルビューが取れます。

&bsky.ActorDefs_ProfileViewDetailed{
  Avatar:         &"https://cdn.bsky.social/imgproxy/0BAuv8Ek6y_0lY2sdnnPb03ZGzYSCVWLa_r9pohTqVY/rs:fill:1000:1000:1:0/plain/bafkreic4qii34cqymxz5en2m5vtff7duvdggp2ljrn2g46gc4d3zr6cv5y@jpeg",
  Banner:         &"https://cdn.bsky.social/imgproxy/Uymx6dZXSfo7fTSbfop5Xa73mOGqT-7mMftziXq0sJI/rs:fill:3000:1000:1:0/plain/bafkreiadta7acsqzwhif3amdgax63ch4cx7knrub7nxhgebvq6d6i5colq@jpeg",
  Description:    &"Software Engineer / Golang / Liberalist / ADHD診断済み  / Long-COVID療養中\nhttps://twitter.com/ono_matope",
  Did:            "did:plc:kzxl37blybhp7kvn2clme7j2",
  DisplayName:    &"小野マトペ",
  FollowersCount: &1501,
  FollowsCount:   &1157,
  Handle:         "matope.bsky.social",
  IndexedAt:      &"2023-08-16T00:26:28.486Z",
  Labels:         []*atproto.LabelDefs_Label{},
  PostsCount:     &1628,
  Viewer:         &bsky.ActorDefs_ViewerState{
    BlockedBy:   &false,
    Blocking:    (*string)(nil),
    FollowedBy:  (*string)(nil),
    Following:   (*string)(nil),
    Muted:       &false,
    MutedByList: (*bsky.GraphDefs_ListViewBasic)(nil),
  },
}.

ユーザー投稿取得

つぎはユーザー投稿の取得です。

func bsky.FeedGetAuthorFeed(ctx context.Context, c xrpc.Client, actor string, cursor string, limit int64) (bsky.FeedGetAuthorFeed_Output, error)

actor引数は、"matope.bsky.social" のようなハンドルか、またはDID(ユーザーの不変の識別子。CreateSessionActorGetProfileで取得できます)で指定します。

feed, err := bsky.FeedGetAuthorFeed(context.TODO(), cli, ”matope.bsky.social", "", 10)
if err != nil {
  return err
}

出力はこんな感じになります。

&bsky.FeedGetAuthorFeed_Output{
  Cursor: &"1692252773757::bafyreihuynzpx5457nryn5b4hllhpknl4fihgt4knhhieactxrlqc7ooyq",
  Feed:   []*bsky.FeedDefs_FeedViewPost{
    &bsky.FeedDefs_FeedViewPost{
      Post: &bsky.FeedDefs_PostView{
        LexiconTypeID: "",
        Author:        &bsky.ActorDefs_ProfileViewBasic{
          Avatar:      &"https://cdn.bsky.social/imgproxy/0BAuv8Ek6y_0lY2sdnnPb03ZGzYSCVWLa_r9pohTqVY/rs:fill:1000:1000:1:0/plain/bafkreic4qii34cqymxz5en2m5vtff7duvdggp2ljrn2g46gc4d3zr6cv5y@jpeg",
          Did:         "did:plc:kzxl37blybhp7kvn2clme7j2",
          DisplayName: &"小野マトペ",
          Handle:      "matope.bsky.social",
          Labels:      []*atproto.LabelDefs_Label{},
          Viewer:      &bsky.ActorDefs_ViewerState{
            BlockedBy:   &false,
            Blocking:    (*string)(nil),
            FollowedBy:  (*string)(nil),
            Following:   (*string)(nil),
            Muted:       &false,
            MutedByList: (*bsky.GraphDefs_ListViewBasic)(nil),
          },
        },
        Cid:       "bafyreiborxodz3x66y32744ymmh6sqmv4np5konehhp5qcjb3g4mhcbco4",
        Embed:     (*bsky.FeedDefs_PostView_Embed)(nil),
        IndexedAt: "2023-08-17T07:04:05.876Z",
        Labels:    []*atproto.LabelDefs_Label{},
        LikeCount: &0,
        Record:    &util.LexiconTypeDecoder{
          Val: &bsky.FeedPost{
            LexiconTypeID: "app.bsky.feed.post",
            CreatedAt:     "2023-08-17T07:04:05.496Z",
            Embed:         (*bsky.FeedPost_Embed)(nil),
            Entities:      []*bsky.FeedPost_Entity{},
            Facets:        []*bsky.RichtextFacet{},
            Langs:         []string{
              "ja",
            },
            Reply: (*bsky.FeedPost_ReplyRef)(nil),
            Text:  "テスト",
          },
        },
        ReplyCount:  &0,
        RepostCount: &0,
        Uri:         "at://did:plc:kzxl37blybhp7kvn2clme7j2/app.bsky.feed.post/3k5564cyinh2j",
        Viewer:      &bsky.FeedDefs_ViewerState{
          Like:   (*string)(nil),
          Repost: (*string)(nil),
        },
      },
      Reason: (*bsky.FeedDefs_FeedViewPost_Reason)(nil),
      Reply:  (*bsky.FeedDefs_ReplyRef)(nil),
    },
...

タイムライン取得

タイムラインの取得は、 bsky.FeedGetTimeline RPCメソッドを使います。

func bsky.FeedGetTimeline(ctx context.Context, c xrpc.Client, algorithm string, cursor string, limit int64) (bsky.FeedGetTimeline_Output, error)

algorithm 引数があるのが興味深いですが、とりあえず空文字で大丈夫なようです。

tl, err := bsky.FeedGetTimeline(context.TODO(), cli, "", "", 10)
if err != nil {
    return err
}

ポストの投稿

次に、Blueskyにポストを投稿してみましょう。ここまではbskyで定義されていたLexiconを使っていましたが、ポストの投稿では、atprotoレベルのLexiconを使います。

import (
    // ...
    lexutil "github.com/bluesky-social/indigo/lex/util"
)

// ...

input := &atproto.RepoCreateRecord_Input{
    Collection: "app.bsky.feed.post",
    Repo:       cli.Auth.Did, // "matope.bsky.social" のDID
    Record: &lexutil.LexiconTypeDecoder{&bsky.FeedPost{
        Val: &bsky.FeedPost{
            Text:      text,
            CreatedAt: time.Now().Format(util.ISO8601),
            Langs:     []string{"ja"},
        }},
    }},
}

resp, err := atproto.RepoCreateRecord(context.TODO(), cli, input)
if err != nil {
    return err
}
pp.Print(resp)

コード的には atproto.RepoCreateRecord 関数を使い、Input構造体に

  • Repo → ユーザーのDID
  • Collection → "app.bsky.feed.post"
  • Record → bsky.FeedPost 構造体

をセットしています。Repo、Collection、Recordといったキーワードがユニークですね。AT Protocolでは、あるユーザーの持つデータはサーバー上の、ユーザーごとのリポジトリに格納されます。リポジトリDAG-CBORというコーデックでエンコードされたレコードを保持するキーバリューデータベースです。ここでは、

matope.bsky.social リポジトリapp.bsky.feed.post コレクション以下に、(サーバサイドで決定されるタイムスタンプをキーとして)、app.bsky.feed.post スキーマに従うレコードを作成せよ」

という操作を行なっているわけです。

Langsはポスト言語の設定です。Blueskyは投稿ごとにコンテンツの言語メタデータを(複数)設定できるので、ここで設定しておきます。値は(これを入れておかないと、閲覧者のコンテンツ言語と異なる場合に "Translate this post" リンクが出てしまう)

作成したRecordの識別子情報が出力されました。(ここではCIDとURIが何であるかには踏み込みません。私がよく分かっていないからですが…)

&atproto.RepoCreateRecord_Output{
  Cid: "bafyreiffpevc6ew665lqhocz2226q2runa7ubpwqzipszm7moy76zm4mja",
  Uri: "at://did:plc:kzxl37blybhp7kvn2clme7j2/app.bsky.feed.post/3k52rbvnqfg2b",
}.

いいね

上で作成した投稿にいいねをつけてみましょう。下のコードをよく見てください。先ほとど同じ、 atproto.RepoCreateRecord RPCメソッドを呼び出していて、また、 Collection の指定が app.bsky.feed.like になり、 Record の中身が bsky.FeedLike 型になっています。このRecordのSubjectに、先ほど得られたCIDとURIをコピペします。

func like(cli *xrpc.Client) error {
    input := &atproto.RepoCreateRecord_Input{
        Collection: "app.bsky.feed.like",
        Repo:       cli.Auth.Did,
        Record: &lexutil.LexiconTypeDecoder{
            Val: &bsky.FeedLike{
                CreatedAt: time.Now().Format(util.ISO8601),
                Subject: &atproto.RepoStrongRef{
                    Uri: "at://did:plc:kzxl37blybhp7kvn2clme7j2/app.bsky.feed.post/3k52rbvnqfg2b",
                    Cid: "bafyreiffpevc6ew665lqhocz2226q2runa7ubpwqzipszm7moy76zm4mja",
                },
            },
        },
    }
    out, err := atproto.RepoCreateRecord(context.TODO(), cli, input)
    if err != nil {
        return err
    }
    pp.Print(out)

いいねができました。

セルフいいね

つまり、投稿も、いいねも、AT Protocolとしてはリポジトリ上のレコード作成という点で等価であり、違いはパス(コレクション+キー)とデータ定義だけなのです。ちなみに、フォローも同じくRepoCreateRecord Lexiconで、app.bsky.graph.follow コレクションへのレコードを作成します。

  • 投稿
    • コレクション: app.bksy.feed.post
    • スキーマ: bsky.FeedPost
  • いいね
    • コレクション: app.bksy.feed.like
    • スキーマ: bsky.FeedLike
  • フォロー
    • コレクション: app.bsky.graph.follow
    • スキーマ: bsky.GraphFollow

いかがでしたか?このように、分散レポジトリ/コレクション/レコードの階層的データ構造によってATProtocolがプラットフォームとしての拡張性・相互運用性を実現していることの一端をご理解いただけたのではないでしょうか。それではよいブルスコライフを。

カメラ初心者がカメラ沼に入ってみた。(長文)

会社を辞めたので、旅行なんかしたいと思ったことないのだけど、海外旅行をしてみようと思いたち、衝動的にExpediaとAirbnbでバリ島への航空券と宿泊先を予約した(出発8日前のチケットとか、取れるんですね)。で、どうせ海外行くならいい機会だからちゃんとしたカメラ買って風景とか撮りたいなとなり、以前からうっすら欲しかったカメラを買うことにした。私と同じカメラわからん人の助けになるように、購入に至るまでの検討過程をここに記す。また参考にしたWebサイトなども可能な限り添付していく。何を買ったかだけ知りたい人は末尾までスクロールしてくれ。ちなみに今これを書いている時点でカメラを箱から出していない。旅行までに練習する時間があるだろうか…。というか旅行の準備なにもしてない…。

やりたいこと

  • 旅先で写真やムービーを撮りたい
  • 日常でカメラを持ち歩いて、面白い構図とか撮りたい。
  • いい感じにボケてる写真とか撮りたい。
  • 予算は最大でコミコミ20万まで出してもいいかもしれないけど、できれば10万円前後に抑えたい。

ボディ要件

やりたいことから導き出される要件はこんな感じ

コンパクトであること

旅にせよ日常にせよ、「サッと取り出して取る」性は確保しておきたい。ガジェットの軽さは心の軽さに繋がる。あくまで日常の延長に写真の撮影を組み込みたいのだが、一眼レフやフルサイズミラーレスのように大きなカメラは、構えた瞬間に「カメラマン」になってしまうような気がして気後れしてしまう。そのため、今回は上級者向けであるフルサイズのカメラは除外し、マイクロフォーサーズAPS-Cのミラーレスとした。

関係ないけど、一見そうは見えない小さなガジェットで大きなパフォーマンスを発揮するガジェットが私は好きだ。そこらへんは私がゲーミングPCとして Intel NUCを好んだのと似ているかもしれない。

matope.hatenablog.com

ムービー撮影機能が強力であること(特に4K動画)

せっかく高品位レンズとセンサーを買うので、必要とあらばムービーにも使いたい。日常ではムービーの撮影機会はなかなか無いかもしれないが、少なくとも旅では歩きながら撮ったりもするだろう。そしてそのムービーはいい感じでなくてはならない(いい感じじゃなくて良いならスマホ使った方が便利なので)。

4K録画であることはそれほど必須ではないが、撮れるに越したことはない。

www.youtube.com

強力な手ぶれ補正

デジカメの手ぶれ補正は、写真を撮るときには単なる暗所でのブレ防止というだけでなく、一部の機種では夜景を手持ちだけで撮れるなど、表現の広がりをになう。これは特に旅行では使いどころがありそうである。

5軸手ぶれ補正 E-M5 Mark II | デジタル一眼カメラ OM-D | オリンパス

また、旅でのムービー撮影は歩きながらの撮影が想定されるが、その歩行による見苦しい手ブレを取り除くことはムービーを「いい感じ」にする上で極めて重要であろう。デジカメの手ブレ補正には、ボディ内手ぶれ補正レンズ内手ぶれ補正、そしてその両方を組み合わせた方式がある。一般的にはレンズ内手ブレ補正のみのものは手ブレ補正の効果が少ないようだ。

手ぶれ補正の効果については以下の動画が分かりやすい。

レンズ内手ぶれ補正のみのα6400の手ブレを検証する動画

自撮り対応

ムービー撮影では自分を被写体にするケースが想定されるので、構図の確認のために自撮りに対応しているとうれしい。デジカメにおける自撮り対応とは、液晶ファインダーが縦(180度チルト)か横(バリアングル)かに180度回転するもののことを言う。

バリアングル液晶とチルト液晶の違い - フォトスク

hoboshuhu.com

hoboshuhu.com

防塵防滴

デジタルカメラは極めて繊細なガジェットで、旅先での急な雨や砂浜などでの扱いは注意を要すると聞く。一部の機種は「防塵防滴」を謳っており、ラフな扱いが許されるようだ。それは良いのでチェック項目に追加した。

第一ラウンド:ネット調査

そう言うわけで、「コンパクト」「4K動画撮影機能」「自撮り対応」「手ぶれ補正機能の充実」を中心に見ていきたい。とはいえカメラ全然わからんので、この辺から読み始めて、出てくる機種の価格コムと公式ページを回ってスペックを確認していった。

キヤノン

EOS Kiss M

キヤノン:EOS Kiss M|概要

  • コンパクト:本体重量 351g(軽量)
  • 4K動画:対応
  • 自撮り:対応(バリアングル液晶)
  • 手ぶれ補正:レンズ内手ぶれ補正のみ
  • 防塵防滴:非対応

キヤノン EOS KISS シリーズ初のミラーレス EOS Kiss M。EF-M規格レンズの選択肢が少ないことに否定的な意見が多いが、それは自分に問題になるかは分からなかった。店頭で触ってみた感じは、操作がわかりやすくて好印象だった。しかし、手ぶれ補正が「レンズ内手ぶれ補正」のみで弱かったり 、動画撮影中のAFが弱いらしいというレビューがあったので除外した。

ニコン

ヨドバシの売り場でニコンの機種は全て大きくて重かったので「プロ用なのかな」と考えあまり調査せずスルーした。

ソニー

ソニーのαシリーズは差別化のために機能を出し惜しみしているような…。

オリンパス

オリンパスのレンズはパナソニックとともにマイクロフォーサーズ規格を採用しており、選択肢が多いらしい。また、強力なボディ内手ぶれ補正による暗所や夜景の撮影能力をアピールする機種が多くみられた。

OM-D E-M10 Mark III

OM-D E-M10 Mark III | デジタル一眼カメラ OM-D | オリンパス

  • コンパクト: 本体重量 362g(軽量)
  • 手ぶれ補正:強力なボディ内手ぶれ補正。
  • 4K動画:対応(4K@30p)
  • 自撮り:非対応
  • 防塵防滴:非対応

E-M10はコンパクトだし4Kも撮れるけど自撮りができないので除外。惜しい。

OM-D E-M5 Mark II

OM-D E-M5 Mark II | デジタル一眼カメラ OM-D | オリンパス

  • コンパクト: 本体重量 417g(そこそこ軽量)
  • 手ぶれ補正:強力なボディ内手ぶれ補正
  • 自撮り:対応(バリアングル式)
  • 4K動画:非対応
  • 防塵防滴:対応

E-M5 Mk2 は自撮りができるのに4K動画に対応していないのが惜しい。しかし、オリンパス的には動画専用に強化された手ブレ補正やAF機能から「シネマクオリティ」と推しているので、4K撮影こそできないものの「動画の品質」自体には信頼をおけそうだ。公式の作例動画も良さそうである。この機種はすぐに後継機が出るという噂があり、そちらでは4Kに対応するのではないか。私は旅行が来週なので待てないが…。

www.youtube.com

参考にしたレビュー

dent-sweden.com

dc.watch.impress.co.jp

www.youtube.com

www.youtube.com

パナソニック

オリンパスとならんでマイクロフォーサーズ陣営のパナソニックオリンパスはスチルに強いが、パナソニックは動画につよいという定評があるらしい。

LUMIX DC-GF10/GF90/GF9

DC-GF10/GF90 | Gシリーズ 一眼カメラ | デジタルカメラ LUMIX(ルミックス) | Panasonic

  • コンパクト:本体重量 269g(とても軽量)
  • 自撮り:対応(180度チルト)
  • 手ぶれ補正:なし
  • 4K動画:対応
  • 防塵防滴:非対応

ちいさくてかわいい入門機。キットにパンケーキレンズとかついてる。かわいいのに4K動画も取れたりする。USB充電機能とかもうれしい。しかし手ブレ補正はついていないので除外。他の機種にはだいたいついている、スマホと連携してGPS情報を設定する機能がないのも旅の記録としては不便かもしれない。

LUMIX DC-GX7 Mark III

DC-GX7MK3 | Gシリーズ 一眼カメラ | デジタルカメラ LUMIX(ルミックス) | Panasonic

  • コンパクト:本体重量 407g(そこそこ軽量)
  • 自撮り:非対応
  • 手ぶれ補正:ボディ内蔵
  • 4K動画:対応(4k@30p)
  • 防塵防滴:非対応

良さそうなカメラだが自撮り非対応なので除外。あと外部マイク端子がないようだ。ムービーとして使うなら、やはり小型でもいいのでマイクは装着しておきたいところだ。

www.youtube.com

新製品レビュー:Panasonic LUMIX GX7 Mark III (外観・機能編) - デジカメ Watch

LUMIX DMC-G8

DMC-G8|デジタルカメラ LUMIX(ルミックス)|Panasonic

  • コンパクト:本体重量 453g (ちょっと重く大きい)
  • 4K動画:対応(4K@30p)
  • 自撮り:対応(バリアングル式)
  • 手ぶれ補正:ボディ内蔵+レンズ内蔵式
  • 防塵防滴:対応

4K動画、バリアングル、強力な手ぶれ補正と機能スペックが揃ったが、コンパクトさの観点ではでちょっと尻込みしてしまう。しかし有力候補だ。「生活の面白い視点を切り取る」がやや薄れ、「お仕事のムービーを撮影する」に近づいていく、のをどこまで許容するか、である。

www.youtube.com

LUMIX DC-G9 PRO

DC-G9 | Gシリーズ 一眼カメラ | デジタルカメラ LUMIX(ルミックス) | Panasonic

  • 4K動画:対応(4K@60p)
  • 自撮り:対応(バリアングル式)
  • 手ぶれ補正:ボディ内蔵+レンズ内蔵式
  • 防塵防滴:対応
  • 総重量:本体重量 586g(重い)

4K@60p対応かつ自撮り対応と、動画機能のかなりの充実を感じる。LUMIXマイクロフォーサーズカメラとしては最上位機種である。機能的には申し分ないが、いかんせん旅に持って出るには大きく、重い。価格的にもボディのみでおよそ12万と、予算オーバーだ。

www.youtube.com

LUMIX DC-G99

DC-G99 | Gシリーズ 一眼カメラ | デジタルカメラ LUMIX(ルミックス) | Panasonic

  • 4K動画:対応(4K@60p)
  • 自撮り:対応
  • 手ぶれ補正:ボディ内蔵
  • 防塵防滴:対応
  • 総重量:484 g(やや重い)

G8とG9 PROの後継機らしい。実物は見ずにG9の後継機なら重いだろうとスルーしてしまったのだが、今見ると重量はG8に近く、後述する瞳AFの改良も入っており、検討に値する機種だったかもしれない。しかし、いずれにせよG8の時点でサイズに気後れしてはいたし、中古価格もこなれておらず、レンズキットで13万からあるとはいえキットレンズに不満がある(後述)ので、やはり予算オーバーではあった。

e-m-wonderful.com

第一ラウンド総評

上記に挙げた機種のうち、まず「タッチAF」「内蔵手ブレ補正」は必須と考え、これらを装備していない機種を除外していった。その結果…「小さい」「自撮り可能」「4K動画撮影可能」の三つの要件を同時に満たすカメラが、なんと存在しないことがわかった。トリレンマ!

オリンパスOM-D E-M10 Mark III オリンパス OM-D E-M5 Mark II パナソニック LUMIX DC-G7 Mark III パナソニック LUMIX DCM-G8
小型軽量
自撮り × ×
4K動画 ×
防塵防滴 × ×

そのため3要件のうち、一つを諦めて二つを選ぶことで購入する製品を確定させる必要が出てきた。

もっとも優先しておきたい要件は「自撮り可能」だったので、まず E-M10 Mark III と DC-G7 Mark III が脱落した。ムービーで自分を被写体にできないと不便、というのは想像しやすい。次に優先したい要件は「コンパクトさ」で、もっとも優先度が低いのは4K動画の撮影能力なので、「小さい+自撮り可能」のE-M5 Mark IIで確定…かと思われたが、スペック上のサイズ・重量の差が体感として許容可能であれば、4K動画が撮影できるG8 LUMIXの方が魅力的な選択肢だ。ここから先は実際に実機を触ってみないと判断がつかないだろう。

店頭でのインプレッション

そんなわけで、E-M5 Mark II と LUMIX DCM-G8 まで絞り込んだうえで、Twitterで教えてもらった中野のフジヤカメラ本店で店員さんに相談のうえ、機種を決定することにした。フジヤカメラさんは新品・中古のカメラを扱うお店で、初心者だと言うと店員さんがとても丁寧に教えてくれる。「F値ってなんですか?」とか「ダイヤルのAとかPとかって何ですか?え、Aってオートだと思ってました…」「パンケーキレンズとかどうなんですか」とか色々教えてもらった。

で、E-M5 Mark II と LUMIX G8 の中古品を触らせてもらった。

  • フォームファクタ
    • たしかにG8の方が少し大きいが、両者を持ち比べてみるとE-M5の方は中身が詰まっている感じでズッシリした感じ。しかし、重量に関しては標準ズームレンズを装着してしまうと殆ど関係なくなると感じた。ガジェットのコンパクトさを維持するには短い単焦点レンズが必要なようだ。
    • G8はグリップがついてる分確実にホールドしやすいと感じた。大きいのも悪いことばかりではない。しかし、自撮りすると、グリップが逆向きになるのでとても持ちにくいという問題が発生した。(やはり歩き自撮りはスマホの領分だろうか…)
    • E-M5の瞳AFの方が強力なようだ。自撮りモードにした時、E-M5はすぐに瞳を認識して私にフォーカスを合わせてくれたが、G8はうまくフォーカスしてくれず、わたわたとタッチAFで調整しなくてはいけなかった。自撮り中はカメラを持ちにくいので、オートフォーカスの信頼性が重要なことがわかった。G9やG99では瞳AFが強化されているようだ。
  • 手ぶれ補正は、明確にオリンパスの方が優れているとのことだ。本体のみで超強力というのが重要らしい。パナソニックはレンズ内手ぶれ補正に対応したレンズを選ぶ必要ができてしまう。
  • 4K動画はまだまだ観る環境も整ってないし、そこはフルHDで妥協しても良さそう。

以上のインプレッションから、 E-M5 Mark IIに決定した

レンズ編

さて、ここで話は終わらない。一緒に使うレンズを決めないといけない。わからないので、とりあえずGCM-G8とE-M5 Mark II のキットレンズを起点に説明してもらった。二つのカメラはどちらもマイクロフォーサーズなので、どれを選んでも良い。

標準ズームレンズ

LUMIX G VARIO 12-60mm / F3.5-5.6 ASPH/ POWER O.I.S.

G8のキットレンズ。いわゆる標準ズームレンズ。防塵防滴。ズームレンズとしてはサイズが小さいのが魅力。レンズ内手ぶれ補正あり。一本で35mm換算で広角側24mm相当から望遠側120mm相当までカバーできる(マイクロフォーサーズ焦点距離は、2倍すると一般的な画角の数字になる)。しかし、F3.5-F5.6というのは、結構暗くてボケが作れないらしい。ボケを作るには、明るい=F値が小さいレンズを選ばないといけない。一回帰って価格コムでネットで作例を見たが、確かにVARIOの作例は他と比べて霞がかかったような印象があり、正直「こんな写真が撮りたい!」と心がときめくものはあまり見かけなかった(プロが撮ったものはさすがに良いのだが、アマチュアの作品の方が参考になるだろう)。ときめきは大事なのである。

M.ZUIKO DIGITAL ED 12-40mm/F2.8

EM-5 Mark IIのキットレンズ。同じく防塵防滴の標準ズームレンズだ。EM-5のレンズなので、手ぶれ補正は実装されていないことに注意。望遠側が80mm相当までと、上の12-60(120mm相当まで)より少し少ないが、覗かせてもらうとほとんど分からない程度の差だった。サイズが12-60より一回り大きい点を敬遠したが、しかしF2.8通し(望遠側まで全部F2.8)というF値の小ささからくる画面の明るさ、価格コムで作例を見ても明らかに優れていた。これはときめく。

家に帰って、似たスペックのパナライカ12-60/2.8-4.0も検討したが、F2.8通しの方が良い、と判断してM.ZUIKO DIGITAL ED12-40/F2.8に決定した。

sims-lab.com

追記: id:DocSeri さんの記事がとても参考になったのだった。 docseri.hatenablog.jp

単焦点レンズ

標準ズームレンズがあれば広角から望遠までカバーできるが、やや大きいのがネックだ。単焦点レンズはズームができない代わりに小さく、日常的にカメラを持ち出してスナップを撮るには適している。また、F値が小さく明るく撮れる(F1.4〜1.8とかある)特徴がある。一つくらい持っていてもバチは当たらなさそうである。

単焦点レンズはズームができないので、まず自分が一番撮りたい画角を理解しておく必要がある。下記サイトが参考になった。

logcamera.com

検討の結果、自分がカッコいいなー、こういうスナップ撮りたいなー、と思う写真は50mmっぽい雰囲気があるように思った。作品として締まっている感じがする。あと35mmとかだとあんまりボケを作って楽しむ感じじゃなさそうだし。俺はボケが作りたいんだ!スマホじゃできないから!

と言うわけで比較対象としたのが以下の3製品である。評価メモは全て価格コムの作例のみで判断している。

  • M.ZUIKO DIGITAL 25mm F1.8
    • 3万円くらい。個人的に好きなボケ味
  • LUMIX G 25mm/F1.7 ASPH
  • LEICA DG SUMMILUX 25mm/F1.4 ASPH.
    • 4万円くらい。いわゆるパナライカ。F1.4ですんごいボケる。とろけるようなボケ味…ほしい…

と、これもここまで絞り込んでからフジヤカメラで触らせてもらった。ZUIKOはかなり小さく、同じオリンパスのE-M5 Mark IIとデザインの親和性が高かった。パナライカも触らせてもらった。中古価格差は小さかったのだが、やや大きめであることと、カメラ内部の機構のせいでAFが駆動するたびにカチャカチャと耳障りな音がするため諦め、今回は M.ZUIKO DIGITAL 25mm F1.8 に決定した。

パナライカの方がやや明るく撮れるしボケの綺麗さも上ですが、まあ相当厳密に比較しないと違いは分からないですね、とのことであった。

お買い上げ

はいというわけで、購入したのは E-M5 Mark II + M.ZUIKO DIGITAL ED 12-40mm/F2.8 レンズキット(新品)と、M.ZUIKO DIGITAL 25mm F1.8 単焦点レンズ(中古)の組み合わせでした。

本体キットは中古で買ってもよかったのだけど、価格差が10%程度だったのと、バッテリーは消耗品なので不安があったこと、フラッシュの欠品もあったため新品に。中古でもお店の半年保証はつきます。単焦点は中古でコストを抑えました。

周辺機器含めたコストを書いておきますね。液晶保護フィルムは店員さんに熟練の技で貼ってもらいました。

品目 価格
本体キット ¥101,400
単焦点レンズ ¥24,840
レンズプロテクター(ズームレンズ用) ¥2,680
プロテクター(単焦点レンズ用) ¥1,680
SDカード(64GB) ¥1,680
予備バッテリー ¥5,630
液晶保護フィルム ¥840
合計 ¥138,750

今回Twitterレンズ沼の人たちに色々教えてもらったり、店員さんにみっちり教えてもらったり、いい沼や…となりました。ありがとうございます。楽しみます。

ほめられた。やったぜ。

gorilla/csrf で安全なWebフォームを作る

こんにちは。GoでWeb開発していますか?私はしていません。Goに限らず、既成のWebアプリケーションフレームワークを使わずに自前でWebフォームを作る場合、なにも考えずに書くと CSRF (Cross Site Request Forgery) 脆弱性を作りこみ、不正なユーザー操作を実行されてしまう可能性があります。

ダメな例

例えば以下のGoコードで作成されるフォームにはCSRF脆弱性があります。SubmitSignupForm ハンドラは、受け取ったリクエストが自分のサイト上のフォームからサブミットされたものかチェックしていないので、攻撃者が他のサイト上のフォームを使い、第三者のユーザーのブラウザで任意の操作を実行させることができてしまいます。

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/signup", ShowSignupForm)
    r.HandleFunc("/signup/post", SubmitSignupForm).Methods("POST")
    http.ListenAndServe(":8080", r)
}

func ShowSignupForm(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, `
<html>
  <body>
      <form method="POST" action="/signup/post" accept-charset="UTF-8">
          <input type="text" name="name">
          <input type="text" name="email">
          <input type="submit" value="Sign up!">
      </form>
  </body>
</html>`)
}

func SubmitSignupForm(w http.ResponseWriter, r *http.Request) {
    _ = r.ParseForm()
    fmt.Fprintf(w, "%v\n", r.PostForm)
    // TODO: ユーザー登録
}

CSRFについては「はまちちゃん事件」が有名です。mixiにログイン済みのユーザーにCSRF脆弱性のあったmixiの発言投稿用URLリンクを踏ませることで、ユーザーの意図しない発言を投稿させたというものです。CSRF対策にはいくつかの手法がありますが、権威あるセキュリティ団体であるOWASPが各手法の解説とメリット・デメリットを網羅しています。詳しく知りたい方はそちらを参照してください。

github.com

gorilla/csrf

信頼できるWebアプリケーションフレームワークではCSRFへの対策実装され、デフォルトで有効になっていますが、Goの net/http はWAFではないので、別途WAFを導入しないのであれば、自分でセキュリティ対策を導入する必要があります。そこで、今回は gorilla/csrf を使ってCSRF対策を実装します。

github.com

gorilla/csrf は net/http だけでなく、Echo や Gin といった人気のWAFと協調して動作することを目的としたCSRF対策パッケージです。

Cookieの二重送信(gorillaの場合)

gorilla/csrf が採用する対策手法は、OWASP の分類で言えば "Double Submit Cookie" (Cookieの二重送信) に分類されます。 Double Submit Cookieは、要点だけ言えばセッション開始時に暗号的に強い乱数でトークンを生成しておき、それをブラウザのCookieとフォームのhidden input要素の両方に送信するというものです。もちろん、サブミット時にはサーバーでCookieとフォームのトークンの一致を検証し、一致しなければエラーとします。この手法は、トークン情報をステートとして保持する必要がないため、実装・運用コストが低いのが特徴です。

一方、Cookieの二重送信には問題点も指摘されています。

要約すると、一定条件のもとではCookieを不正に書き換えることは可能であり、その場合に Double Submit Cookie の安全性は成立しないというものです。これについて、gorilla/csrfCookie に署名つき暗号化を施すことで回避しています。OWASPチートシートにも、Cookieの暗号化はうまく動作すると書かれています。

Including the token in an encrypted cookie - often within the authentication cookie - and then at the server side matching it (after decrypting authentication cookie) with the token in hidden form field or parameter/header for ajax calls mitigates both the issues mentioned above. This works because a sub domain has no way to over-write an properly crafted encrypted cookie without the necessary information such as encryption key.

また、gorilla/csrf では、リクエスト時に Unique-per-Request な one time padを生成し、CookieのtokenをXORでマスクしたものをフォームに送信する工夫を加えることで、BREACH攻撃を緩和しています。

プレーンな Double Submit Cookie の弱点を回避して、概ね安全なように思います。強いていえばユーザー自身が外部のWebページにフォームを送信してしまえば偽のフォームを作成可能ですが、そのケースはほぼ意図的な漏洩ですし、そこまで守るかどうかはポリシー次第(それも防ぎたかったらパスワード入力を求めるとか)じゃないでしょうか。

gorilla/csrfCSRF対策の流れを紹介します。

初回訪問時

最初にユーザーがWebサイトに訪れたとき、サーバーは暗号的に強い乱数生成器から basetoken を乱数生成します。そして basetoken を auth_key で署名つき暗号化したものを、レスポンスの _gorilla_csrf Cookie に格納します。この暗号は署名つき暗号なので、改竄に対する耐性があります。

f:id:ono_matope:20190605011412p:plain
初回訪問時

フォーム表示時

次に、サーバーが _gorilla_csrf Cookie を持ったクライアントにフォームを表示する際、サーバーはまず OTP (one-time-pad) を乱数生成します。この OTPと、basetokenをOTPでマスクした結果を結合し、CSRFトークンを生成します( csrfToken = OTP + XOR(basetoken, OTP) )。このCSRFトークンは、フォームのhidden inputとしてユーザーにレスポンスされます。

f:id:ono_matope:20190605092057p:plain
フォーム表示時

サブミット時

ユーザーがフォームをサブミットしてきたとき、サーバーはフォームのCSRFトークンからOTPを取り出し、Cookieのbasetokenを使って先ほどと同じ操作( OTP + XOR(basetoken, OTP)) をおこない、その結果がフォームのCSRFトークンと一致するか確かめることで、リクエストが正しく要求されたものか判別します。

f:id:ono_matope:20190605102640p:plain
サブミット時

How to use gorilla/csrf

さて、原理はわかったので使ってみましょう。

package main

import (
    "fmt"
    "html/template"
    "net/http"

    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/signup", ShowSignupForm)
    r.HandleFunc("/signup/post", SubmitSignupForm).Methods("POST")

    // Protect ミドルウェアは、非冪等なHTTPメソッドの場合にサブミットされたフォーム
    // をチェックし、CSRFトークンが一致しているか検証する。
    //
    // auth-key はコードに書き込まず、/dev/urandom したものを外部から与えること。
    // キーが変わるとそれまでに発行したトークンの検証に失敗する。
    //
    // HTTP な開発環境では opt に csrf.Secure(false) 指定が必要。本番では外すこと。
    h := csrf.Protect([]byte("32-byte-long-auth-key"), csrf.Secure(false))(r)
    http.ListenAndServe(":8080", h)
}

func ShowSignupForm(w http.ResponseWriter, r *http.Request) {

    // {{.CSRFField}} にCSRFトークンの隠しinputを埋め込む。
    t, _ := template.New("form").Parse(`
<html>
  <body>
      <form method="POST" action="/signup/post" accept-charset="UTF-8">
          <input type="text" name="name">
          <input type="text" name="email">
          {{.CSRFField}}
          <input type="submit" value="Sign up!">
      </form>
  </body>
</html> `)
    t.Execute(w, map[string]interface{}{
        "CSRFField": csrf.TemplateField(r),
    })
}

func SubmitSignupForm(w http.ResponseWriter, r *http.Request) {
    _ = r.ParseForm()
    fmt.Fprintf(w, "%v\n", r.PostForm)
    // TODO: ユーザー登録
}

csrf.ProtectCSRF保護機能を提供するHTTPミドルウェアを作成します。このミドルウェアは、POSTなどの非安全リクエストを受けた時にリクエストボディのフォームを自動的にチェックし、CSRFトークンが適格であれば次のハンドラを実行し、適格でなければエラー画面を表示して終了します。そのため、アプリケーション側のHTTPハンドラは(サブミットについては)CSRF検証済みであることを前提に開発することができます。OWASPチートシートにも、セキュリティ対策で怖いのはうっかりミスなので自動的な対策をせよと書かれているので、この仕様は歓迎できます。

一方、csrf.TemplateField(r *http.Request) template.HTML は、リクエストのCSRFトークンのhidden inputタグをリターンします。これをtemplateで置換してHTMLフォームを表示すると、gorilla.csrf.Token という名前の 隠し inputタグが埋め込まれます。

f:id:ono_matope:20190605144134p:plain
出力されるHTMLフォーム

同時に、Cookieにも _gorilla_csrf が記録されているのがわかります(内容は暗号化されているので確認も改竄もできません)。

f:id:ono_matope:20190605142544p:plain
Cookie

そこで、わざとフォームの値を書き換えてからSubmitすると、ちゃんとエラーになります。

f:id:ono_matope:20190605141731p:plain
シンプルなエラー画面

ところで、csrf.Protectはトークン不一致に簡単なエラー画面を出力するのですが、オプションでカスタムのエラーハンドラを指定させることができます。以下のように、何らかのセキュリティ対応を取れるようなログを落とすと良いと思います。

   h := csrf.Protect([]byte("32-byte-long-auth-key"),
        csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            log.Println("CSRF攻撃の疑いのあるリクエストが発行されました")
            fmt.Fprintf(w, "ニャーン (%s)", csrf.FailureReason(r).Error())
        })),
        csrf.Secure(false),
    )(r)

f:id:ono_matope:20190605142904p:plain
にゃーん

以上です。

外付け(として使う)SSDって今何買えばいいのか調べたメモ

昨日の記事で言ったようにうっかりSSD+ケースを買ってしまったのだが、最近はUSB3.1 Gen2もThunderbolt3も普及してきてデータ転送帯域がどんどん広がっているし、m.2?NVMe?とかもなんかちっちゃくてかっこいいし、SSDも価格爆下がり中って聞くし、「最近の外付けSSDはどうなの?」みたいなところをちょっと調べてみた。買ってから調べるという。容量は1TBを前提に調べた。

外付けSSDについての調査だが、単品のSSDにケースを調達して外付けSSDに自作する、ということを選択肢に入れて調べたのでほとんど内蔵SSDについてです。あとNANDとかTLCとかには触れません。

まず、SSDの製品を分類するには、サイズ規格インターフェイスに注目する必要がある。

サイズ規格は以下のふたつがある。

覚えておく必要があるインターフェイスも二つだけだ。

  • SATA3.0
  • PCI-Express (PCIe 3.0)
    • 1レーンで転送速度8Gbps (1GB/s). 最大4レーン32Gbps (4GB/s)での転送が可能
    • コントローラのプロトコルはNVMe。NVMeとはPCIe上でブロックデバイスを効率よく動作させるために導入されたプロトコルである。

つまり、SSD市場には、以下の3カテゴリが存在していることになる。

外付け化のためのケーブルインターフェイス

PCIeはSATAの6倍以上でるのかーすごいなという感じだが、私は私のMacに外付けして使いたいので、つなげるやつをさがす。

2.5インチ + SATA3.0用のUSB 3.1 Gen2変換ケース

Amazonで大量に見つかる。どれも2,000円から3,000円程度と、安価。

Amazon.co.jp: USB3.1 gen2 ssd 2.5 sata ケース: パソコン・周辺機器ストア

m.2 + SATA3.0用のUSB 3.1 Gen2 変換ケース

m.2の変換ケースは、「Bキー、Mキー」などサポートするカードに相性があるので注意。

m.2 + PCIe3.0x4変換ケース

Thunderbolt3-PCIe3.0変換ケース

変わり種で、Thunderbolt3-PCIe3.0変換ケースというのもあった。ただ、書き込み性能の表記がないがレビューによると900MB/sしかでないらしい。

外付けSSD製品

USB接続の外付けSSD製品は、基本的にSATA3.0構成のようで、USB 3.1のGen1を搭載した安価なものとGen2を搭載した高価なものに二極化している。 Gen1の製品はリード性能300MB/s〜400MB/sでGB単価が14円/GBから、Gen2の製品はリード性能が560MB/sでGB単価が23円から、となっているようだ。

kakaku.com

フォームファクタに関しては、実はいまは外付けSSDは2.5インチ製品は少なくなっており、ほとんどがコンパクトなm.2仕様のようだ。2.5インチ: 価格.com - インターフェイス:USBのSSD 人気売れ筋ランキング (規格サイズ:2.5インチ)

考察・あなたが何を買うべきか

関連するパフォーマンス指標を書き起こしてみた。まず、PCにUSBで接続したい場合、データ転送速度はUSBの転送速度である500MB/s(USB 3.1 Gen1)や1GB/s(USB 3.1 Gen2)で律速する。この場合、ドライブにUSBの転送速度を超えたあまり高性能なものを買っても活かせないことに注意。

f:id:ono_matope:20190503055433p:plain

その上で、獲得したい転送速度に応じて最適な購入ルートを考えたみた。

転送速度を気にしない人向け (Gen1コース)

外付けSSDのGen1のものなら1,3000円/1TBくらいで買えるので、転送速度が300MB/s〜400MB/sでいいよという人はわざわざ内蔵ドライブとケースで自作せず、普通に外付けを買うといいと思います。

転送速度は560MB/sくらいはほしいという人(SATA+Gen2コース)

単価の安いSATA SSDに、安いUSB 3.1 Gen2-SATA変換ケースをつけると手頃に560MB/s帯のポータブルSSDになる。Gen2の製品は外付けドライブだと23円/GB程度するが、以下の構成は15GB/円で作れて嬉しい。

代表構成: 12,339 + 2,999 = 15,338円 / 1TB。WDのSSDTranscendのケースとのセットバージョンもある。

パッケージ品で良さそうなのはこれとか。でも23,763円/1TB。丈夫にできてそうってメリットはある。

転送速度は1000MB/sほしいよという人向け(PCIe+Gen2コース)

わかる。MacBook Pro 2018の内蔵SSDの転送速度が3,200MB/sに対して、300MB/sや500MB/sではなにか物足りない。もうちょっとハイスペックを楽しみたい。そんなあなたは m.2/PCIe3x4のSSDに、USB 3.1 Gen2変換ケースをの組み合わせで1000MB/sなポータブルストレージを作りましょう。既製品の外付けドライブではこの選択肢はそもそも存在しないのかな。ちょっと見つかんなかったです。

代表構成:¥15,980 + ¥5,680 = 21,660円 / 1TB

Crucial SSD M.2 1000GB P1シリーズ Type2280 PCIe3.0x4 NVMe 5年保証 CT1000P1SSD8JP

Crucial SSD M.2 1000GB P1シリーズ Type2280 PCIe3.0x4 NVMe 5年保証 CT1000P1SSD8JP

USB-C付属バージョン

akiba-pc.watch.impress.co.jp

金に糸目をつけない人向け (Thunderbolt 3プロ向け機材コース)

Thunderbolt3 SSD変換ケースとか、

そもそもThunderbolt3 SSDとか買うといいんじゃないでしょうか。

blog.livedoor.jp

聞いたことないメーカーだけど3万円で 1,600MB/s でる謎の Thunderbolt3 1TB SSD とかある