github.com/goreleaser/nfpm/v2@v2.44.0/internal/sign/pgp.go (about) 1 package sign 2 3 import ( 4 "bytes" 5 "crypto" 6 "errors" 7 "fmt" 8 "io" 9 "os" 10 "strconv" 11 "unicode" 12 13 "github.com/ProtonMail/go-crypto/openpgp" 14 "github.com/ProtonMail/go-crypto/openpgp/clearsign" 15 "github.com/ProtonMail/go-crypto/openpgp/packet" 16 "github.com/goreleaser/nfpm/v2" 17 ) 18 19 // PGPSignerWithKeyID returns a PGP signer that creates a detached non-ASCII-armored 20 // signature and is compatible with rpmpack's signature API. 21 func PGPSignerWithKeyID(keyFile, passphrase string, hexKeyID *string) func([]byte) ([]byte, error) { 22 return func(data []byte) ([]byte, error) { 23 keyID, err := parseKeyID(hexKeyID) 24 if err != nil { 25 return nil, fmt.Errorf("%v is not a valid key id: %w", hexKeyID, err) 26 } 27 28 key, err := readSigningKey(keyFile, passphrase) 29 if err != nil { 30 return nil, &nfpm.ErrSigningFailure{Err: err} 31 } 32 33 var signature bytes.Buffer 34 35 if err := openpgp.DetachSign( 36 &signature, 37 key, 38 bytes.NewReader(data), 39 &packet.Config{ 40 SigningKeyId: keyID, 41 DefaultHash: crypto.SHA256, 42 }, 43 ); err != nil { 44 return nil, &nfpm.ErrSigningFailure{Err: err} 45 } 46 47 return signature.Bytes(), nil 48 } 49 } 50 51 // PGPArmoredDetachSign creates an ASCII-armored detached signature. 52 func PGPArmoredDetachSign(message io.Reader, keyFile, passphrase string) ([]byte, error) { 53 return PGPArmoredDetachSignWithKeyID(message, keyFile, passphrase, nil) 54 } 55 56 // PGPArmoredDetachSignWithKeyID creates an ASCII-armored detached signature. 57 func PGPArmoredDetachSignWithKeyID(message io.Reader, keyFile, passphrase string, hexKeyID *string) ([]byte, error) { 58 keyID, err := parseKeyID(hexKeyID) 59 if err != nil { 60 return nil, fmt.Errorf("%v is not a valid key id: %w", hexKeyID, err) 61 } 62 63 key, err := readSigningKey(keyFile, passphrase) 64 if err != nil { 65 return nil, fmt.Errorf("armored detach sign: %w", err) 66 } 67 68 var signature bytes.Buffer 69 70 err = openpgp.ArmoredDetachSign(&signature, key, message, &packet.Config{ 71 SigningKeyId: keyID, 72 DefaultHash: crypto.SHA256, 73 }) 74 if err != nil { 75 return nil, fmt.Errorf("armored detach sign: %w", err) 76 } 77 78 return signature.Bytes(), nil 79 } 80 81 func PGPClearSignWithKeyID(message io.Reader, keyFile, passphrase string, hexKeyID *string) ([]byte, error) { 82 keyID, err := parseKeyID(hexKeyID) 83 if err != nil { 84 return nil, fmt.Errorf("%v is not a valid key id: %w", hexKeyID, err) 85 } 86 87 key, err := readSigningKey(keyFile, passphrase) 88 if err != nil { 89 return nil, fmt.Errorf("clear sign: %w", err) 90 } 91 92 var signature bytes.Buffer 93 94 writeCloser, err := clearsign.Encode( 95 &signature, 96 key.PrivateKey, 97 &packet.Config{ 98 SigningKeyId: keyID, 99 DefaultHash: crypto.SHA256, 100 }, 101 ) 102 if err != nil { 103 return nil, fmt.Errorf("clear sign: %w", err) 104 } 105 106 if _, err := io.Copy(writeCloser, message); err != nil { 107 return nil, fmt.Errorf("clear sign: %w", err) 108 } 109 110 if err := writeCloser.Close(); err != nil { 111 return nil, fmt.Errorf("clear sign: %w", err) 112 } 113 114 return signature.Bytes(), nil 115 } 116 117 // PGPVerify is exported for use in tests and verifies an ASCII-armored or non-ASCII-armored 118 // signature using an ASCII-armored or non-ASCII-armored public key file. The signer 119 // identity is not explicitly checked, other that the obvious fact that the signer's key must 120 // be in the armoredPubKeyFile. 121 func PGPVerify(message io.Reader, signature []byte, armoredPubKeyFile string) error { 122 keyFileContent, err := os.ReadFile(armoredPubKeyFile) 123 if err != nil { 124 return fmt.Errorf("reading armored public key file: %w", err) 125 } 126 127 var keyring openpgp.EntityList 128 129 if isASCII(keyFileContent) { 130 keyring, err = openpgp.ReadArmoredKeyRing(bytes.NewReader(keyFileContent)) 131 if err != nil { 132 return fmt.Errorf("decoding armored public key file: %w", err) 133 } 134 } else { 135 keyring, err = openpgp.ReadKeyRing(bytes.NewReader(keyFileContent)) 136 if err != nil { 137 return fmt.Errorf("decoding public key file: %w", err) 138 } 139 } 140 141 if isASCII(signature) { 142 _, err = openpgp.CheckArmoredDetachedSignature(keyring, message, bytes.NewReader(signature), nil) 143 return err 144 } 145 146 _, err = openpgp.CheckDetachedSignature(keyring, message, bytes.NewReader(signature), nil) 147 return err 148 } 149 150 func PGPReadMessage(message []byte, armoredPubKeyFile string) (plaintext []byte, err error) { 151 keyFileContent, err := os.ReadFile(armoredPubKeyFile) 152 if err != nil { 153 return nil, fmt.Errorf("reading armored public key file: %w", err) 154 } 155 156 var keyring openpgp.EntityList 157 158 if isASCII(keyFileContent) { 159 keyring, err = openpgp.ReadArmoredKeyRing(bytes.NewReader(keyFileContent)) 160 if err != nil { 161 return nil, fmt.Errorf("decoding armored public key file: %w", err) 162 } 163 } else { 164 keyring, err = openpgp.ReadKeyRing(bytes.NewReader(keyFileContent)) 165 if err != nil { 166 return nil, fmt.Errorf("decoding public key file: %w", err) 167 } 168 } 169 170 block, _ := clearsign.Decode(message) 171 _, err = block.VerifySignature(keyring, nil) 172 173 return block.Plaintext, err 174 } 175 176 func parseKeyID(hexKeyID *string) (uint64, error) { 177 if hexKeyID == nil || *hexKeyID == "" { 178 return 0, nil 179 } 180 181 result, err := strconv.ParseUint(*hexKeyID, 16, 64) 182 if err != nil { 183 return 0, err 184 } 185 return result, nil 186 } 187 188 var ( 189 errMoreThanOneKey = errors.New("more than one signing key in keyring") 190 errNoKeys = errors.New("no signing key in keyring") 191 errNoPassword = errors.New("key is encrypted but no passphrase was provided") 192 ) 193 194 func readSigningKey(keyFile, passphrase string) (*openpgp.Entity, error) { 195 fileContent, err := os.ReadFile(keyFile) 196 if err != nil { 197 return nil, fmt.Errorf("reading PGP key file: %w", err) 198 } 199 200 var entityList openpgp.EntityList 201 202 if isASCII(fileContent) { 203 entityList, err = openpgp.ReadArmoredKeyRing(bytes.NewReader(fileContent)) 204 if err != nil { 205 return nil, fmt.Errorf("decoding armored PGP keyring: %w", err) 206 } 207 } else { 208 entityList, err = openpgp.ReadKeyRing(bytes.NewReader(fileContent)) 209 if err != nil { 210 return nil, fmt.Errorf("decoding PGP keyring: %w", err) 211 } 212 } 213 var key *openpgp.Entity 214 215 for _, candidate := range entityList { 216 if candidate.PrivateKey == nil { 217 continue 218 } 219 220 if !candidate.PrivateKey.CanSign() { 221 continue 222 } 223 224 if key != nil { 225 return nil, errMoreThanOneKey 226 } 227 228 key = candidate 229 } 230 231 if key == nil { 232 return nil, errNoKeys 233 } 234 235 if key.PrivateKey.Encrypted { 236 if passphrase == "" { 237 return nil, errNoPassword 238 } 239 pw := []byte(passphrase) 240 err = key.PrivateKey.Decrypt(pw) 241 if err != nil { 242 return nil, fmt.Errorf("decrypt secret signing key: %w", err) 243 } 244 for _, sub := range key.Subkeys { 245 if sub.PrivateKey != nil { 246 if err := sub.PrivateKey.Decrypt(pw); err != nil { 247 return nil, fmt.Errorf("gopenpgp: error in unlocking sub key: %w", err) 248 } 249 } 250 } 251 } 252 253 return key, nil 254 } 255 256 func isASCII(s []byte) bool { 257 for i := range s { 258 if s[i] > unicode.MaxASCII { 259 return false 260 } 261 } 262 return true 263 }