github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/vault/helper/pgpkeys/keybase.go (about) 1 package pgpkeys 2 3 import ( 4 "bytes" 5 "encoding/base64" 6 "fmt" 7 "strings" 8 9 "github.com/hashicorp/errwrap" 10 cleanhttp "github.com/hashicorp/go-cleanhttp" 11 "github.com/hashicorp/terraform-plugin-sdk/internal/vault/sdk/helper/jsonutil" 12 "github.com/keybase/go-crypto/openpgp" 13 ) 14 15 const ( 16 kbPrefix = "keybase:" 17 ) 18 19 // FetchKeybasePubkeys fetches public keys from Keybase given a set of 20 // usernames, which are derived from correctly formatted input entries. It 21 // doesn't use their client code due to both the API and the fact that it is 22 // considered alpha and probably best not to rely on it. The keys are returned 23 // as base64-encoded strings. 24 func FetchKeybasePubkeys(input []string) (map[string]string, error) { 25 client := cleanhttp.DefaultClient() 26 if client == nil { 27 return nil, fmt.Errorf("unable to create an http client") 28 } 29 30 if len(input) == 0 { 31 return nil, nil 32 } 33 34 usernames := make([]string, 0, len(input)) 35 for _, v := range input { 36 if strings.HasPrefix(v, kbPrefix) { 37 usernames = append(usernames, strings.TrimPrefix(v, kbPrefix)) 38 } 39 } 40 41 if len(usernames) == 0 { 42 return nil, nil 43 } 44 45 ret := make(map[string]string, len(usernames)) 46 url := fmt.Sprintf("https://keybase.io/_/api/1.0/user/lookup.json?usernames=%s&fields=public_keys", strings.Join(usernames, ",")) 47 resp, err := client.Get(url) 48 if err != nil { 49 return nil, err 50 } 51 defer resp.Body.Close() 52 53 type PublicKeys struct { 54 Primary struct { 55 Bundle string 56 } 57 } 58 59 type LThem struct { 60 PublicKeys `json:"public_keys"` 61 } 62 63 type KbResp struct { 64 Status struct { 65 Name string 66 } 67 Them []LThem 68 } 69 70 out := &KbResp{ 71 Them: []LThem{}, 72 } 73 74 if err := jsonutil.DecodeJSONFromReader(resp.Body, out); err != nil { 75 return nil, err 76 } 77 78 if out.Status.Name != "OK" { 79 return nil, fmt.Errorf("got non-OK response: %q", out.Status.Name) 80 } 81 82 missingNames := make([]string, 0, len(usernames)) 83 var keyReader *bytes.Reader 84 serializedEntity := bytes.NewBuffer(nil) 85 for i, themVal := range out.Them { 86 if themVal.Primary.Bundle == "" { 87 missingNames = append(missingNames, usernames[i]) 88 continue 89 } 90 keyReader = bytes.NewReader([]byte(themVal.Primary.Bundle)) 91 entityList, err := openpgp.ReadArmoredKeyRing(keyReader) 92 if err != nil { 93 return nil, err 94 } 95 if len(entityList) != 1 { 96 return nil, fmt.Errorf("primary key could not be parsed for user %q", usernames[i]) 97 } 98 if entityList[0] == nil { 99 return nil, fmt.Errorf("primary key was nil for user %q", usernames[i]) 100 } 101 102 serializedEntity.Reset() 103 err = entityList[0].Serialize(serializedEntity) 104 if err != nil { 105 return nil, errwrap.Wrapf(fmt.Sprintf("error serializing entity for user %q: {{err}}", usernames[i]), err) 106 } 107 108 // The API returns values in the same ordering requested, so this should properly match 109 ret[kbPrefix+usernames[i]] = base64.StdEncoding.EncodeToString(serializedEntity.Bytes()) 110 } 111 112 if len(missingNames) > 0 { 113 return nil, fmt.Errorf("unable to fetch keys for user(s) %q from keybase", strings.Join(missingNames, ",")) 114 } 115 116 return ret, nil 117 }