Go openpgp 참고 사항

Go 패키지 중에 golang.org/x/crypto/openpgp에서 PGP encrypt/decrypt 사용 팁이다.

openpgp 패키지

Go로 프로그래밍 중에 PGP encrypt/decrypt 할 일이 생겨서 (기존에 Java로 구현했을 때에는 Bouncy Castle을 이용했었음) 관련 패키지를 찾아보니 golang.org/x/crypto/openpgp 패키지가 있었다.
이 패키지는 간단해서 사용하기가 좋았는데, encrypt/decrypt 시 약간의 문제가 발생하여 이에 대한 해결 팁을 기록한다.

PGP encrypt 예제

아래는 openpgp 패키지를 이용하여 내가 입력 plain 파일을 PGP encrypt 하는 코드를 구현한 소스이다. (아래 소스에서 publicKey 변수에는 실제로 사용할 PGP public key를 넣어야 함, PGP decrypt도 유사하므로 본 글에서는 생략)

package main
import (
    "bytes"
    "fmt"
    "io"
    "os"
    "golang.org/x/crypto/openpgp"
)
const publicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----

-----END PGP PUBLIC KEY BLOCK-----`

func main() {
    if len(os.Args) < 2 {
        fmt.Println("No file name to be encrypted")
        return
    }

    // PGP encrypt 할 PGP 파일 이름을 얻는다.
    fileToEnc := os.Args[1]
    clearFile, err := os.Open(fileToEnc)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer clearFile.Close()

    // Encrypt 해서 저장할 파일을 생성한다.
    pgpFile, err := os.Create(fileToEnc + ".pgp")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer pgpFile.Close()

    // 수신자들의 public key를 읽는다.
    publicKeys := []string{publicKey}
    entityList, err := readPublicKeys(publicKeys)
    if err != nil {
        fmt.Println("Fail to read public key")
        return
    }

    // PGP 파일을 encrypt 하여 파일에 저장한다.
    encrypt(entityList, clearFile, pgpFile)
}

// 입력 PGP key 들을 읽어서 EntityList로 리턴한다.
func readPublicKeys(keys []string) (openpgp.EntityList, error) {
    var entityLists openpgp.EntityList
    for _, key := range keys {
        entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(key))
        if err != nil {
            return entityList, err
        }
        entityLists = append(entityLists, entityList[0])
    }
    return entityLists, nil
}

// 입력 clear 파일을 입력 수신자 키로 encrypt 하여 PGP 출력 파일을 생성한다.
func encrypt(entityList openpgp.EntityList, r io.Reader, w io.Writer) error {
    // PGP encrypt 한다.
    wc, err := openpgp.Encrypt(w, entityList, signer, &openpgp.FileHints{IsBinary: true}, nil)
    if err != nil {
        fmt.Println(err)
        return err
    }

    // 파일에 write 한다.
    if _, err := io.Copy(wc, r); err != nil {
        fmt.Println(err)
        return err
    }
    return wc.Close()
}

PGP encrypt가 실패하는 경우

그런데 위 소스로 PGP encrypt를 테스트 해 보면, 특정인의 경우에는 encrypt 시에 아래 메시지를 출력하면서 실패하였다.

openpgp: invalid argument: cannot encrypt because recipient set shares no common algorithms

또는

openpgp: invalid argument: cannot encrypt because no candidate hash functions are compiled in. (Wanted RIPEMD160 in this case.)

실패하는 경우를 살펴보니, PGP key가 2010년 이전에 많이 사용되던 DSA 알고리즘으로 ELG 암호를 사용하는 경우였다.

그래서 openpgp 패키지의 write.go 파일에서 Encrypt() 함수를 확인해 보니 아래와 같이 되어 있었다.

candidateHashes := []uint8{
    hashToHashId(crypto.SHA256),
    hashToHashId(crypto.SHA384),
    hashToHashId(crypto.SHA512),
    hashToHashId(crypto.SHA1),
    hashToHashId(crypto.RIPEMD160),
}

에러 메시지 중에서 (Wanted RIPEMD160 in this case.) 내용을 참조하여 이 부분을 아래와 같이 순서를 조정하였더니, 더이상 PGP encrypt가 실패하지 않고 성공하였다.

candidateHashes := []uint8{
    hashToHashId(crypto.SHA256),
    hashToHashId(crypto.SHA384),
    hashToHashId(crypto.SHA512),
    hashToHashId(crypto.RIPEMD160),
    hashToHashId(crypto.SHA1),
}

🚩 그런데 위 방법은 openpgp 패키지의 소스를 수정해야 하므로 좋은 방법은 아니어서, 좀 더 구글링하다가 이런 경우에 openpgp를 사용하는 곳에서 아래와 같이 "golang.org/x/crypto/ripemd160"를 import 하면 된다는 자료를 찾았다.

import _ "golang.org/x/crypto/ripemd160"

혹시나 하고 시도해 보았는데 신기하게도 이 import 추가만으로 RIPEMD160 에러없이 encrypt가 성공하였다. 🤔

PGP armor 파일을 decrypt 하기

Binary PGP 파일을 decrypt 하는 것은 openpgp 패키지로 쉽게 되는데, ASCII PGP 파일은 decrypt가 되질 않았다.

ASCII PGP 파일은 -----BEGIN PGP MESSAGE----- 문자열로 시작해서 -----END PGP MESSAGE----- 문자열로 끝나는 ASCII 파일이다.

그래서 ASCII PGP 파일 여부를 asciiArmored 파라미터로 받아서 아래와 같이 PGP decrypt 하는 함수를 구현했더니, binary/ascii 둘 다 decrypt가 잘 되었다. 😛

func pgpDecrypt(asciiArmored bool, entityList openpgp.EntityList, r io.Reader, w io.Writer) error {
    entity := entityList[0]

    // Private key 암호를 세팅한다.
    passphraseByte := []byte(myPrivatePw)
    entity.PrivateKey.Decrypt(passphraseByte)
    for _, subkey := range entity.Subkeys {
        subkey.PrivateKey.Decrypt(passphraseByte)
    }

    // PGP decrypt 한다.
    pgpBodyReader := r
    if asciiArmored == true {
        block, err := armor.Decode(r)
        if err != nil {
            return err
        }
        pgpBodyReader = block.Body
    }
    md, err := openpgp.ReadMessage(pgpBodyReader, entityList, nil, nil)
    if err != nil {
        return err
    }

    // Decrypt 된 메시지를 출력 파일에 write 한다.
    _, err = io.Copy(w, md.UnverifiedBody)
    return err
}

카테고리:

업데이트: