120 lines
3.0 KiB
Go
120 lines
3.0 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package pgpkeys
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/ProtonMail/go-crypto/openpgp"
|
|
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
|
"github.com/hashicorp/vault/sdk/helper/jsonutil"
|
|
)
|
|
|
|
const (
|
|
kbPrefix = "keybase:"
|
|
)
|
|
|
|
// FetchKeybasePubkeys fetches public keys from Keybase given a set of
|
|
// usernames, which are derived from correctly formatted input entries. It
|
|
// doesn't use their client code due to both the API and the fact that it is
|
|
// considered alpha and probably best not to rely on it. The keys are returned
|
|
// as base64-encoded strings.
|
|
func FetchKeybasePubkeys(input []string) (map[string]string, error) {
|
|
client := cleanhttp.DefaultClient()
|
|
if client == nil {
|
|
return nil, fmt.Errorf("unable to create an http client")
|
|
}
|
|
|
|
if len(input) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
usernames := make([]string, 0, len(input))
|
|
for _, v := range input {
|
|
if strings.HasPrefix(v, kbPrefix) {
|
|
usernames = append(usernames, strings.TrimPrefix(v, kbPrefix))
|
|
}
|
|
}
|
|
|
|
if len(usernames) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
ret := make(map[string]string, len(usernames))
|
|
url := fmt.Sprintf("https://keybase.io/_/api/1.0/user/lookup.json?usernames=%s&fields=public_keys", strings.Join(usernames, ","))
|
|
resp, err := client.Get(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
type PublicKeys struct {
|
|
Primary struct {
|
|
Bundle string
|
|
}
|
|
}
|
|
|
|
type LThem struct {
|
|
PublicKeys `json:"public_keys"`
|
|
}
|
|
|
|
type KbResp struct {
|
|
Status struct {
|
|
Name string
|
|
}
|
|
Them []LThem
|
|
}
|
|
|
|
out := &KbResp{
|
|
Them: []LThem{},
|
|
}
|
|
|
|
if err := jsonutil.DecodeJSONFromReader(resp.Body, out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if out.Status.Name != "OK" {
|
|
return nil, fmt.Errorf("got non-OK response: %q", out.Status.Name)
|
|
}
|
|
|
|
missingNames := make([]string, 0, len(usernames))
|
|
var keyReader *bytes.Reader
|
|
serializedEntity := bytes.NewBuffer(nil)
|
|
for i, themVal := range out.Them {
|
|
if themVal.Primary.Bundle == "" {
|
|
missingNames = append(missingNames, usernames[i])
|
|
continue
|
|
}
|
|
keyReader = bytes.NewReader([]byte(themVal.Primary.Bundle))
|
|
entityList, err := openpgp.ReadArmoredKeyRing(keyReader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(entityList) != 1 {
|
|
return nil, fmt.Errorf("primary key could not be parsed for user %q", usernames[i])
|
|
}
|
|
if entityList[0] == nil {
|
|
return nil, fmt.Errorf("primary key was nil for user %q", usernames[i])
|
|
}
|
|
|
|
serializedEntity.Reset()
|
|
err = entityList[0].Serialize(serializedEntity)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error serializing entity for user %q: %w", usernames[i], err)
|
|
}
|
|
|
|
// The API returns values in the same ordering requested, so this should properly match
|
|
ret[kbPrefix+usernames[i]] = base64.StdEncoding.EncodeToString(serializedEntity.Bytes())
|
|
}
|
|
|
|
if len(missingNames) > 0 {
|
|
return nil, fmt.Errorf("unable to fetch keys for user(s) %q from keybase", strings.Join(missingNames, ","))
|
|
}
|
|
|
|
return ret, nil
|
|
}
|