2
// Copyright 2021 The Sigstore Authors.
4
// Licensed under the Apache License, Version 2.0 (the "License");
5
// you may not use this file except in compliance with the License.
6
// You may obtain a copy of the License at
8
// http://www.apache.org/licenses/LICENSE-2.0
10
// Unless required by applicable law or agreed to in writing, software
11
// distributed under the License is distributed on an "AS IS" BASIS,
12
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
// See the License for the specific language governing permissions and
14
// limitations under the License.
32
type CertificateResponse struct {
38
type RootResponse struct {
44
Content []byte `json:"content"`
45
Algorithm string `json:"algorithm,omitempty"`
48
type CertificateRequest struct {
50
PublicKey Key `json:"publicKey"`
53
SignedEmailAddress []byte `json:"signedEmailAddress"`
56
CertificateSigningRequest []byte `json:"certificateSigningRequest"`
60
signingCertPath = "/api/v1/signingCert"
61
rootCertPath = "/api/v1/rootCert"
64
// SigstorePublicServerURL is the URL of Sigstore's public Fulcio service.
65
const SigstorePublicServerURL = "https://fulcio.sigstore.dev"
67
// LegacyClient is the interface for accessing the Fulcio API.
68
type LegacyClient interface {
69
// SigningCert sends the provided CertificateRequest to the /api/v1/signingCert
70
// endpoint of a Fulcio API, authenticated with the provided bearer token.
71
SigningCert(cr CertificateRequest, token string) (*CertificateResponse, error)
72
// RootCert sends a request to get the current CA used by Fulcio.
73
RootCert() (*RootResponse, error)
76
// ClientOption is a functional option for customizing static signatures.
77
type ClientOption func(*clientOptions)
79
// NewClient creates a new Fulcio API client talking to the provided URL.
80
func NewClient(url *url.URL, opts ...ClientOption) LegacyClient {
81
o := makeOptions(opts...)
86
Transport: createRoundTripper(http.DefaultTransport, o),
97
var _ LegacyClient = (*client)(nil)
99
// SigningCert implements Client
100
func (c *client) SigningCert(cr CertificateRequest, token string) (*CertificateResponse, error) {
101
// Construct the API endpoint for this handler
102
endpoint := *c.baseURL
103
endpoint.Path = path.Join(endpoint.Path, signingCertPath)
105
b, err := json.Marshal(cr)
107
return nil, fmt.Errorf("marshal: %w", err)
110
req, err := http.NewRequest(http.MethodPost, endpoint.String(), bytes.NewBuffer(b))
112
return nil, fmt.Errorf("request: %w", err)
114
// Set the authorization header to our OIDC bearer token.
115
req.Header.Set("Authorization", "Bearer "+token)
116
// Set the content-type to reflect we're sending JSON.
117
req.Header.Set("Content-Type", "application/json")
119
resp, err := c.client.Do(req)
121
return nil, fmt.Errorf("client: %w", err)
123
defer resp.Body.Close()
125
body, err := io.ReadAll(resp.Body)
127
return nil, fmt.Errorf("%s read: %w", endpoint.String(), err)
130
// The API should return a 201 Created on success. If we see anything else,
131
// then turn the response body into an error.
132
if resp.StatusCode != http.StatusCreated {
133
return nil, fmt.Errorf("%s %s returned %s: %q", http.MethodPost, endpoint.String(), resp.Status, body)
136
// Extract the SCT from the response header.
137
sct, err := base64.StdEncoding.DecodeString(resp.Header.Get("SCT"))
139
return nil, fmt.Errorf("decode: %w", err)
142
// Split the cert and the chain
143
certBlock, chainPem := pem.Decode(body)
144
if certBlock == nil {
145
return nil, errors.New("did not find a cert from Fulcio")
147
certPem := pem.EncodeToMemory(certBlock)
148
return &CertificateResponse{
155
func (c *client) RootCert() (*RootResponse, error) {
156
// Construct the API endpoint for this handler
157
endpoint := *c.baseURL
158
endpoint.Path = path.Join(endpoint.Path, rootCertPath)
160
req, err := http.NewRequest(http.MethodGet, endpoint.String(), nil)
162
return nil, fmt.Errorf("request: %w", err)
164
resp, err := c.client.Do(req)
168
defer resp.Body.Close()
169
body, err := io.ReadAll(resp.Body)
175
if resp.StatusCode != http.StatusOK {
176
return nil, errors.New(string(body))
178
return &RootResponse{ChainPEM: body}, nil
181
type clientOptions struct {
183
Timeout time.Duration
186
func makeOptions(opts ...ClientOption) *clientOptions {
191
for _, opt := range opts {
198
// WithTimeout sets the request timeout for the client
199
func WithTimeout(timeout time.Duration) ClientOption {
200
return func(o *clientOptions) {
205
// WithUserAgent sets the media type of the signature.
206
func WithUserAgent(userAgent string) ClientOption {
207
return func(o *clientOptions) {
208
o.UserAgent = userAgent
212
type roundTripper struct {
217
// RoundTrip implements `http.RoundTripper`
218
func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
219
req.Header.Set("User-Agent", rt.UserAgent)
220
return rt.RoundTripper.RoundTrip(req)
223
func createRoundTripper(inner http.RoundTripper, o *clientOptions) http.RoundTripper {
225
inner = http.DefaultTransport
227
if o.UserAgent == "" {
228
// There's nothing to do...
231
return &roundTripper{
233
UserAgent: o.UserAgent,