Go 言語(以下 golang)で OpenPGP の公開鍵から、ノーテーション情報(注釈情報以下
Notation
)を取り出したい。
Keyoxide 用の Proof
を、公開鍵から golang で取得したかったのです。
Go 本家の crypto
モジュールにある openpgp
パッケージは廃止されたため、ProtonMail がフォーク&メンテしている go-crypto
から、openpgp
だけをスピンオフさせた GopenPGP を利用しています。
GopenPGP v3
package main
import (
"fmt"
"os"
"time"
"github.com/ProtonMail/gopenpgp/v3/crypto"
)
func main(pathFilePubKey string) {
// OpenPGP 互換の公開鍵。ASCII Armored, バイナリ形式どちらでもOK
pathFilePubKey := "path/to/pgppubkey.asc"
// 公開鍵ファイル読込
f, err := os.Open(pathFilePubKey)
panicOnErr(err)
defer f.Close()
// gopenpgp v3 で公開鍵パース
pubKey, err := crypto.NewKeyFromReader(f)
panicOnErr(err)
fmt.Println("* Fingerprint:", pubKey.GetFingerprint())
for _, val := range pubKey.GetEntity().Identities {
fmt.Println("* Identity Name:", val.Name)
sigs, err := val.Primary.PrimarySelfSignature(time.Now().Add(300*time.Second), nil)
panicOnErr(err)
fmt.Println("* Notations:")
for _, notation := range sigs.Notations {
// 自動生成されるsaltはスキップ
if notation.Name == "salt@notations.openpgpjs.org" {
fmt.Printf("- Key(Name): %s, Value: [random value]\n", notation.Name)
continue
}
// 人間可読ならそのまま
if notation.IsHumanReadable {
fmt.Printf("- Key(Name): %s, Value: %s\n", notation.Name, notation.Value)
} else {
// そうでなければ16進数で表示
fmt.Printf("- Key(Name): %s, Value: %X\n", notation.Name, notation.Value)
}
}
}
}
func panicOnErr(err error) {
if err != nil {
panic(err)
}
}
GopenPGP v3 は、GopenPGP v2 と互換がありません。
v3 は、今後のメンテナンス性を向上するため、仕様の見直しや大幅なリファクタリングが行われました。そのため使い勝手も、いささか v2 と異なります。
特に、構造体のプロパティ(struct
のフィールド)の多くが private
になったことで露出度が減り、セキュリティの懸念事項が少なくなったり、メンテナンスしやすくなった反面、以前はアクセスできていたプロパティにアクセスできなくなっています。
これから GopenPGP モジュールを扱うなら v3 を使うべきですが、以前から v2 を使っており v3 を利用できない事情がある場合は、下記 GopenPGP v2 のサンプルをご覧ください。
GopenPGP v2
上記 GopenPGP v3 と同じ内容を GopenPGP v2 で出力する例です。
v3 と v2 は互換がないとは言え、v3 のコードとほとんど同じなのは、v3 側が内部的に v2 を利用しており、こちらでは直接プロパティを参照しています。
package main
import (
"fmt"
"os"
cryptoV2 "github.com/ProtonMail/gopenpgp/v2/crypto"
)
func main(pathFilePubKey string) {
// OpenPGP 互換の公開鍵。ASCII Armored, バイナリ形式どちらでもOK
pathFilePubKey := "path/to/pgppubkey.asc"
// 公開鍵ファイル読込
f, err := os.Open(pathFilePubKey)
panicOnErr(err)
defer f.Close()
var pubKey *cryptoV2.Key
// 鍵の形式を検知して gopenpgp v2 でパース
if r, isArmored := IsPGPArmored(f); isArmored {
pubKey, err = cryptoV2.NewKeyFromArmoredReader(r)
panicOnErr(err)
} else {
pubKey, err = cryptoV2.NewKeyFromReader(f)
panicOnErr(err)
}
fmt.Println("* Fingerprint:", pubKey.GetFingerprint())
for _, val := range pubKey.GetEntity().Identities {
fmt.Println("- Identity Name:", val.Name)
fmt.Println("* Notations:")
for _, notation := range val.SelfSignature.Notations {
// 自動生成されるsaltはスキップ
if notation.Name == "salt@notations.openpgpjs.org" {
fmt.Printf("- Key(Name): %s, Value: [random value]\n", notation.Name)
continue
}
// 可読ならそのまま、そうでなければ16進数で表示
if notation.IsHumanReadable {
fmt.Printf("- Key(Name): %s, Value: %s\n", notation.Name, notation.Value)
} else {
fmt.Printf("- Key(Name): %s, Value: %X\n", notation.Name, notation.Value)
}
}
}
}
func panicOnErr(err error) {
if err != nil {
panic(err)
}
}
// IsPGPArmored reads a prefix from the reader and checks if it is equal to a
// pgp armored message prefix.
//
// Returns an io.Reader that is reset to the state of the in reader, and a bool
// that indicates if there is a match. If reading from the reader fails, the
// returned bool is set to false.
//
// This function was taken from:
// - https://github.com/ProtonMail/gopenpgp/blob/b138a82ed0527abd91d1252be35166e45babcfae/armor/armor.go#L175
// - Copyright (c) 2019 Proton Technologies AG, MIT License
func IsPGPArmored(in io.Reader) (io.Reader, bool) {
const armorPrefix = "-----BEGIN PGP"
const maxGarbageBytes = 128
buffer := make([]byte, len(armorPrefix)+maxGarbageBytes)
n, _ := io.ReadFull(in, buffer)
outReader := io.MultiReader(bytes.NewReader(buffer[:n]), in)
if bytes.Contains(buffer[:n], []byte(armorPrefix)) {
return outReader, true
}
return outReader, false
}