Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
165 lines
5.6 KiB
165 lines
5.6 KiB
// Copyright 2020 Matthew Holt |
|
// |
|
// Licensed under the Apache License, Version 2.0 (the "License"); |
|
// you may not use this file except in compliance with the License. |
|
// You may obtain a copy of the License at |
|
// |
|
// http://www.apache.org/licenses/LICENSE-2.0 |
|
// |
|
// 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 acme |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"crypto" |
|
"crypto/x509" |
|
"encoding/base64" |
|
"fmt" |
|
"net/http" |
|
) |
|
|
|
// Certificate represents a certificate chain, which we usually refer |
|
// to as "a certificate" because in practice an end-entity certificate |
|
// is seldom useful/practical without a chain. |
|
type Certificate struct { |
|
// The certificate resource URL as provisioned by |
|
// the ACME server. Some ACME servers may split |
|
// the chain into multiple URLs that are Linked |
|
// together, in which case this URL represents the |
|
// starting point. |
|
URL string `json:"url"` |
|
|
|
// The PEM-encoded certificate chain, end-entity first. |
|
ChainPEM []byte `json:"-"` |
|
} |
|
|
|
// GetCertificateChain downloads all available certificate chains originating from |
|
// the given certURL. This is to be done after an order is finalized. |
|
// |
|
// "To download the issued certificate, the client simply sends a POST- |
|
// as-GET request to the certificate URL." |
|
// |
|
// "The server MAY provide one or more link relation header fields |
|
// [RFC8288] with relation 'alternate'. Each such field SHOULD express |
|
// an alternative certificate chain starting with the same end-entity |
|
// certificate. This can be used to express paths to various trust |
|
// anchors. Clients can fetch these alternates and use their own |
|
// heuristics to decide which is optimal." §7.4.2 |
|
func (c *Client) GetCertificateChain(ctx context.Context, account Account, certURL string) ([]Certificate, error) { |
|
if err := c.provision(ctx); err != nil { |
|
return nil, err |
|
} |
|
|
|
var chains []Certificate |
|
|
|
addChain := func(certURL string) (*http.Response, error) { |
|
// can't pool this buffer; bytes escape scope |
|
buf := new(bytes.Buffer) |
|
|
|
// TODO: set the Accept header? ("application/pem-certificate-chain") See end of §7.4.2 |
|
resp, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, certURL, nil, buf) |
|
if err != nil { |
|
return resp, err |
|
} |
|
contentType := parseMediaType(resp) |
|
|
|
switch contentType { |
|
case "application/pem-certificate-chain": |
|
chains = append(chains, Certificate{ |
|
URL: certURL, |
|
ChainPEM: buf.Bytes(), |
|
}) |
|
default: |
|
return resp, fmt.Errorf("unrecognized Content-Type from server: %s", contentType) |
|
} |
|
|
|
// "For formats that can only express a single certificate, the server SHOULD |
|
// provide one or more "Link: rel="up"" header fields pointing to an |
|
// issuer or issuers so that ACME clients can build a certificate chain |
|
// as defined in TLS (see Section 4.4.2 of [RFC8446])." (end of §7.4.2) |
|
allUp := extractLinks(resp, "up") |
|
for _, upURL := range allUp { |
|
upCerts, err := c.GetCertificateChain(ctx, account, upURL) |
|
if err != nil { |
|
return resp, fmt.Errorf("retrieving next certificate in chain: %s: %w", upURL, err) |
|
} |
|
for _, upCert := range upCerts { |
|
chains[len(chains)-1].ChainPEM = append(chains[len(chains)-1].ChainPEM, upCert.ChainPEM...) |
|
} |
|
} |
|
|
|
return resp, nil |
|
} |
|
|
|
// always add preferred/first certificate chain |
|
resp, err := addChain(certURL) |
|
if err != nil { |
|
return chains, err |
|
} |
|
|
|
// "The server MAY provide one or more link relation header fields |
|
// [RFC8288] with relation 'alternate'. Each such field SHOULD express |
|
// an alternative certificate chain starting with the same end-entity |
|
// certificate. This can be used to express paths to various trust |
|
// anchors. Clients can fetch these alternates and use their own |
|
// heuristics to decide which is optimal." §7.4.2 |
|
alternates := extractLinks(resp, "alternate") |
|
for _, altURL := range alternates { |
|
resp, err = addChain(altURL) |
|
if err != nil { |
|
return nil, fmt.Errorf("retrieving alternate certificate chain at %s: %w", altURL, err) |
|
} |
|
} |
|
|
|
return chains, nil |
|
} |
|
|
|
// RevokeCertificate revokes the given certificate. If the certificate key is not |
|
// provided, then the account key is used instead. See §7.6. |
|
func (c *Client) RevokeCertificate(ctx context.Context, account Account, cert *x509.Certificate, certKey crypto.Signer, reason int) error { |
|
if err := c.provision(ctx); err != nil { |
|
return err |
|
} |
|
|
|
body := struct { |
|
Certificate string `json:"certificate"` |
|
Reason int `json:"reason"` |
|
}{ |
|
Certificate: base64.RawURLEncoding.EncodeToString(cert.Raw), |
|
Reason: reason, |
|
} |
|
|
|
// "Revocation requests are different from other ACME requests in that |
|
// they can be signed with either an account key pair or the key pair in |
|
// the certificate." §7.6 |
|
kid := "" |
|
if certKey == account.PrivateKey { |
|
kid = account.Location |
|
} |
|
|
|
_, err := c.httpPostJWS(ctx, certKey, kid, c.dir.RevokeCert, body, nil) |
|
return err |
|
} |
|
|
|
// Reasons for revoking a certificate, as defined |
|
// by RFC 5280 §5.3.1. |
|
// https://tools.ietf.org/html/rfc5280#section-5.3.1 |
|
const ( |
|
ReasonUnspecified = iota // 0 |
|
ReasonKeyCompromise // 1 |
|
ReasonCACompromise // 2 |
|
ReasonAffiliationChanged // 3 |
|
ReasonSuperseded // 4 |
|
ReasonCessationOfOperation // 5 |
|
ReasonCertificateHold // 6 |
|
_ // 7 (unused) |
|
ReasonRemoveFromCRL // 8 |
|
ReasonPrivilegeWithdrawn // 9 |
|
ReasonAACompromise // 10 |
|
)
|
|
|