JavaScript
Node.js
SSH
SSL
ContentsSecurityPolicy
More than 1 year has passed since last update.

はじめに

本記事をAdvent Calenderに捧げます。

herokuでnodejsのプログラムをうpしようと画策し、いろいろと学んだお話。
基礎的なセキュリティ知識なんかはIPAがいろいろ教えてくれるのでそちらを参照するべし。
安全なウェブサイトの作り方

本記事では、SSLやSSHやObservatory by Mozillaのことを主体で取り上げる。

SSHとSSLと鍵の形式

ECC > RSA

SSLもSSHも、鍵に関してのアルゴリズムで共通する部分がある。いくつかある中で現在よく使われているのは素因数アルゴリズムのRSAと楕円曲線アルゴリズムのECCである。
RSAの場合、計算方法が構築されている都合相当な長さの暗号bitが必要になり、セキュリティbit比でそれは非常に膨大な値になる。セキュリティbit256に対してRSAは15360bitの長さが必要であり、この長さのRSAを使用するのは、特に回数の膨れるSSL処理の計算面であまりにも大きなハンディになる。(筆者の手元でSSL実装したサーバーに15360bitの鍵を使って試験した結果、2048bit鍵に比較してパフォーマンスが激烈に低下した)
ならば同等の強度を少ないbit数で得られ、より計算速度の早い楕円暗号を使うほうが効率的である。これでセキュリティビット256相当の硬さを得るには521bitあれば十分で、短い分だけRSAよりもはるかに計算量が少なくて済み、その分速度が確保される。
もっとも、現状楕円曲線暗号なら256bitでもRSAの3072bitと同等程度の堅さがあり、速度面でも十分なのでそこで止めるのが一般的。

ばっくどあ?

その昔OpenSSLにはこの楕円暗号のアルゴリズムに関し、fips対応の煽りで米国政府用のバックドアがあったとかなかったとか言われているが、話題自体が3年前であり、その仕様自体が2015年に削除されているため、現状は塞がれていると判断してよいだろう。

Curve25519

そんなバックドアの心配から注目を集めたのがCurve25519という方式で、わずか128bitであるにも関わらず穴の少ないアルゴリズムなので、強固かつ高速で処理可能である。
サーバー側のバックグラウンドのOpenSSL versionが1.1.0以降で、秘密鍵作成を楕円曲線暗号のパラメータであるprime256v1で設定した場合、ECDHE-ECDSAで始まるCipher suiteのときに、ECDHEの暗号アルゴリズムがGoogleでも使われているCurve25519にあたる、X25519方式に代わる。これはSSHにおいてEd25519鍵として定義できるもの。

なおOpenSSLは秘密鍵作成をx25519で行えるが、SSLの証明書用に使用することはできない。
これは、証明書の署名に使う秘密鍵は交換用の鍵で行う必要があり、x25519ではこの部分にかみ合わないため。

Cipher suite

SSLにはCipher suiteという、どの部分でどの暗号をどう使うかという決まり事があり、例えばECDHE-ECDSA-AES128-GCM-SHA256あたりで示される。
これは、「証明書認証の公開鍵はECDSA方式で、公開鍵のやり取りにはECDHE方式の共通鍵を使って、データボディをAES-128で、GCM方式の暗号ブロックモードで行う。それらの検証をSHA256で行う」というもの。

何のことやらという話だが、一つずつ。

ECDHE……前方秘匿性と呼ばれる部分。次に書かれているECDSAという形式の公開鍵をやり取りするための鍵で、共通鍵暗号と呼ばれる。DHEとかDSAというのは従来の暗号方式で、これを楕円曲線で強化したものと考えるとよい。
これによってパフォーマンスを低下させずに鍵を堅牢化させている。
先に説明したCurve25519はここで使用されている。

ECDSA……SSL証明書の署名と検証に使われる公開鍵。DSAという従来の方式を以下略して行う。オレオレ証明書を作るときに色々吐き出されるファイルで使うのがこれ。
ここにはDSA用の256~521bitの楕円曲線暗号が使われる。521bitは誤植のように見えるが合っている。

AES128……その昔使われていたDES式に代わって使われている方式。効率的な計算方法がないので、128bitでもRSAの8192bit相当に硬い。鍵ではなくデータ本体の暗号を行う役割を持つ。

