JSのWeb Cryptoで生成したECDHのJWKとGo言語でのJWKを共有して共通の鍵のバイト列を生成する
やりたいこと
デモ
https://gyazo.com/382499270a35f065d43c444344b5b888
上記のコピペでJWKを貼り付けている部分をネットワーク経由で行う感じを想定している。
JavaScript側のコード
code:js
(async () => {
const aKeyPair = await window.crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, 'deriveKey', 'deriveBits'); const aPublicKeyJWK = await crypto.subtle.exportKey('jwk', aKeyPair.publicKey);
const jwtStrInGo = prompt("ECDH Public JWK", "");
alert(JSON.stringify(aPublicKeyJWK));
const bRemotePublicKey = await crypto.subtle.importKey('jwk', JSON.parse(jwtStrInGo), { name: 'ECDH', namedCurve: 'P-256' }, false, []);
const aKey = await crypto.subtle.deriveBits({ name: 'ECDH', public: bRemotePublicKey }, aKeyPair.privateKey, 256);
return shared bytes: ${new Uint8Array(aKey)};
})();
Go側のコード
code:go
package main
import (
"bufio"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"github.com/lestrrat-go/jwx/jwk"
"os"
)
func jsonStrToPublicKey(jsonStr string) (*ecdsa.PublicKey, error) {
set, err := jwk.Parse([]byte(jsonStr))
if err != nil {
return nil, err
}
key, b := set.Get(0)
if !b {
return nil, errors.New("key not found")
}
var publicKey ecdsa.PublicKey
if err := key.Raw(&publicKey); err != nil {
return nil, err
}
return &publicKey, nil
}
func main() {
privInGo, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
jwkInGo, err := jwk.New(privInGo.PublicKey)
if err != nil {
panic(err)
}
jwkBufInGo, err := json.Marshal(jwkInGo)
if err != nil {
panic(err)
}
fmt.Printf("public JWK in JS:\n%s\n", string(jwkBufInGo))
fmt.Print("ECDH Public JWK: ")
jwkStrInJs, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
panic(err)
}
pubInJs, err := jsonStrToPublicKey(jwkStrInJs)
if err != nil {
panic(err)
}
// Derive shared bytes
x, _ := pubInJs.Curve.ScalarMult(pubInJs.X, pubInJs.Y, privInGo.D.Bytes())
fmt.Println("shared bytes", x.Bytes())
}
hr.icon
code:go
package main
import (
"bufio"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math/big"
"os"
)
type ecdhPublicJwk struct {
Crv string json:"crv"
Kty string json:"kty"
X string json:"x"
Y string json:"y"
}
// parse json to jwk struct
func jwkStringToEcdhJwk(jwkStr string) (*ecdhPublicJwk, error) {
var jwk ecdhPublicJwk
err := json.Unmarshal([]byte(jwkStr), &jwk)
if err != nil {
return nil, err
}
return &jwk, nil
}
// convert bytes to big.Int
func bytesToBigInt(bytes []byte) *big.Int {
i := new(big.Int)
i.SetBytes(bytes)
return i
}
// convert jwk struct to public key
func ecdhJwkToPublicKey(ecdhJwk *ecdhPublicJwk) (*ecdsa.PublicKey, error) {
if ecdhJwk.Crv != "P-256" {
return nil, errors.New("curve is not P-256")
}
xBytes, err := base64.RawURLEncoding.DecodeString(ecdhJwk.X)
if err != nil {
return nil, err
}
yBytes, err := base64.RawURLEncoding.DecodeString(ecdhJwk.Y)
if err != nil {
return nil, err
}
return &ecdsa.PublicKey{
Curve: elliptic.P256(),
X: bytesToBigInt(xBytes),
Y: bytesToBigInt(yBytes),
}, nil
}
func publicKeyToJwk(key *ecdsa.PublicKey) (string, error) {
if key.Curve != elliptic.P256() {
return "", errors.New("curve is not P-256")
}
jsonBytes, err := json.Marshal(ecdhPublicJwk{
Crv: "P-256",
Kty: "EC",
X: base64.RawURLEncoding.EncodeToString(key.X.Bytes()),
Y: base64.RawURLEncoding.EncodeToString(key.Y.Bytes()),
})
if err != nil {
return "", err
}
return string(jsonBytes), nil
}
func main() {
privInGo, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
jwkStrInGo, err := publicKeyToJwk(&privInGo.PublicKey)
if err != nil {
panic(err)
}
fmt.Printf("public JWK in JS:\n%s\n", jwkStrInGo)
fmt.Print("ECDH Public JWK: ")
jwkStrInJs, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
panic(err)
}
ecdhJwkInJs, err := jwkStringToEcdhJwk(jwkStrInJs)
if err != nil {
panic(err)
}
pubInJs, err := ecdhJwkToPublicKey(ecdhJwkInJs)
if err != nil {
panic(err)
}
// Derive shared bytes
x, _ := pubInJs.Curve.ScalarMult(pubInJs.X, pubInJs.Y, privInGo.D.Bytes())
fmt.Println("shared bytes", x.Bytes())
}
Web Cryptoと違ってGoはJWKの標準の実装がなかったことと、「JWKの文字列 ⇄ ecdsa.PublicKey」の相互変換ライブラリがすぐに見当たらなかったのもあり、JWKの相互変換部分は上記に全て実装が書かれている(必要なECDHのP-256のみ)。良さそうなJWKのライブラリがあれば置き換えたい。 また、Go標準で共有された公開鍵と自分の秘密鍵で共有の鍵のバイト列を生成する関数がなかった。そのため、上記の.ScalarMult()でバイト列の導出をしている。こういうのは標準ライブラリに用意されていると安心できるがなさそう。