1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
|
// Copyright (c) 2019-2020, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.
package client
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)
const (
pathPKSAdd = "/pks/add"
pathPKSLookup = "/pks/lookup"
)
// ErrInvalidKeyText is returned when the key text is invalid.
var ErrInvalidKeyText = errors.New("invalid key text")
// ErrInvalidSearch is returned when the search value is invalid.
var ErrInvalidSearch = errors.New("invalid search")
// ErrInvalidOperation is returned when the operation is invalid.
var ErrInvalidOperation = errors.New("invalid operation")
// PKSAdd submits an ASCII armored keyring to the Key Service, as specified in section 4 of the
// OpenPGP HTTP Keyserver Protocol (HKP) specification. The context controls the lifetime of the
// request.
//
// If an non-200 HTTP status code is received, an error wrapping an HTTPError is returned.
func (c *Client) PKSAdd(ctx context.Context, keyText string) error {
if keyText == "" {
return fmt.Errorf("%w", ErrInvalidKeyText)
}
ref := &url.URL{Path: pathPKSAdd}
v := url.Values{}
v.Set("keytext", keyText)
req, err := c.NewRequest(ctx, http.MethodPost, ref, strings.NewReader(v.Encode()))
if err != nil {
return fmt.Errorf("%w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
res, err := c.Do(req)
if err != nil {
return fmt.Errorf("%w", err)
}
defer res.Body.Close()
if res.StatusCode/100 != 2 { // non-2xx status code
return fmt.Errorf("%w", errorFromResponse(res))
}
return nil
}
// PageDetails includes pagination details.
type PageDetails struct {
// Maximum number of results per page (server may ignore or return fewer).
Size int
// Token for next page (advanced with each request, empty for last page).
Token string
}
const (
// OperationGet is a PKSLookup operation value to perform a "get" operation.
OperationGet = "get"
// OperationIndex is a PKSLookup operation value to perform a "index" operation.
OperationIndex = "index"
// OperationVIndex is a PKSLookup operation value to perform a "vindex" operation.
OperationVIndex = "vindex"
)
// OptionMachineReadable is a PKSLookup options value to return machine readable output.
const OptionMachineReadable = "mr"
// PKSLookup requests data from the Key Service, as specified in section 3 of the OpenPGP HTTP
// Keyserver Protocol (HKP) specification. The context controls the lifetime of the request.
//
// If an non-200 HTTP status code is received, an error wrapping an HTTPError is returned.
func (c *Client) PKSLookup(ctx context.Context, pd *PageDetails, search, operation string, fingerprint, exact bool, options []string) (response string, err error) {
if search == "" {
return "", fmt.Errorf("%w", ErrInvalidSearch)
}
if operation == "" {
return "", fmt.Errorf("%w", ErrInvalidOperation)
}
v := url.Values{}
v.Set("search", search)
v.Set("op", operation)
if 0 < len(options) {
v.Set("options", strings.Join(options, ","))
}
if fingerprint {
v.Set("fingerprint", "on")
}
if exact {
v.Set("exact", "on")
}
if pd != nil {
if pd.Size != 0 {
v.Set("x-pagesize", strconv.Itoa(pd.Size))
}
if pd.Token != "" {
v.Set("x-pagetoken", pd.Token)
}
}
ref := &url.URL{Path: pathPKSLookup, RawQuery: v.Encode()}
req, err := c.NewRequest(ctx, http.MethodGet, ref, nil)
if err != nil {
return "", fmt.Errorf("%w", err)
}
res, err := c.Do(req)
if err != nil {
return "", fmt.Errorf("%w", err)
}
defer res.Body.Close()
if res.StatusCode/100 != 2 { // non-2xx status code
return "", fmt.Errorf("%w", errorFromResponse(res))
}
if pd != nil {
pd.Token = res.Header.Get("X-HKP-Next-Page-Token")
}
body, err := io.ReadAll(res.Body)
if err != nil {
return "", fmt.Errorf("%w", err)
}
return string(body), nil
}
// GetKey retrieves an ASCII armored keyring matching search from the Key Service. A 32-bit key ID,
// 64-bit key ID, 128-bit version 3 fingerprint, or 160-bit version 4 fingerprint can be specified
// in search. The context controls the lifetime of the request.
//
// If an non-200 HTTP status code is received, an error wrapping an HTTPError is returned.
func (c *Client) GetKey(ctx context.Context, search []byte) (keyText string, err error) {
switch len(search) {
case 4, 8, 16, 20:
return c.PKSLookup(ctx, nil, fmt.Sprintf("%#x", search), OperationGet, false, true, nil)
default:
return "", fmt.Errorf("%w", ErrInvalidSearch)
}
}
|