GCM……データ本体の長さは不定で、通常のやり方では暗号化できないので、一定のブロック単位に切り分けながら暗号化していくための方法。

SHA256……データが間違いないものか、発信元から発信されているもので正しいかどうかを保証する部分。
よくネットでアプリをダウンロードしようとした場合に、一緒にSHA形式のフィンガープリントが並んでいたりする。
これは、このソフトが間違いなく公式のソフトであるという保証をするためのデータで、例えばNodejsのサイトでNodejsのソフトを入手しようとした場合に「リリースファイルのSHASUM署名」というリンクから見ることのできるのがそれ。

ちなみにこのCipher-suiteは新しい形式で、古いブラウザやスマホ、ガラケだと読めない可能性があるので、RSA鍵も持たせたりなどして幅をもたせる必要がある。それらはサーバーによってやり方が異なるので、それはまたそれぞれで調べてみるとよい。

前方秘匿性の大切さ

このCipher-suite、前方秘匿性にECDHEではなくECのないDHEとか、それこそ秘匿しないままのやり取りもできる。
これは前方秘匿性の計算の分だけ処理が重くなったり、古いブラウザへの対処によるものであるが、脆弱なDHEは非常に危険である。公開鍵をむき出しでやり取りするなど論外である。
前方秘匿は安全に公開鍵をやり取りするためのものなので、絶対に欠かすことはできない。

公開鍵

この形式は前述しているので省略。

最近実装された

つまり?

Githubに登録するSSH鍵も3072bit以上のRSA鍵か、256bit以上のECDSA鍵なりed25519鍵なりにしておけば確実に自分の余命よりも長い間堅牢な鍵を確保可能。

SSLはRSA3072bitだとパフォーマンス的な問題もあるので現状2048bitのままに留めるか、256bit以上のECDSA鍵を作って署名する。
opensslの秘密鍵作成は、ecparamオプションでprime256v1を指定すること推奨。

参考:
GitHubユーザーのSSH鍵6万個を調べてみた
お前らのSSH Keysの作り方は間違っている
Apache 2.4系でHTTP/2対応サーバを構築してみるテスト。
Curve25519(wikipedia英語版)

セキュリティチェックサイト

フロントエンドJavascriptでなぜビルドが必要か。
それは別にリクエストを縮めるためだけではなく、Polyfillの適応やContentsSecurityPolicy的な意味で重要だから。

自作のflowerKnightGirlのスペチケ選択スクリプトに関し、現状はobservatory by mozillaA+がもらえるように直しているが、それでも一番上の項目にある"Contents Security Policy"でunsafe-evalを入れている件を指摘されている。
これはVueをビルドせずに使う場合に必要だからと、VueのChrome拡張が教えてくれる。
また、unsafe-inlineについてのエラーも指摘されているが、これは単にbabelによるpolyfill動作を読み込みスクリプトに対してオンブラウザで行う挙動にする都合で発生している問題である。その点は、script-srcにhashコードを入れることによって補っているわけだが、これもbuildでpolyfillを適応してあればこのような面倒は発生しない。
(なお、F12でスペチケ選択スクリプトの裏側を見るとCSPでブロックしました表示が出るが、faviconとかrobotsとかのブラウザが欲しがっているデータを読めないためのもので、現verにおいて問題はないため気にしない)

Observatoryの点数をよくするように、発行ヘッダでいろいろと必要項目を埋めていくのだが、expressを使うならhelmetミドルウェアで簡単に設定可能。
なお、expressはデフォだとx-powerd-byでexpress使用を自己主張するため、wapparyzerにフレームワークに紐付きでサーバー言語まで解析されてしまう。こういった自己主張はもしバージョンがらみで脆弱性の問題があった場合はもちろん、フレームワークなり言語なり専用のクラック攻撃手段を構築され、それに衝かれる危険性があるので、隠せるところは隠すべき。

Observatory by Mozillaとは

