forked from globalsign/hvclient
-
Notifications
You must be signed in to change notification settings - Fork 0
/
client_api.go
575 lines (502 loc) · 16.8 KB
/
client_api.go
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
/*
Copyright (c) 2019-2021 GMO GlobalSign Pte. Ltd.
Licensed under the MIT License (the "License"); you may not use this file except
in compliance with the License. You may obtain a copy of the License at
https://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package hvclient
import (
"context"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net/http"
"net/url"
"time"
)
// counter is a reponse body from any HVCA request which returns a
// single count.
type counter struct {
Value int64 `json:"value"`
}
// claimsDNSRequest represents the body used for an HVCA request to assert domain control through DNS validation method
type claimsDNSRequest struct {
AuthorizationDomain string `json:"authorization_domain,omitempty"`
}
// claimsHTTPRequest represents the body used for an HVCA request to assert domain control through HTTP validation method
type claimsHTTPRequest struct {
AuthorizationDomain string `json:"authorization_domain,omitempty"`
Scheme string `json:"scheme"`
}
// claimsEmailRequest represents the body used for an HVCA request to assert domain control through Email validation method
type claimsEmailRequest struct {
EmailAddress string `json:"email_address"`
}
// AuthorisedEmails represents the body of a request returned when retrieving emails used for verifying DNS records
type AuthorisedEmails struct {
Constructed []string `json:"constructed"`
DNS DNSResults `json:"DNS"`
}
// DNSResults is a set of maps for all queried record types. Record types are the keys of the maps.
type DNSResults struct {
SOA SOAResults `json:"SOA"`
// TXT is not supported
// CAA is not supported
}
// SOAResults is a map of SOA records for DNS results
type SOAResults struct {
Emails []string `json:"emails"`
Errors []string `json:"errors,omitempty"`
}
// RevocationReason is a type for specifying the reason why a certificate is being
// revoked when requesting revocation.
type RevocationReason string
// Revocation reasons to provide when revoking a certificate and providing a
// reason for its revocation.
const (
RevocationReasonUnspecified = RevocationReason("unspecified")
RevocationReasonAffiliationChanged = RevocationReason("affiliationChanged")
RevocationReasonKeyCompromise = RevocationReason("keyCompromise")
RevocationReasonSuperseded = RevocationReason("superseded")
RevocationReasonCessationOfOperation = RevocationReason("cessationOfOperation")
RevocationReasonPrivilegeWithdrawn = RevocationReason("privilegeWithdrawn")
)
const (
// certSNHeaderName is the name of the HTTP header in which the
// URL of a certificate can be found.
certSNHeaderName = "Location"
// claimLocationHeaderName is the name of the HTTP header in which the
// URL of a claim can be found.
claimLocationHeaderName = "Location"
// totalCountHeaderName is the name of the HTTP header in which a total
// count field can be found.
totalCountHeaderName = "Total-Count"
)
// HVCA API endpoints.
const (
endpointCertificates = "/certificates"
endpointClaimsDomains = "/claims/domains"
endpointCountersCertificatesIssued = "/counters/certificates/issued"
endpointCountersCertificatesRevoked = "/counters/certificates/revoked"
endpointQuotasIssuance = "/quotas/issuance"
endpointStatsExpiring = "/stats/expiring"
endpointStatsIssued = "/stats/issued"
endpointStatsRevoked = "/stats/revoked"
endpointTrustChain = "/trustchain"
endpointPolicy = "/validationpolicy"
pathReassert = "/reassert"
pathDNS = "/dns"
pathHTTP = "/http"
pathEmail = "/email"
)
// CertificateRequest requests a new certificate based. The HVCA API is
// asynchronous, and on success this method returns the serial number of
// the new certificate. After a short delay, the certificate itself may be
// retrieved via the CertificateRetrieve method.
func (c *Client) CertificateRequest(
ctx context.Context,
req *Request,
) (*big.Int, error) {
var r, err = c.makeRequest(
ctx,
endpointCertificates,
http.MethodPost,
req,
nil,
)
if err != nil {
return nil, err
}
var snString string
snString, err = basePathHeaderFromResponse(r, certSNHeaderName)
if err != nil {
return nil, err
}
var sn, ok = big.NewInt(0).SetString(snString, 16)
if !ok {
return nil, fmt.Errorf("invalid serial number returned: %s", snString)
}
return sn, nil
}
// CertificateRetrieve retrieves a certificate.
func (c *Client) CertificateRetrieve(
ctx context.Context,
serial *big.Int,
) (*CertInfo, error) {
var r CertInfo
var _, err = c.makeRequest(
ctx,
endpointCertificates+"/"+url.QueryEscape(fmt.Sprintf("%X", serial)),
http.MethodGet,
nil,
&r,
)
if err != nil {
return nil, err
}
return &r, nil
}
// CertificateRevoke revokes a certificate.
func (c *Client) CertificateRevoke(
ctx context.Context,
serial *big.Int,
) error {
return c.CertificateRevokeWithReason(ctx, serial, RevocationReasonUnspecified, 0)
}
// CertificateRevokeWithReason revokes a certificate with a specified reason
// and UTC UNIX timestamp indicating when the private key was compromised if
// supported by the HVCA server. A special case holds when time is 0 which
// indicates that the current time should be used.
func (c *Client) CertificateRevokeWithReason(
ctx context.Context,
serial *big.Int,
reason RevocationReason,
time int64,
) error {
type certificatePatch struct {
RevocationReason RevocationReason `json:"revocation_reason"`
RevocationTime int64 `json:"revocation_time,omitempty"`
}
var patch = certificatePatch{
RevocationReason: reason,
RevocationTime: time,
}
var _, err = c.makeRequest(
ctx,
endpointCertificates+"/"+url.QueryEscape(fmt.Sprintf("%X", serial)),
http.MethodPatch,
&patch,
nil,
)
return err
}
// TrustChain returns the chain of trust for the certificates issued
// by the calling account.
func (c *Client) TrustChain(ctx context.Context) ([]*x509.Certificate, error) {
var chain []string
var _, err = c.makeRequest(
ctx,
endpointTrustChain,
http.MethodGet,
nil,
&chain,
)
if err != nil {
return nil, err
}
var certs []*x509.Certificate
for _, enc := range chain {
var block, rest = pem.Decode([]byte(enc))
if block == nil {
return nil, errors.New("invalid PEM in response")
} else if len(rest) > 0 {
return nil, errors.New("trailing data after PEM block in response")
}
var cert, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate in response: %w", err)
}
certs = append(certs, cert)
}
return certs, nil
}
// Policy returns the calling account's validation policy.
func (c *Client) Policy(ctx context.Context) (*Policy, error) {
var pol Policy
var _, err = c.makeRequest(
ctx,
endpointPolicy,
http.MethodGet,
nil,
&pol,
)
if err != nil {
return nil, err
}
return &pol, nil
}
// CounterCertsIssued returns the number of certificates issued
// by the calling account.
func (c *Client) CounterCertsIssued(ctx context.Context) (int64, error) {
return c.countersCommon(ctx, endpointCountersCertificatesIssued)
}
// CounterCertsRevoked returns the number of certificates revoked
// by the calling account.
func (c *Client) CounterCertsRevoked(ctx context.Context) (int64, error) {
return c.countersCommon(ctx, endpointCountersCertificatesRevoked)
}
// QuotaIssuance returns the remaining quota of certificate
// issuances for the calling account.
func (c *Client) QuotaIssuance(ctx context.Context) (int64, error) {
return c.countersCommon(ctx, endpointQuotasIssuance)
}
// countersCommon is the common method for all /counters and /quotas endpoints.
func (c *Client) countersCommon(
ctx context.Context,
path string,
) (int64, error) {
var count counter
var _, err = c.makeRequest(ctx, path, http.MethodGet, nil, &count)
if err != nil {
return 0, err
}
return count.Value, nil
}
// StatsExpiring returns a slice of the certificates which expired or which
// will expire during the specified time window, along with the total count
// of those certificates. The total count may be higher than the number of
// certificates in the slice if the total count is higher than the specified
// number of certificates per page. The HVCA API enforces a maximum number of
// certificates per page. If the total count is higher than the number of
// certificates in the slice, the remaining certificates may be retrieved
// by incrementing the page number in subsequent calls of this method.
func (c *Client) StatsExpiring(
ctx context.Context,
page, perPage int,
from, to time.Time,
) ([]CertMeta, int64, error) {
return c.statsCommon(ctx, endpointStatsExpiring, page, perPage, from, to)
}
// StatsIssued returns a slice of the certificates which were issued during
// the specified time window, along with the total count of those certificates.
// The total count may be higher than the number of certificates in the slice if
// the total count is higher than the specified number of certificates per
// page. The HVCA API enforces a maximum number of certificates per page. If
// the total count is higher than the number of certificates in the slice, the
// remaining certificates may be retrieved by incrementing the page number in
// subsequent calls of this method.
func (c *Client) StatsIssued(
ctx context.Context,
page, perPage int,
from, to time.Time,
) ([]CertMeta, int64, error) {
return c.statsCommon(ctx, endpointStatsIssued, page, perPage, from, to)
}
// StatsRevoked returns a slice of the certificates which were revoked during
// the specified time window, along with the total count of those certificates.
// The total count may be higher than the number of certificates in the slice if
// the total count is higher than the specified number of certificates per
// page. The HVCA API enforces a maximum number of certificates per page. If
// the total count is higher than the number of certificates in the slice, the
// remaining certificates may be retrieved by incrementing the page number in
// subsequent calls of this method.
func (c *Client) StatsRevoked(
ctx context.Context,
page, perPage int,
from, to time.Time,
) ([]CertMeta, int64, error) {
return c.statsCommon(ctx, endpointStatsRevoked, page, perPage, from, to)
}
// statsCommon is the common method for all /stats endpoints.
func (c *Client) statsCommon(
ctx context.Context,
path string,
page, perPage int,
from, to time.Time,
) ([]CertMeta, int64, error) {
var stats []CertMeta
var r, err = c.makeRequest(
ctx,
path+paginationString(page, perPage, from, to),
http.MethodGet,
nil,
&stats,
)
if err != nil {
return nil, 0, err
}
var count int64
count, err = intHeaderFromResponse(r, totalCountHeaderName)
if err != nil {
return nil, 0, err
}
return stats, count, nil
}
// ClaimsDomains returns a slice of either pending or verified domain claims
// along with the total count of domain claims in either category. The total
// count may be higher than the number of claims in the slice if the total
// count is higher than the specified number of claims per page. The HVCA API
// enforces a maximum number of claims per page. If the total count is higher
// than the number of claims in the slice, the remaining claims may be
// retrieved by incrementing the page number in subsequent calls of this
// method.
func (c *Client) ClaimsDomains(
ctx context.Context,
page, perPage int,
status ClaimStatus,
) ([]Claim, int64, error) {
var claims []Claim
var r, err = c.makeRequest(
ctx,
endpointClaimsDomains+
paginationString(page, perPage, time.Time{}, time.Time{})+
fmt.Sprintf("&status=%s", status),
http.MethodGet,
nil,
&claims,
)
if err != nil {
return nil, 0, err
}
var count int64
count, err = intHeaderFromResponse(r, totalCountHeaderName)
if err != nil {
return nil, 0, err
}
return claims, count, nil
}
// ClaimSubmit submits a new domain claim and returns the token value that
// should be used to verify control of that domain.
func (c *Client) ClaimSubmit(ctx context.Context, domain string) (*ClaimAssertionInfo, error) {
var info ClaimAssertionInfo
var r, err = c.makeRequest(
ctx,
endpointClaimsDomains+"/"+url.QueryEscape(domain),
http.MethodPost,
nil,
&info,
)
if err != nil {
return nil, err
}
var location string
location, err = basePathHeaderFromResponse(r, claimLocationHeaderName)
if err != nil {
return nil, err
}
info.ID = location
return &info, nil
}
// ClaimRetrieve returns a domain claim.
func (c *Client) ClaimRetrieve(ctx context.Context, id string) (*Claim, error) {
var claim Claim
var _, err = c.makeRequest(
ctx,
endpointClaimsDomains+"/"+url.QueryEscape(id),
http.MethodGet,
nil,
&claim,
)
if err != nil {
return nil, err
}
return &claim, nil
}
// ClaimDelete deletes a domain claim.
func (c *Client) ClaimDelete(ctx context.Context, id string) error {
var _, err = c.makeRequest(
ctx,
endpointClaimsDomains+"/"+url.QueryEscape(id),
http.MethodDelete,
nil,
nil,
)
return err
}
// ClaimDNS requests assertion of domain control using DNS once the appropriate
// token has been placed in the relevant DNS records. A return value of false
// indicates that the assertion request was created. A return value of true
// indicates that domain control was verified.
func (c *Client) ClaimDNS(ctx context.Context, id, authDomain string) (bool, error) {
var body interface{}
// The HVCA API documentation indicates that the request body is
// required, but practice suggests that it is not. The request does
// definitely fail if the empty string is provided as the authorization
// domain, however, so we'll only include the body in the request if
// an authorization domain was provided.
//
if authDomain != "" {
body = claimsDNSRequest{AuthorizationDomain: authDomain}
}
return c.claimAssert(ctx, body, id, pathDNS)
}
// ClaimHTTP requests assertion of domain control using HTTP once the appropriate
// token has been placed at the expected path. A return value of false
// indicates that the assertion request was created. A return value of true
// indicates that domain control was verified.
func (c *Client) ClaimHTTP(ctx context.Context, id, authDomain, scheme string) (bool, error) {
var body = claimsHTTPRequest{
AuthorizationDomain: authDomain,
Scheme: scheme,
}
return c.claimAssert(ctx, body, id, pathHTTP)
}
// ClaimEmail requests for an email with a verification link be sent to the
// provided emailAddress in order for the user to assert control of a domain by
// following the link inside the sent email. A return value of false indicates
// that the assertion request was created. A return value of true indicates
// that domain control was verified.
func (c *Client) ClaimEmail(ctx context.Context, id, emailAddress string) (bool, error) {
var body = claimsEmailRequest{
EmailAddress: emailAddress,
}
return c.claimAssert(ctx, body, id, pathEmail)
}
// ClaimEmailRetrieve retrieves a list of email addresses authorized to perform
// Email validation.
func (c *Client) ClaimEmailRetrieve(ctx context.Context, id string) (*AuthorisedEmails, error) {
var authorisedEmails AuthorisedEmails
var response, err = c.makeRequest(
ctx,
endpointClaimsDomains+"/"+url.QueryEscape(id)+pathEmail,
http.MethodGet,
nil,
&authorisedEmails,
)
if err != nil {
return nil, err
}
switch response.StatusCode {
case http.StatusOK:
return &authorisedEmails, nil
}
return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode)
}
// ClaimReassert reasserts an existing domain claim, for example if the
// assert-by time of a previous assertion request has expired.
func (c *Client) ClaimReassert(ctx context.Context, id string) (*ClaimAssertionInfo, error) {
var info ClaimAssertionInfo
var r, err = c.makeRequest(
ctx,
endpointClaimsDomains+"/"+url.QueryEscape(id)+pathReassert,
http.MethodPost,
nil,
&info,
)
if err != nil {
return nil, err
}
var location string
location, err = basePathHeaderFromResponse(r, claimLocationHeaderName)
if err != nil {
return nil, err
}
info.ID = location
return &info, err
}
func (c *Client) claimAssert(ctx context.Context, body interface{}, id, path string) (bool, error) {
var response, err = c.makeRequest(
ctx,
endpointClaimsDomains+"/"+url.QueryEscape(id)+path,
http.MethodPost,
body,
nil,
)
if err != nil {
return false, err
}
switch response.StatusCode {
case http.StatusCreated:
return false, nil
case http.StatusNoContent:
return true, nil
}
return false, fmt.Errorf("unexpected status code: %d", response.StatusCode)
}