pax_global_header00006660000000000000000000000064141604540420014512gustar00rootroot0000000000000052 comment=83bc6966024f67b80e9f601e01f149cd40071d61 container-key-client-0.7.2/000077500000000000000000000000001416045404200155445ustar00rootroot00000000000000container-key-client-0.7.2/.github/000077500000000000000000000000001416045404200171045ustar00rootroot00000000000000container-key-client-0.7.2/.github/dependabot.yml000066400000000000000000000001771416045404200217410ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: gomod directory: "/" schedule: interval: daily open-pull-requests-limit: 10 container-key-client-0.7.2/.github/workflows/000077500000000000000000000000001416045404200211415ustar00rootroot00000000000000container-key-client-0.7.2/.github/workflows/ci.yml000066400000000000000000000020501416045404200222540ustar00rootroot00000000000000name: ci on: pull_request: push: branches: - main tags: - 'v*.*.*' jobs: build_and_test: name: build_and_test runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Check for markdown Lint run: | sudo npm install -g markdownlint-cli markdownlint . - name: Set up Go uses: actions/setup-go@v1 with: go-version: '1.17.x' - name: Check go.mod and go.sum are tidy run: | go mod tidy git diff --exit-code -- go.mod go.sum - name: Build Source run: go build ./... - name: Install Lint uses: golangci/golangci-lint-action@v2 with: version: v1.43 - name: Run Lint run: | golangci-lint run - name: Run Tests run: go test -coverprofile cover.out -race ./... - name: Upload coverage report uses: codecov/codecov-action@v1 with: files: cover.out flags: unittests name: codecov container-key-client-0.7.2/.gitignore000066400000000000000000000000031416045404200175250ustar00rootroot00000000000000*~ container-key-client-0.7.2/.golangci.yml000066400000000000000000000011121416045404200201230ustar00rootroot00000000000000linters: disable-all: true enable: - bidichk - bodyclose - contextcheck - deadcode - depguard - dogsled - dupl - errcheck - gochecknoinits - goconst - gocritic - gocyclo - godox - gofumpt - goimports - goprintffuncname - gosec - gosimple - govet - ineffassign - ireturn - misspell - nakedret - nilnil - prealloc - revive - rowserrcheck - staticcheck - structcheck - tenv - typecheck - unconvert - unparam - unused - varcheck - whitespace container-key-client-0.7.2/.markdownlint.yml000066400000000000000000000000151416045404200210520ustar00rootroot00000000000000MD013: false container-key-client-0.7.2/.vscode/000077500000000000000000000000001416045404200171055ustar00rootroot00000000000000container-key-client-0.7.2/.vscode/settings.json000066400000000000000000000000461416045404200216400ustar00rootroot00000000000000{ "go.lintTool": "golangci-lint" }container-key-client-0.7.2/LICENSE.md000066400000000000000000000026371416045404200171600ustar00rootroot00000000000000# LICENSE Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 1. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 1. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. container-key-client-0.7.2/README.md000066400000000000000000000026371416045404200170330ustar00rootroot00000000000000# Container Key Client [![PkgGoDev](https://pkg.go.dev/badge/github.com/apptainer/container-key-client)](https://pkg.go.dev/github.com/apptainer/container-key-client/client) [![Build Status](https://github.com/apptainer/container-key-client/actions/workflows/ci.yml/badge.svg)](https://github.com/apptainer/container-key-client/actions/workflows/ci.yml) [![Code Coverage](https://codecov.io/gh/apptainer/container-key-client/branch/master/graph/badge.svg)](https://codecov.io/gh/apptainer/container-key-client) [![Go Report Card](https://goreportcard.com/badge/github.com/apptainer/container-key-client)](https://goreportcard.com/report/github.com/apptainer/container-key-client) This project provides a Go client to Apptainer for key storage and retrieval using the HKP protocol. Forked from [sylabs/scs-key-client](https://github.com/sylabs/scs-key-client). This is maintained as a sub-project by the Apptainer project. To contribute, see the [Apptainer CONTRIBUTING file](https://github.com/apptainer/apptainer/blob/main/CONTRIBUTING.md). ## Go Version Compatibility This module aims to maintain support for the two most recent stable versions of Go. This corresponds to the Go [Release Maintenance Policy](https://github.com/golang/go/wiki/Go-Release-Cycle#release-maintenance) and [Security Policy](https://golang.org/security), ensuring critical bug fixes and security patches are available for all supported language versions. container-key-client-0.7.2/client/000077500000000000000000000000001416045404200170225ustar00rootroot00000000000000container-key-client-0.7.2/client/client.go000066400000000000000000000122111416045404200206240ustar00rootroot00000000000000// 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" "net/http" "net/url" "strings" ) const ( schemeHTTP = "http" schemeHTTPS = "https" schemeHKP = "hkp" schemeHKPS = "hkps" ) // errUnsupportedProtocolScheme is returned when an unsupported protocol scheme is encountered. var errUnsupportedProtocolScheme = errors.New("unsupported protocol scheme") // ErrTLSRequired is returned when an auth token is supplied with a non-TLS BaseURL. var ErrTLSRequired = errors.New("TLS required when auth token provided") // normalizeURL normalizes rawURL, translating HKP/HKPS schemes to HTTP/HTTPS respectively, and // ensures the path component is terminated with a separator. func normalizeURL(rawURL string) (*url.URL, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err } switch u.Scheme { case schemeHTTP, schemeHTTPS: break case schemeHKP: // The HKP scheme is HTTP and implies port 11371. u.Scheme = schemeHTTP if u.Port() == "" { u.Host = net.JoinHostPort(u.Hostname(), "11371") } case schemeHKPS: // The HKPS scheme is HTTPS and implies port 443. u.Scheme = schemeHTTPS default: return nil, fmt.Errorf("%w %s", errUnsupportedProtocolScheme, u.Scheme) } // Ensure path is terminated with a separator, to prevent url.ResolveReference from stripping // the final path component of BaseURL when constructing request URL from a relative path. if !strings.HasSuffix(u.Path, "/") { u.Path += "/" } return u, nil } // clientOptions describes the options for a Client. type clientOptions struct { baseURL string bearerToken string userAgent string httpClient *http.Client } // Option are used to populate co. type Option func(co *clientOptions) error // OptBaseURL sets the base URL of the key server to url. The supported URL schemes are "http", // "https", "hkp", and "hkps". func OptBaseURL(url string) Option { return func(co *clientOptions) error { co.baseURL = url return nil } } // OptBearerToken sets the bearer token to include in the "Authorization" header of each request. func OptBearerToken(token string) Option { return func(co *clientOptions) error { co.bearerToken = token return nil } } // OptUserAgent sets the HTTP user agent to include in the "User-Agent" header of each request. func OptUserAgent(agent string) Option { return func(co *clientOptions) error { co.userAgent = agent return nil } } // OptHTTPClient sets the client to use to make HTTP requests. func OptHTTPClient(c *http.Client) Option { return func(co *clientOptions) error { co.httpClient = c return nil } } const defaultBaseURL = "https://keys.openpgp.org/" // Client describes the client details. type Client struct { baseURL *url.URL // Parsed base URL. bearerToken string // Bearer token to include in "Authorization" header. userAgent string // Value to include in "User-Agent" header. httpClient *http.Client // Client to use for HTTP requests. } // NewClient returns a Client to interact with an HKP key server according to opts. // // By default, the Sylabs Key Service is used. To override this behaviour, use OptBaseURL. If the // key server requires authentication, consider using OptBearerToken. // // If a bearer token is specified with a non-localhost base URL that does not utilize Transport // Layer Security (TLS), an error wrapping ErrTLSRequired is returned. func NewClient(opts ...Option) (*Client, error) { co := clientOptions{ baseURL: defaultBaseURL, httpClient: http.DefaultClient, } // Apply options. for _, opt := range opts { if err := opt(&co); err != nil { return nil, fmt.Errorf("%w", err) } } c := Client{ bearerToken: co.bearerToken, userAgent: co.userAgent, httpClient: co.httpClient, } // Normalize base URL. u, err := normalizeURL(co.baseURL) if err != nil { return nil, fmt.Errorf("%w", err) } c.baseURL = u // If auth token is used, verify TLS. if c.bearerToken != "" && c.baseURL.Scheme != schemeHTTPS && c.baseURL.Hostname() != "localhost" { return nil, fmt.Errorf("%w", ErrTLSRequired) } return &c, nil } // NewRequest returns a new Request given a method, ref, and optional body. // // The context controls the entire lifetime of a request and its response: obtaining a connection, // sending the request, and reading the response headers and body. func (c *Client) NewRequest(ctx context.Context, method string, ref *url.URL, body io.Reader) (*http.Request, error) { u := c.baseURL.ResolveReference(ref) r, err := http.NewRequestWithContext(ctx, method, u.String(), body) if err != nil { return nil, fmt.Errorf("%v", err) } if v := c.bearerToken; v != "" { r.Header.Set("Authorization", fmt.Sprintf("BEARER %s", v)) } if v := c.userAgent; v != "" { r.Header.Set("User-Agent", v) } return r, nil } // Do sends an HTTP request and returns an HTTP response. func (c *Client) Do(req *http.Request) (*http.Response, error) { return c.httpClient.Do(req) } container-key-client-0.7.2/client/client_test.go000066400000000000000000000210571416045404200216730ustar00rootroot00000000000000// 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" "io" "net/http" "net/url" "reflect" "strings" "testing" ) func TestNormalizeURL(t *testing.T) { tests := []struct { name string url string wantErr bool wantURL *url.URL }{ {"BadURL", ":", true, nil}, {"BadScheme", "bad:", true, nil}, {"HTTPBaseURL", "http://p80.pool.sks-keyservers.net", false, &url.URL{ Scheme: "http", Host: "p80.pool.sks-keyservers.net", Path: "/", }}, {"HTTPSBaseURL", "https://hkps.pool.sks-keyservers.net", false, &url.URL{ Scheme: "https", Host: "hkps.pool.sks-keyservers.net", Path: "/", }}, {"HKPBaseURL", "hkp://pool.sks-keyservers.net", false, &url.URL{ Scheme: "http", Host: "pool.sks-keyservers.net:11371", Path: "/", }}, {"HKPSBaseURL", "hkps://hkps.pool.sks-keyservers.net", false, &url.URL{ Scheme: "https", Host: "hkps.pool.sks-keyservers.net", Path: "/", }}, {"BaseURLSlash", "hkps://hkps.pool.sks-keyservers.net/", false, &url.URL{ Scheme: "https", Host: "hkps.pool.sks-keyservers.net", Path: "/", }}, {"BaseURLPath", "hkps://hkps.pool.sks-keyservers.net/path", false, &url.URL{ Scheme: "https", Host: "hkps.pool.sks-keyservers.net", Path: "/path/", }}, {"BaseURLPathSlash", "hkps://hkps.pool.sks-keyservers.net/path/", false, &url.URL{ Scheme: "https", Host: "hkps.pool.sks-keyservers.net", Path: "/path/", }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { u, err := normalizeURL(tt.url) if (err != nil) != tt.wantErr { t.Fatalf("got err %v, want %v", err, tt.wantErr) } if err == nil { if got, want := u, tt.wantURL; !reflect.DeepEqual(got, want) { t.Errorf("got url %v, want %v", got, want) } } }) } } func TestNewClient(t *testing.T) { httpClient := &http.Client{} tests := []struct { name string opts []Option wantErr error wantURL string bearerToken string wantUserAgent string wantHTTPClient *http.Client }{ {"UnsupportedProtocolScheme", []Option{ OptBaseURL("bad:"), }, errUnsupportedProtocolScheme, "", "", "", nil}, {"TLSRequiredHTTP", []Option{ OptBaseURL("http://p80.pool.sks-keyservers.net"), OptBearerToken("blah"), }, ErrTLSRequired, "", "", "", nil}, {"TLSRequiredHKP", []Option{ OptBaseURL("hkp://pool.sks-keyservers.net"), OptBearerToken("blah"), }, ErrTLSRequired, "", "", "", nil}, {"Defaults", nil, nil, defaultBaseURL, "", "", http.DefaultClient}, {"BaseURL", []Option{ OptBaseURL("hkps://hkps.pool.sks-keyservers.net"), }, nil, "https://hkps.pool.sks-keyservers.net/", "", "", http.DefaultClient}, {"BaseURLSlash", []Option{ OptBaseURL("hkps://hkps.pool.sks-keyservers.net/"), }, nil, "https://hkps.pool.sks-keyservers.net/", "", "", http.DefaultClient}, {"BaseURLWithPath", []Option{ OptBaseURL("hkps://hkps.pool.sks-keyservers.net/path"), }, nil, "https://hkps.pool.sks-keyservers.net/path/", "", "", http.DefaultClient}, {"BaseURLWithPathSlash", []Option{ OptBaseURL("hkps://hkps.pool.sks-keyservers.net/path/"), }, nil, "https://hkps.pool.sks-keyservers.net/path/", "", "", http.DefaultClient}, {"BearerToken", []Option{ OptBearerToken("blah"), }, nil, defaultBaseURL, "blah", "", http.DefaultClient}, {"UserAgent", []Option{ OptUserAgent("Secret Agent Man"), }, nil, defaultBaseURL, "", "Secret Agent Man", http.DefaultClient}, {"HTTPClient", []Option{ OptHTTPClient(httpClient), }, nil, defaultBaseURL, "", "", httpClient}, {"LocalhostBearerTokenHTTP", []Option{ OptBaseURL("http://localhost"), OptBearerToken("blah"), }, nil, "http://localhost/", "blah", "", http.DefaultClient}, {"LocalhostBearerTokenHTTP8080", []Option{ OptBaseURL("http://localhost:8080"), OptBearerToken("blah"), }, nil, "http://localhost:8080/", "blah", "", http.DefaultClient}, {"LocalhostBearerTokenHKP", []Option{ OptBaseURL("hkp://localhost"), OptBearerToken("blah"), }, nil, "http://localhost:11371/", "blah", "", http.DefaultClient}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c, err := NewClient(tt.opts...) if got, want := err, tt.wantErr; !errors.Is(got, want) { t.Fatalf("got error %v, want %v", got, want) } if err == nil { if got, want := c.baseURL.String(), tt.wantURL; got != want { t.Errorf("got host %v, want %v", got, want) } if got, want := c.bearerToken, tt.bearerToken; got != want { t.Errorf("got auth token %v, want %v", got, want) } if got, want := c.userAgent, tt.wantUserAgent; got != want { t.Errorf("got user agent %v, want %v", got, want) } if got, want := c.httpClient, tt.wantHTTPClient; got != want { t.Errorf("got HTTP client %v, want %v", got, want) } } }) } } func TestNewRequest(t *testing.T) { tests := []struct { name string opts []Option method string path string rawQuery string body string wantErr bool wantURL string wantAuthBearer string wantUserAgent string }{ {"BadMethod", nil, "b@d ", "", "", "", true, "", "", ""}, {"Get", nil, http.MethodGet, "/path", "", "", false, "https://keys.openpgp.org/path", "", ""}, {"Post", nil, http.MethodPost, "/path", "", "", false, "https://keys.openpgp.org/path", "", ""}, {"PostRawQuery", nil, http.MethodPost, "/path", "a=b", "", false, "https://keys.openpgp.org/path?a=b", "", ""}, {"PostBody", nil, http.MethodPost, "/path", "", "body", false, "https://keys.openpgp.org/path", "", ""}, {"BaseURLAbsolute", []Option{ OptBaseURL("hkps://hkps.pool.sks-keyservers.net"), }, http.MethodGet, "/path", "", "", false, "https://hkps.pool.sks-keyservers.net/path", "", ""}, {"BaseURLRelative", []Option{ OptBaseURL("hkps://hkps.pool.sks-keyservers.net"), }, http.MethodGet, "path", "", "", false, "https://hkps.pool.sks-keyservers.net/path", "", ""}, {"BaseURLPathAbsolute", []Option{ OptBaseURL("hkps://hkps.pool.sks-keyservers.net/a/b"), }, http.MethodGet, "/path", "", "", false, "https://hkps.pool.sks-keyservers.net/path", "", ""}, {"BaseURLPathRelative", []Option{ OptBaseURL("hkps://hkps.pool.sks-keyservers.net/a/b"), }, http.MethodGet, "path", "", "", false, "https://hkps.pool.sks-keyservers.net/a/b/path", "", ""}, {"BearerToken", []Option{ OptBearerToken("blah"), }, http.MethodGet, "/path", "", "", false, "https://keys.openpgp.org/path", "BEARER blah", ""}, {"UserAgent", []Option{ OptUserAgent("Secret Agent Man"), }, http.MethodGet, "/path", "", "", false, "https://keys.openpgp.org/path", "", "Secret Agent Man"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c, err := NewClient(tt.opts...) if err != nil { t.Fatal(err) } ref := &url.URL{Path: tt.path, RawQuery: tt.rawQuery} r, err := c.NewRequest(context.Background(), tt.method, ref, strings.NewReader(tt.body)) if (err != nil) != tt.wantErr { t.Fatalf("got err %v, wantErr %v", err, tt.wantErr) } if err == nil { if got, want := r.Method, tt.method; got != want { t.Errorf("got method %v, want %v", got, want) } if got, want := r.URL.String(), tt.wantURL; got != want { t.Errorf("got URL %v, want %v", got, want) } b, err := io.ReadAll(r.Body) if err != nil { t.Errorf("failed to read body: %v", err) } if got, want := string(b), tt.body; got != want { t.Errorf("got body %v, want %v", got, want) } authBearer, ok := r.Header["Authorization"] if got, want := ok, (tt.wantAuthBearer != ""); got != want { t.Fatalf("presence of auth bearer %v, want %v", got, want) } if ok { if got, want := len(authBearer), 1; got != want { t.Fatalf("got %v auth bearer(s), want %v", got, want) } if got, want := authBearer[0], tt.wantAuthBearer; got != want { t.Errorf("got auth bearer %v, want %v", got, want) } } userAgent, ok := r.Header["User-Agent"] if got, want := ok, (tt.wantUserAgent != ""); got != want { t.Fatalf("presence of user agent %v, want %v", got, want) } if ok { if got, want := len(userAgent), 1; got != want { t.Fatalf("got %v user agent(s), want %v", got, want) } if got, want := userAgent[0], tt.wantUserAgent; got != want { t.Errorf("got user agent %v, want %v", got, want) } } } }) } } container-key-client-0.7.2/client/error.go000066400000000000000000000027561416045404200205140ustar00rootroot00000000000000// Copyright (c) 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 ( "errors" "fmt" "net/http" jsonresp "github.com/sylabs/json-resp" ) // HTTPError represents an error returned from an HTTP server. type HTTPError struct { code int err error } // Code returns the HTTP status code associated with e. func (e *HTTPError) Code() int { return e.code } // Unwrap returns the error wrapped by e. func (e *HTTPError) Unwrap() error { return e.err } // Error returns a human-readable representation of e. func (e *HTTPError) Error() string { if e.err != nil { return fmt.Sprintf("%v %v: %v", e.code, http.StatusText(e.code), e.err.Error()) } return fmt.Sprintf("%v %v", e.code, http.StatusText(e.code)) } // Is compares e against target. If target is a HTTPError with the same code as e, true is returned. func (e *HTTPError) Is(target error) bool { t, ok := target.(*HTTPError) return ok && (t.code == e.code) } // errorFromResponse returns an HTTPError containing the status code and detailed error message (if // available) from res. func errorFromResponse(res *http.Response) error { httpErr := HTTPError{code: res.StatusCode} var jerr *jsonresp.Error if err := jsonresp.ReadError(res.Body); errors.As(err, &jerr) { httpErr.err = errors.New(jerr.Message) } return &httpErr } container-key-client-0.7.2/client/error_test.go000066400000000000000000000023451416045404200215450ustar00rootroot00000000000000// Copyright (c) 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 ( "errors" "net/http" "testing" ) func TestHTTPError(t *testing.T) { tests := []struct { name string code int err error wantMessage string }{ { name: "BadRequest", code: http.StatusBadRequest, wantMessage: "400 Bad Request", }, { name: "BadRequestWithMessage", code: http.StatusBadRequest, err: errors.New("more good needed"), wantMessage: "400 Bad Request: more good needed", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := &HTTPError{ code: tt.code, err: tt.err, } if got, want := err.Code(), tt.code; got != want { t.Errorf("got code %v, want %v", got, want) } if got, want := err.Unwrap(), tt.err; got != want { t.Errorf("got unwrapped error %v, want %v", got, want) } if got, want := err.Error(), tt.wantMessage; got != want { t.Errorf("got message %v, want %v", got, want) } }) } } container-key-client-0.7.2/client/pks.go000066400000000000000000000110521416045404200201450ustar00rootroot00000000000000// 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) } } container-key-client-0.7.2/client/pks_test.go000066400000000000000000000360411416045404200212110ustar00rootroot00000000000000// 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/http/httptest" "strconv" "strings" "testing" jsonresp "github.com/sylabs/json-resp" ) type MockPKSAdd struct { t *testing.T code int message string keyText string } func (m *MockPKSAdd) ServeHTTP(w http.ResponseWriter, r *http.Request) { if m.code/100 != 2 { // non-2xx status code if m.message != "" { if err := jsonresp.WriteError(w, m.message, m.code); err != nil { m.t.Fatalf("failed to write error: %v", err) } } else { w.WriteHeader(m.code) } return } if got, want := r.URL.Path, pathPKSAdd; got != want { m.t.Errorf("got path %v, want %v", got, want) } if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want { m.t.Errorf("got content type %v, want %v", got, want) } if err := r.ParseForm(); err != nil { m.t.Fatalf("failed to parse form: %v", err) } if got, want := r.Form.Get("keytext"), m.keyText; got != want { m.t.Errorf("got key text %v, want %v", got, want) } } func TestPKSAdd(t *testing.T) { cancelled, cancel := context.WithCancel(context.Background()) cancel() tests := []struct { name string ctx context.Context keyText string code int message string wantErr error }{ { name: "OK", ctx: context.Background(), keyText: "key", code: http.StatusOK, }, { name: "Accepted", ctx: context.Background(), keyText: "key", code: http.StatusAccepted, }, { name: "HTTPError", ctx: context.Background(), keyText: "key", code: http.StatusBadRequest, wantErr: &HTTPError{code: http.StatusBadRequest}, }, { name: "HTTPErrorMessage", ctx: context.Background(), keyText: "key", code: http.StatusBadRequest, message: "blah", wantErr: &HTTPError{code: http.StatusBadRequest}, }, { name: "ContextCanceled", ctx: cancelled, keyText: "key", code: http.StatusOK, wantErr: context.Canceled, }, { name: "InvalidKeyText", ctx: context.Background(), keyText: "", wantErr: ErrInvalidKeyText, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() s := httptest.NewServer(&MockPKSAdd{ t: t, code: tt.code, message: tt.message, keyText: tt.keyText, }) defer s.Close() c, err := NewClient(OptBaseURL(s.URL)) if err != nil { t.Fatalf("failed to create client: %v", err) } err = c.PKSAdd(tt.ctx, tt.keyText) if got, want := err, tt.wantErr; !errors.Is(got, want) { t.Fatalf("got error %v, want %v", got, want) } }) } } type MockPKSLookup struct { t *testing.T code int message string search string op string options string fingerprint bool exact bool pageSize string pageToken string nextPageToken string response string } func (m *MockPKSLookup) ServeHTTP(w http.ResponseWriter, r *http.Request) { if m.code/100 != 2 { // non-2xx status code if m.message != "" { if err := jsonresp.WriteError(w, m.message, m.code); err != nil { m.t.Fatalf("failed to write error: %v", err) } } else { w.WriteHeader(m.code) } return } if got, want := r.URL.Path, pathPKSLookup; got != want { m.t.Errorf("got path %v, want %v", got, want) } if got, want := r.ContentLength, int64(0); got != want { m.t.Errorf("got content length %v, want %v", got, want) } if err := r.ParseForm(); err != nil { m.t.Fatalf("failed to parse form: %v", err) } if got, want := r.Form.Get("search"), m.search; got != want { m.t.Errorf("got search %v, want %v", got, want) } if got, want := r.Form.Get("op"), m.op; got != want { m.t.Errorf("got op %v, want %v", got, want) } // options is optional. options, ok := r.Form["options"] if got, want := ok, m.options != ""; got != want { m.t.Errorf("options presence %v, want %v", got, want) } else if ok { if len(options) != 1 { m.t.Errorf("got multiple options values") } else if got, want := options[0], m.options; got != want { m.t.Errorf("got options %v, want %v", got, want) } } // fingerprint is optional. fp, ok := r.Form["fingerprint"] if got, want := ok, m.fingerprint; got != want { m.t.Errorf("fingerprint presence %v, want %v", got, want) } else if ok { if len(fp) != 1 { m.t.Errorf("got multiple fingerprint values") } else if got, want := fp[0], "on"; got != want { m.t.Errorf("got fingerprint %v, want %v", got, want) } } // exact is optional. exact, ok := r.Form["exact"] if got, want := ok, m.exact; got != want { m.t.Errorf("exact presence %v, want %v", got, want) } else if ok { if len(exact) != 1 { m.t.Errorf("got multiple exact values") } else if got, want := exact[0], "on"; got != want { m.t.Errorf("got exact %v, want %v", got, want) } } // x-pagesize is optional. pageSize, ok := r.Form["x-pagesize"] if got, want := ok, m.pageSize != ""; got != want { m.t.Errorf("page size presence %v, want %v", got, want) } else if ok { if len(pageSize) != 1 { m.t.Error("got multiple page size values") } else if got, want := pageSize[0], m.pageSize; got != want { m.t.Errorf("got page size %v, want %v", got, want) } } // x-pagetoken is optional. pageToken, ok := r.Form["x-pagetoken"] if got, want := ok, m.pageToken != ""; got != want { m.t.Errorf("page token presence %v, want %v", got, want) } else if ok { if len(pageToken) != 1 { m.t.Error("got multiple page token values") } else if got, want := pageToken[0], m.pageToken; got != want { m.t.Errorf("got page token %v, want %v", got, want) } } w.Header().Set("X-HKP-Next-Page-Token", m.nextPageToken) if _, err := io.Copy(w, strings.NewReader(m.response)); err != nil { m.t.Fatalf("failed to copy: %v", err) } } func TestPKSLookup(t *testing.T) { cancelled, cancel := context.WithCancel(context.Background()) cancel() tests := []struct { name string ctx context.Context code int message string search string op string options []string fingerprint bool exact bool pageToken string pageSize int nextPageToken string wantErr error }{ { name: "Get", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationGet, }, { name: "GetNPT", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationGet, nextPageToken: "bar", }, { name: "GetSize", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationGet, pageSize: 42, }, { name: "GetSizeNPT", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationGet, pageSize: 42, nextPageToken: "bar", }, { name: "GetPT", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationGet, pageToken: "foo", }, { name: "GetPTNPT", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationGet, pageToken: "foo", nextPageToken: "bar", }, { name: "GetPTSize", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationGet, pageToken: "foo", pageSize: 42, }, { name: "GetPTSizeNPT", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationGet, pageToken: "foo", pageSize: 42, nextPageToken: "bar", }, { name: "GetMachineReadable", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationGet, options: []string{OptionMachineReadable}, }, { name: "GetMachineReadableBlah", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationGet, options: []string{OptionMachineReadable, "blah"}, }, { name: "GetExact", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationGet, exact: true, }, { name: "Index", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationIndex, }, { name: "IndexMachineReadable", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationIndex, options: []string{OptionMachineReadable}, }, { name: "IndexMachineReadableBlah", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationIndex, options: []string{OptionMachineReadable, "blah"}, }, { name: "IndexFingerprint", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationIndex, fingerprint: true, }, { name: "IndexExact", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationIndex, exact: true, }, { name: "VIndex", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationVIndex, }, { name: "VIndexMachineReadable", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationVIndex, options: []string{OptionMachineReadable}, }, { name: "VIndexMachineReadableBlah", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationVIndex, options: []string{OptionMachineReadable, "blah"}, }, { name: "VIndexFingerprint", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationVIndex, fingerprint: true, }, { name: "VIndexExact", ctx: context.Background(), code: http.StatusOK, search: "search", op: OperationVIndex, exact: true, }, { name: "NonAuthoritativeInfo", ctx: context.Background(), code: http.StatusNonAuthoritativeInfo, search: "search", op: OperationGet, }, { name: "HTTPError", ctx: context.Background(), code: http.StatusBadRequest, search: "search", op: OperationGet, wantErr: &HTTPError{code: http.StatusBadRequest}, }, { name: "HTTPErrorMessage", ctx: context.Background(), code: http.StatusBadRequest, message: "blah", search: "search", op: OperationGet, wantErr: &HTTPError{code: http.StatusBadRequest}, }, { name: "ContextCanceled", ctx: cancelled, code: http.StatusOK, search: "search", op: OperationGet, wantErr: context.Canceled, }, { name: "InvalidSearch", ctx: context.Background(), code: http.StatusOK, op: OperationGet, wantErr: ErrInvalidSearch, }, { name: "InvalidOperation", ctx: context.Background(), code: http.StatusOK, search: "search", options: []string{}, wantErr: ErrInvalidOperation, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() m := MockPKSLookup{ t: t, response: "Not valid, but it'll do for testing", code: tt.code, message: tt.message, search: tt.search, op: tt.op, options: strings.Join(tt.options, ","), fingerprint: tt.fingerprint, exact: tt.exact, pageToken: tt.pageToken, nextPageToken: tt.nextPageToken, } if tt.pageSize != 0 { m.pageSize = strconv.Itoa(tt.pageSize) } s := httptest.NewServer(&m) defer s.Close() c, err := NewClient(OptBaseURL(s.URL)) if err != nil { t.Fatalf("failed to create client: %v", err) } pd := PageDetails{ Token: tt.pageToken, Size: tt.pageSize, } r, err := c.PKSLookup(tt.ctx, &pd, tt.search, tt.op, tt.fingerprint, tt.exact, tt.options) if got, want := err, tt.wantErr; !errors.Is(got, want) { t.Fatalf("got error %v, want %v", got, want) } if err == nil { if got, want := pd.Token, tt.nextPageToken; got != want { t.Errorf("got page token %v, want %v", got, want) } if got, want := r, m.response; got != want { t.Errorf("got response %v, want %v", got, want) } } }) } } func TestGetKey(t *testing.T) { search := []byte{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, } cancelled, cancel := context.WithCancel(context.Background()) cancel() tests := []struct { name string ctx context.Context code int message string search []byte wantErr error }{ { name: "ShortKeyID", ctx: context.Background(), code: http.StatusOK, search: search[len(search)-4:], }, { name: "KeyID", ctx: context.Background(), code: http.StatusOK, search: search[len(search)-8:], }, { name: "V3Fingerprint", ctx: context.Background(), code: http.StatusOK, search: search[len(search)-16:], }, { name: "V4Fingerprint", ctx: context.Background(), code: http.StatusOK, search: search, }, { name: "NonAuthoritativeInfo", ctx: context.Background(), code: http.StatusNonAuthoritativeInfo, search: search, }, { name: "HTTPError", ctx: context.Background(), code: http.StatusBadRequest, search: search, wantErr: &HTTPError{code: http.StatusBadRequest}, }, { name: "HTTPErrorMessage", ctx: context.Background(), code: http.StatusBadRequest, message: "blah", search: search, wantErr: &HTTPError{code: http.StatusBadRequest}, }, { name: "ContextCanceled", ctx: cancelled, code: http.StatusOK, search: search, wantErr: context.Canceled, }, { name: "InvalidSearch", ctx: context.Background(), search: search[:1], wantErr: ErrInvalidSearch, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() m := MockPKSLookup{ t: t, code: tt.code, message: tt.message, search: fmt.Sprintf("%#x", tt.search), op: OperationGet, exact: true, response: "Not valid, but it'll do for testing", } s := httptest.NewServer(&m) defer s.Close() c, err := NewClient(OptBaseURL(s.URL)) if err != nil { t.Fatalf("failed to create client: %v", err) } kt, err := c.GetKey(tt.ctx, tt.search) if got, want := err, tt.wantErr; !errors.Is(got, want) { t.Fatalf("got error %v, want %v", got, want) } if err == nil { if got, want := kt, m.response; got != want { t.Errorf("got keyText %v, want %v", got, want) } } }) } } container-key-client-0.7.2/client/version.go000066400000000000000000000023121416045404200210340ustar00rootroot00000000000000// 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" "fmt" "net/http" "net/url" jsonresp "github.com/sylabs/json-resp" ) const pathVersion = "version" // GetVersion gets version information from the Key Service. 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) GetVersion(ctx context.Context) (string, error) { ref := &url.URL{Path: pathVersion} 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)) } vi := struct { Version string `json:"version"` }{} if err := jsonresp.ReadResponse(res.Body, &vi); err != nil { return "", fmt.Errorf("%w", err) } return vi.Version, nil } container-key-client-0.7.2/client/version_test.go000066400000000000000000000060601416045404200220770ustar00rootroot00000000000000// 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" "net/http" "net/http/httptest" "testing" jsonresp "github.com/sylabs/json-resp" ) type MockVersion struct { t *testing.T code int message string wantPath string version string } func (m *MockVersion) ServeHTTP(w http.ResponseWriter, r *http.Request) { if m.code/100 != 2 { // non-2xx status code if err := jsonresp.WriteError(w, m.message, m.code); err != nil { m.t.Fatalf("failed to write error: %v", err) } return } if got, want := r.URL.Path, m.wantPath; got != want { m.t.Errorf("got path %v, want %v", got, want) } if got, want := r.ContentLength, int64(0); got != want { m.t.Errorf("got content length %v, want %v", got, want) } vi := struct { Version string `json:"version"` }{ Version: m.version, } if err := jsonresp.WriteResponse(w, vi, m.code); err != nil { m.t.Fatalf("failed to write response: %v", err) } } func TestGetVersion(t *testing.T) { cancelled, cancel := context.WithCancel(context.Background()) cancel() tests := []struct { name string path string ctx context.Context code int message string wantPath string version string wantErr error }{ { name: "OK", ctx: context.Background(), code: http.StatusOK, wantPath: "/version", version: "1.2.3", }, { name: "OKWithPath", path: "/path", ctx: context.Background(), code: http.StatusOK, wantPath: "/path/version", version: "1.2.3", }, { name: "NonAuthoritativeInfo", ctx: context.Background(), code: http.StatusNonAuthoritativeInfo, wantPath: "/version", version: "1.2.3", }, { name: "HTTPError", ctx: context.Background(), code: http.StatusBadRequest, wantErr: &HTTPError{code: http.StatusBadRequest}, }, { name: "HTTPErrorMessage", ctx: context.Background(), code: http.StatusBadRequest, message: "blah", wantErr: &HTTPError{code: http.StatusBadRequest}, }, { name: "ContextCanceled", ctx: cancelled, code: http.StatusOK, wantErr: context.Canceled, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() s := httptest.NewServer(&MockVersion{ t: t, code: tt.code, message: tt.message, wantPath: tt.wantPath, version: tt.version, }) defer s.Close() c, err := NewClient(OptBaseURL(s.URL + tt.path)) if err != nil { t.Fatalf("failed to create client: %v", err) } v, err := c.GetVersion(tt.ctx) if got, want := err, tt.wantErr; !errors.Is(got, want) { t.Fatalf("got error %v, want %v", got, want) } if err == nil { if got, want := v, tt.version; got != want { t.Errorf("got version %v, want %v", got, want) } } }) } } container-key-client-0.7.2/go.mod000066400000000000000000000001461416045404200166530ustar00rootroot00000000000000module github.com/apptainer/container-key-client go 1.17 require github.com/sylabs/json-resp v0.8.0 container-key-client-0.7.2/go.sum000066400000000000000000000002551416045404200167010ustar00rootroot00000000000000github.com/sylabs/json-resp v0.8.0 h1:bZ932uaF220aPqT0+x/vakoaGCGNbpLCjUFm1f+JKlY= github.com/sylabs/json-resp v0.8.0/go.mod h1:bUGV9cqShOyxz7RxBq03Yt9raKGfELKrfN6Yac3lfiw=