Mozillaが作成しているチェックサイトで、Mozillaがセキュアであるという条件に対し、そのサイトがどれくらい守っているかというのをチェックしてくれる。A+〜Fまであり、何も対策していなければまず0点でF評価、すべて対策すると130点満点。100点以上でA+評価がもらえる。
今は多くのサイトで全http化が行われているのだが、実はhttpと入力して繋いだ場合にhttpsへリダイレクトせずにそのままhttpで表示してしまうサイトがまだまだ相当数ある。
これはObservatoryの評価の真ん中辺にあるHSTSとHTTPS Redirectに相当し、これをやっておくことでF評価をC〜B評価に持っていける。
HSTSは予めhttpsで接続してあれば、ブラウザにスキーマ省略で入力した場合にキャッシュからhttpsを自動選択してくれるようになるヘッダ設定、Redirectはそのままhttpへの接続をhttps接続にリダイレクトする設定。どちらもそれほど難しくないやり方ですぐに可能になるので、やっておくだけで点数が上がる。
問題はContent Security Policyであるが、ざっくり言うと、「配信コンテンツの種類ごとに、どの場所からのみの配信を可能にするか」という設定で、ようは生成元同一性が保たれているかどうかをブラウザが判断できるようになっているかどうかというもの。
これが正しく設定してあれば、少なくともブラウザ上の動作においての不正を防ぐことができ、特にXSSに対して強い対策となりえる。

詳しい解説はMDNやgoogleやそのほか詳しい説明をしているページがあるのでそちらを参考に。

MDNのCSPページ
GoogleのCSPページ
Contents Security Policyのお勉強

Node.js上のコード

セキュアな部分は上記の通りexpressのhelmetで簡単に実装可能。
helmet()でもそこそこ強化されるが、フロントエンドの実装もそこそこな静的コンテンツ限定でもない限り、必ずサイトによって環境が変わってくるはずなので、きちんと設定してやる。

実行Node.jsバージョンは現行LTSの8.9.1。
以下は、実際に使用しているスクリプトの抜粋である。

secure.js
//他require記述あり。
const helmet = require('helmet');
const app = require('./app');

//app.jsでexpressを定義してある。

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: [
      "'none'"
    ],
    scriptSrc: [
      "'self'",
      "'unsafe-eval'",
      "'unsafe-inline'",
      'https://cdnjs.cloudflare.com',
      "'sha256-D8zfByJ1/tPsnjVkvmxj79eAZUPIMfY755xDpM9mDzM='"
    ],
    frameAncestors: [
      "'none'"
    ],
    objectSrc: [
      "'none'"
    ],
    baseUri: [
      "'self'"
    ],
    imgSrc: [
      "'self'"
    ],
    styleSrc: [
      "'self'"
    ],
    connectSrc: [
      "'self'"
    ],
    fontSrc: [
      "'self'"
    ]
  }
  }
}));

app.use(helmet.frameguard({
  action: 'deny'
}));

app.use(helmet.hsts({
  maxAge: 63072000,
  includeSubDomain: true,
  preload: true
}));

app.use(helmet.noSniff());
app.use(helmet.xssFilter());
app.use(helmet.referrerPolicy({
  policy: 'no-referrer'
}));
app.disable('x-powered-by');

app.use((req, res, next) => {
  if (req.header('x-forwarded-proto') !== 'https' && req.hostname === 'special-ticket-afn.herokuapp.com') {
    res.redirect(301, 'https://special-ticket-afn.herokuapp.com/');
    return;
  }

//...記述抜粋。
}

herokuの場合、httpのリダイレクトはreq.headerでx-fowarded-protoをたどることで実現する。
hostname条件をつけているのはローカルテストではない場合を振り分けるため。
includeSubDomainはのちのち独自ドメイン化することを想定した実装である。

終わりに

実際、セキュリティチェックサイト自体はそれぞれの基準があるのでいろいろなところを渡り歩いてみて、それを自分の提供するサービスに組み込んだ技術と照らし合わせて取捨選択する必要がある。
その上でプログラムの構造上における脆弱性の可能性を精査し取り除いていくことになる。

Node.jsに触れるまで、フロントのフレームワークをいろいろ追っていた身としては、こういった部分への配慮が大事な点でサーバーサイドもまた捨てがたいと感じた。
そのあたりの面白さに触れて、Node.js以外のサーバーサイド言語もまた学んで形にしていくのも良さそうと感じました。