diff --git a/ctclient/client.go b/ctclient/client.go index d9b2d3a..cbf1c9b 100644 --- a/ctclient/client.go +++ b/ctclient/client.go @@ -22,6 +22,8 @@ import ( "net/http" "net/url" "time" + + "software.sslmate.com/src/certspotter/cttypes" ) var UserAgent = "" @@ -108,3 +110,55 @@ func getRoots(ctx context.Context, httpClient *http.Client, logURL *url.URL) ([] } return parsedResponse.Certificates, nil } + +func addChainOrPreChain(ctx context.Context, httpClient *http.Client, logURL *url.URL, isPreChain bool, chain [][]byte) (*cttypes.SignedCertificateTimestamp, error) { + var endpoint string + if isPreChain { + endpoint = "/ct/v1/add-pre-chain" + } else { + endpoint = "/ct/v1/add-chain" + } + fullURL := logURL.JoinPath(endpoint).String() + + requestBody, err := json.Marshal(struct { + Chain [][]byte `json:"chain"` + }{ + Chain: chain, + }) + if err != nil { + return nil, err + } + + request, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(requestBody)) + if err != nil { + return nil, err + } + request.Header.Set("User-Agent", UserAgent) + request.Header.Set("Content-Type", "application/json") + + if httpClient == nil { + httpClient = defaultHTTPClient + } + + response, err := httpClient.Do(request) + if err != nil { + return nil, err + } + + responseBody, err := io.ReadAll(response.Body) + response.Body.Close() + if err != nil { + return nil, fmt.Errorf("Post %q: error reading response: %w", fullURL, err) + } + + if response.StatusCode != 200 { + return nil, fmt.Errorf("Post %q: %s (%q)", fullURL, response.Status, bytes.TrimSpace(responseBody)) + } + + sct := new(cttypes.SignedCertificateTimestamp) + if err := json.Unmarshal(responseBody, sct); err != nil { + return nil, fmt.Errorf("Post %q: error parsing response JSON: %w", fullURL, err) + } + + return sct, nil +} diff --git a/ctclient/log.go b/ctclient/log.go index 93b2c88..d18d098 100644 --- a/ctclient/log.go +++ b/ctclient/log.go @@ -16,6 +16,12 @@ import ( "software.sslmate.com/src/certspotter/merkletree" ) +type WritableLog interface { + AddChain(context.Context, [][]byte) (*cttypes.SignedCertificateTimestamp, error) + AddPreChain(context.Context, [][]byte) (*cttypes.SignedCertificateTimestamp, error) + GetRoots(context.Context) ([][]byte, error) +} + type Log interface { GetSTH(context.Context) (*cttypes.SignedTreeHead, string, error) GetRoots(context.Context) ([][]byte, error) diff --git a/ctclient/rfc6962.go b/ctclient/rfc6962.go index 6ea4e74..0a7151b 100644 --- a/ctclient/rfc6962.go +++ b/ctclient/rfc6962.go @@ -31,6 +31,14 @@ type RFC6962LogEntry struct { Extra_data []byte `json:"extra_data"` } +func (ctlog *RFC6962Log) AddChain(ctx context.Context, chain [][]byte) (*cttypes.SignedCertificateTimestamp, error) { + return addChainOrPreChain(ctx, ctlog.HTTPClient, ctlog.URL, false, chain) +} + +func (ctlog *RFC6962Log) AddPreChain(ctx context.Context, chain [][]byte) (*cttypes.SignedCertificateTimestamp, error) { + return addChainOrPreChain(ctx, ctlog.HTTPClient, ctlog.URL, true, chain) +} + func (ctlog *RFC6962Log) GetSTH(ctx context.Context) (*cttypes.SignedTreeHead, string, error) { fullURL := ctlog.URL.JoinPath("/ct/v1/get-sth").String() sth := new(cttypes.SignedTreeHead) diff --git a/ctclient/static.go b/ctclient/static.go index 9242a01..bf527a4 100644 --- a/ctclient/static.go +++ b/ctclient/static.go @@ -46,6 +46,14 @@ type StaticLogEntry struct { chain [][32]byte } +func (ctlog *StaticLog) AddChain(ctx context.Context, chain [][]byte) (*cttypes.SignedCertificateTimestamp, error) { + return addChainOrPreChain(ctx, ctlog.HTTPClient, ctlog.SubmissionURL, false, chain) +} + +func (ctlog *StaticLog) AddPreChain(ctx context.Context, chain [][]byte) (*cttypes.SignedCertificateTimestamp, error) { + return addChainOrPreChain(ctx, ctlog.HTTPClient, ctlog.SubmissionURL, true, chain) +} + func (ctlog *StaticLog) GetSTH(ctx context.Context) (*cttypes.SignedTreeHead, string, error) { fullURL := ctlog.MonitoringURL.JoinPath("/checkpoint").String() responseBody, err := get(ctx, ctlog.HTTPClient, fullURL)