【Golang】OpenPGP 公開鍵の Notation 情報を取得する

> Go 言語（以下 golang）で OpenPGP の公開鍵から、ノーテーション情報（注釈情報以下 `Notation`）を取り出したい。

[Keyoxide](https://keyoxide.org/) 用の `Proof` を、公開鍵から golang で取得したかったのです。

:::note info
Go 本家の `crypto` モジュールにある `openpgp` パッケージは[廃止された](https://pkg.go.dev/golang.org/x/crypto/openpgp)ため、[ProtonMail](github.com/ProtonMail) がフォーク&メンテしている [`go-crypto`](https://pkg.go.dev/github.com/ProtonMail/go-crypto) から、**`openpgp` だけをスピンオフさせた [GopenPGP](https://gopenpgp.org/) を利用**しています。
:::

### GopenPGP v3

```go
package main

import (
	"fmt"
	"os"
	"time"

	"github.com/ProtonMail/gopenpgp/v3/crypto"
)

func main() {
    // 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)
	}
}
```

:::note warn
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 を利用しており、こちらでは直接プロパティを参照しています。

```go
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
}

```

https://pkg.go.dev/github.com/ProtonMail/gopenpgp/v3/crypto

https://pkg.go.dev/github.com/ProtonMail/go-crypto

https://www.rfc-editor.org/rfc/rfc4880#section-5.2.3.16

https://github.com/ProtonMail/gopenpgp/issues/268