Go + RFC 8219 Message Encryption for Web Push

[RFC 8219 Message Encryption for Web Push](https://tools.ietf.org/html/rfc8291) 理解のため読みつつ Go での Application Server 側の実装をなぞる。

[実装コード](https://github.com/nokamoto/webpush-101/pull/8)

参考にしたコードは以下。

- [gauntface/web-push-go](https://github.com/gauntface/web-push-go) は aes128gcm が実装されているがアーカイブされている
- [SherClockHolmes/webpush-go](https://github.com/SherClockHolmes/webpush-go) は aesgcm のみで aes128gcm が実装されていない :question:
  - aesgcm は [draft04](https://tools.ietf.org/html/draft-ietf-webpush-encryption-04) 時のもので古いが現時点での PushService 側での対応具合は要調査

## [Encryption Summary](https://tools.ietf.org/html/rfc8291#section-3.4)

以下を実装し、必要なキーを生成する必要がある。

```
      -- For an application server:
      ecdh_secret = ECDH(as_private, ua_public)
      auth_secret = <from user agent>
      salt = random(16)

      -- For both:

      ## Use HKDF to combine the ECDH and authentication secrets
      # HKDF-Extract(salt=auth_secret, IKM=ecdh_secret)
      PRK_key = HMAC-SHA-256(auth_secret, ecdh_secret)
      # HKDF-Expand(PRK_key, key_info, L_key=32)
      key_info = "WebPush: info" || 0x00 || ua_public || as_public
      IKM = HMAC-SHA-256(PRK_key, key_info || 0x01)

      ## HKDF calculations from RFC 8188
      # HKDF-Extract(salt, IKM)
      PRK = HMAC-SHA-256(salt, IKM)
      # HKDF-Expand(PRK, cek_info, L_cek=16)
      cek_info = "Content-Encoding: aes128gcm" || 0x00
      CEK = HMAC-SHA-256(PRK, cek_info || 0x01)[0..15]
      # HKDF-Expand(PRK, nonce_info, L_nonce=12)
      nonce_info = "Content-Encoding: nonce" || 0x00
      NONCE = HMAC-SHA-256(PRK, nonce_info || 0x01)[0..11]
```

[Push Message Encryption Example](https://tools.ietf.org/html/rfc8291#section-5) と [Appendix A.  Intermediate Values for Encryption](https://tools.ietf.org/html/rfc8291#appendix-A) により、それぞれの値を再現するテストを書くことが可能。

### ecdh_secret = ECDH(as_private, ua_public)

[Diffie-Hellman Key Agreement](https://tools.ietf.org/html/rfc8291#section-3.1) を参照。

```go
var curve = elliptic.P256()

func newApplicationServerKeys() (*applicationServerKeys, error) {
	priv, x, y, err := elliptic.GenerateKey(curve, rand.Reader)
	if err != nil {
		return nil, err
	}
	pub := elliptic.Marshal(curve, x, y)
	return &applicationServerKeys{private: priv, public: pub}, nil
}
```

まず `as_private` をとして使うため ApplicationServer のキーペアを作成する。
ここで出来た公開鍵は後で `keyid` に入れる。

```go
func newSharedSecret(as *applicationServerKeys, p256dh []byte) ([]byte, error) {
	x, y := elliptic.Unmarshal(curve, p256dh)
	if x == nil {
		return nil, errors.New("elliptic.Unmarshal failed")
	}
	secret, _ := curve.ScalarMult(x, y, as.private)
	return secret.Bytes(), nil
}

func TestEncrypt_ecdh_secret(t *testing.T) {
	expected := "kyrL1jIIOHEzg3sM2ZWRHDRB62YACZhhSlknJ672kSs"

	actual, _ := newSharedSecret(as(), uaPublic)
	if s := encode(actual); s != expected {
		t.Fatalf("ecdh_secret expected %s but actual %s", expected, s)
	}
}
```

`ua_public` つまり UserAgent の公開鍵（p256dh として PushSubscription から得られる）と `as_private` を合わせると shared secret が出来上がる（ここら辺はたぶん ECDH のドキュメントに書いてあるが読んでいないので理解していない）。

### auth_secret = \<from user agent\>
UserAgent から PushSubscription を取得した際に得られる `auth` が必要となる。

### salt = random(16)
ランダムな 16 バイト。

### IKM = HMAC-SHA-256(PRK_key, key_info || 0x01)

[Combining Shared and Authentication Secrets](https://tools.ietf.org/html/rfc8291#section-3.3) を参照。

```go
func newKeyInfo(ua, as []byte) []byte {
	prefix := []byte("WebPush: info\x00")
	bytes := make([]byte, len(prefix) + len(ua) + len(as))
	n := copy(bytes, prefix)
	n += copy(bytes[n:], ua)
	copy(bytes[n:], as)
	return bytes
}

func TestEncrypt_key_info(t *testing.T) {
	expected := "V2ViUHVzaDogaW5mbwAEJXGyvs3942BVGq8e0PTNNmwRzr5VX4m8t7GGpTM5FzFo7OLr4BhZe9MEebhuPI-OztV3ylkYfpJGmQ22ggCLDgT-M_SrDepxkU21WCP3O1SUj0EwbZIHMtu5pZpTKGSCIA5Zent7wmC6HCJ5mFgJkuk5cwAvMBKiiujwa7t45ewP"

	actual := newKeyInfo(uaPublic, as().public)
	if s := encode(actual); s != expected {
		t.Fatalf("key_info expected %s but actual %s", expected, s)
	}
}
```

info として上記のバイトを生成する。
`as_public` は上で生成した ApplicationServer の公開鍵を指定する。

```go
func newIkm(ua, as, auth, ecdh []byte) ([]byte, error) {
	keyInfo := newKeyInfo(ua, as)
	return _hkdf(ecdh, auth, keyInfo, 32)
}

func TestEncrypt_ikm(t *testing.T) {
	expected := "S4lYMb_L0FxCeq0WhDx813KgSYqU26kOyzWUdsXYyrg"

	sharedSecret, _ := newSharedSecret(as(), uaPublic)

	ikm, _ := newIkm(uaPublic, as().public, authSecret, sharedSecret)
	if s := encode(ikm); s != expected {
		t.Fatalf("ikm expected %s but actual %s", expected, s)
	}
}
```

IKM は `auth_secret`, `ecdh_secret`, `key_info` を HMAC-based key derivation function(HKDF) を利用して合わせる。

### CEK = HMAC-SHA-256(PRK, cek_info || 0x01)[0..15]

CEK (Content Encryption Key) を生成する。

```go
var cekInfo = []byte("Content-Encoding: aes128gcm\x00")

func TestEncrypt_cek_info(t *testing.T) {
	expected := "Q29udGVudC1FbmNvZGluZzogYWVzMTI4Z2NtAA"

	if s := encode(cekInfo); s != expected {
		t.Fatalf("cek_info expected %s but actual %s", expected, s)
	}
}

func newCek(salt, ikm []byte) ([]byte, error) {
	return _hkdf(ikm, salt, cekInfo, 16)
}

func TestEncrypt_cek(t *testing.T) {
	expected := "oIhVW04MRdy2XN9CiKLxTg"

	sharedSecret, _ := newSharedSecret(as(), uaPublic)
	ikm, _ := newIkm(uaPublic, as().public, authSecret, sharedSecret)

	cek, _ := newCek(salt, ikm)
	if s := encode(cek); s != expected {
		t.Fatalf("cek expected %s but actual %s", expected, s)
	}
}
```

CEK は `ikm` と `salt` を HKDF で合わせる。

### NONCE = HMAC-SHA-256(PRK, nonce_info || 0x01)[0..11]

```go
var nonceInfo = []byte("Content-Encoding: nonce\x00")

func TestEncrypt_nonce_info(t *testing.T) {
	expected := "Q29udGVudC1FbmNvZGluZzogbm9uY2UA"

	if s := encode(nonceInfo); s != expected {
		t.Fatalf("nonce_info expected %s but actual %s", expected, s)
	}
}

func newNonce(salt, ikm []byte) ([]byte, error) {
	return _hkdf(ikm, salt, nonceInfo, 12)
}

func TestEncrypt_nonce(t *testing.T) {
	expected := "4h_95klXJ5E_qnoN"

	sharedSecret, _ := newSharedSecret(as(), uaPublic)
	ikm, _ := newIkm(uaPublic, as().public, authSecret, sharedSecret)
	
	nonce, _ := newNonce(salt, ikm)
	if s := encode(nonce); s != expected {
		t.Fatalf("nonce expected %s but actual %s", expected, s)
	}
}
```

## [The "aes128gcm" HTTP Content Coding](https://tools.ietf.org/html/rfc8188#section-2)

aes128gcm は [RFC8188 Encrypted Content-Encoding for HTTP](https://tools.ietf.org/html/rfc8188) に書かれている。
Webpush Encryption における制限は [Restrictions on Use of "aes128gcm" Content Coding](https://tools.ietf.org/html/rfc8291#section-4) にある。

```go
func newCiphertext(plaintext string, cek, nonce []byte) ([]byte, error) {
	b := []byte(plaintext)
	b = append(b, 0x02)

	block, err := aes.NewCipher(cek)
	if err != nil {
		return nil, err
	}

	aesgcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}

	return aesgcm.Seal(nil, nonce, b, nil), nil
}

func TestEncrypt_ciphertext(t *testing.T) {
	expected := "8pfeW0KbunFT06SuDKoJH9Ql87S1QUrdirN6GcG7sFz1y1sqLgVi1VhjVkHsUoEsbI_0LpXMuGvnzQ"

	sharedSecret, _ := newSharedSecret(as(), uaPublic)
	ikm, _ := newIkm(uaPublic, as().public, authSecret, sharedSecret)
	cek, _ := newCek(salt, ikm)
	nonce, _ := newNonce(salt, ikm)

	ciphertext, err := newCiphertext(body, cek, nonce)
	if err != nil {
		t.Fatal(err)
	}
	if s := encode(ciphertext); s != expected {
		t.Fatalf("ciphertext expected %s but actual %s", expected, s)
	}
}
```

`cek` と `nonce` を利用して暗号化する。
他に 0x02 を padding delimiter として利用する、 plaintext が 3993 octets までであるなどが rfc から読み取れる。

```go
func newHeader(salt, keyid []byte) []byte {
	var b []byte

	b = append(b, salt...)

	rs := make([]byte, 4)
	binary.BigEndian.PutUint32(rs, 4096)
	b = append(b, rs...)

	b = append(b, byte(len(keyid)))

	b = append(b, keyid...)

	return b
}

func TestEncrypt_header(t *testing.T) {
	expected := "DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8"

	header := newHeader(salt, as().public)
	if s := encode(header); s != expected {
		t.Fatalf("header expected %s but actual %s", expected, s)
	}
}
```

`rs` については、

> An application server MUST set the "rs" parameter in the "aes128gcm" content coding header to a size that is greater than the sum of the lengths of the plaintext, the padding delimiter (1 octet), any padding, and the authentication tag (16 octets).

とあるので正確である必要はないのかもしれない（Example は 4096 になっている）。

`keyid` として前述した ApplicationServer の公開鍵を指定する。