-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathlowlevel.go
More file actions
232 lines (206 loc) · 8.34 KB
/
Copy pathlowlevel.go
File metadata and controls
232 lines (206 loc) · 8.34 KB
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
// Copyright (C) 2022 Opsmate, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// Except as contained in this notice, the name(s) of the above copyright
// holders shall not be used in advertising or otherwise to promote the
// sale, use or other dealings in this Software without prior written
// authorization.
package ocsputil // import "software.sslmate.com/src/ocsputil"
import (
"bytes"
"context"
"crypto/x509"
"encoding/asn1"
"errors"
"fmt"
"golang.org/x/crypto/ocsp"
"io"
"net/http"
"strings"
"time"
)
var (
// ErrUnknown is returned when the certificate status is not good or revoked
ErrUnknown = errors.New("OCSP responder does not know this certificate")
// ErrNoResponder is returned when the certificte does not contain an HTTP OCSP responder URL
ErrNoResponder = errors.New("Certificate does not contain an HTTP OCSP responder URL")
// ErrNoCheck is returned when the certificate is an OCSP Responder certificate with the OCSP No Check extension
ErrNoCheck = errors.New("Certificate is an OCSP responder certificate with the OCSP No Check extension")
)
// The maximum amount of time to wait for an OCSP response, as specified by Section
// 4.10.2 of the Baseline Requirements: "The CA SHALL operate and maintain its CRL
// and OCSP capability with resources sufficient to provide a response time of ten
// seconds or less under normal operating conditions."
const QueryTimeout = 10 * time.Second
var oidOCSPNoCheck = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 48, 1, 5}
func getOCSPServer(cert *x509.Certificate) string {
for _, server := range cert.OCSPServer {
if strings.HasPrefix(server, "http://") {
return server
}
}
return ""
}
func isOCSPResponderCert(cert *x509.Certificate) bool {
for _, eku := range cert.ExtKeyUsage {
if eku == x509.ExtKeyUsageOCSPSigning {
return true
}
}
return false
}
func hasOCSPNoCheck(cert *x509.Certificate) bool {
for _, ext := range cert.Extensions {
if ext.Id.Equal(oidOCSPNoCheck) {
return true
}
}
return false
}
// Given a certificate, its issuer's subject, and its issuer's public key, return
// the parsed certificate and an issuer certificate suitable for passing to
// [CreateRequest] and [CheckResponse]. The returned issuerCert is not a fully-populated
// certificate and is only suitable for use with [CreateRequest] and [CheckResponse].
//
// cert can be a precertificate, but issuerSubject and issuerPubkeyBytes must be
// from the final certificate's issuer, not the precertificate's issuer.
//
// Returns an error if any of the arguments can't be parsed by the crypto/x509 package.
func ParseCertificate(certData []byte, issuerSubject []byte, issuerPubkeyBytes []byte) (cert *x509.Certificate, issuerCert *x509.Certificate, err error) {
cert, err = x509.ParseCertificate(certData)
if err != nil {
err = fmt.Errorf("unable to parse certificate: %w", err)
return
}
issuerPubkey, err := x509.ParsePKIXPublicKey(issuerPubkeyBytes)
if err != nil {
err = fmt.Errorf("unable to parse issuer public key: %w", err)
return
}
issuerCert = &x509.Certificate{
RawSubjectPublicKeyInfo: issuerPubkeyBytes,
RawSubject: issuerSubject,
PublicKey: issuerPubkey,
}
return
}
// Given a certificate and its issuer, return the "http://" OCSP server URL and
// an OCSP request suitable for passing to Query.
//
// cert can be a precertificate, but issuerCert must be the final certificate's issuer,
// not the precertificate's issuer.
//
// Returns [ErrNoResponder] if the certificate lacks an "http://" OCSP responder,
// [ErrNoCheck] if the certificate is an OCSP Responder certificate with the OCSP
// No Check extension, or an error from [golang.org/x/crypto/ocsp.CreateRequest]
func CreateRequest(cert *x509.Certificate, issuerCert *x509.Certificate) (serverURL string, requestBytes []byte, err error) {
serverURL = getOCSPServer(cert)
if serverURL == "" {
err = ErrNoResponder
return
}
if isOCSPResponderCert(cert) && hasOCSPNoCheck(cert) {
err = ErrNoCheck
return
}
requestBytes, err = ocsp.CreateRequest(cert, issuerCert, nil)
if err != nil {
err = fmt.Errorf("error creating OCSP request: %w", err)
return
}
return
}
// Given an OCSP server URL and an OCSP request (which can be created with [CreateRequest]),
// send the OCSP query using a POST request and return the response, which is suitable for
// passing to [CheckResponse]. The timeout for the query is defined by [QueryTimeout].
//
// If config is nil, a zero-value [Config] is used, which provides
// sensible defaults.
//
// Returns errors for the following conditions:
// - There's a problem parsing serverURL
// - There's an error from the HTTP client
// - There's an error reading the response
// - The HTTP response code is not 200
// - The Content-Type of the response is not "application/ocsp-response"
func Query(ctx context.Context, serverURL string, requestBytes []byte, config *Config) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, QueryTimeout)
defer cancel()
httpRequest, err := http.NewRequestWithContext(ctx, "POST", serverURL, bytes.NewBuffer(requestBytes))
if err != nil {
return nil, fmt.Errorf("error with OCSP responder URL: %w", err)
}
httpRequest.Header.Set("Content-Type", "application/ocsp-request")
httpRequest.Header.Set("User-Agent", config.userAgent())
httpRequest.Header["Idempotency-Key"] = nil // Forces net/http to retry on failure even though it's a POST request
httpResponse, err := config.httpClient().Do(httpRequest)
if err != nil {
return nil, fmt.Errorf("error querying OCSP responder over HTTP: %w", err)
}
body, err := io.ReadAll(httpResponse.Body)
httpResponse.Body.Close()
if err != nil {
return nil, fmt.Errorf("error reading response from OCSP responder: %w", err)
}
if httpResponse.StatusCode != 200 {
return nil, fmt.Errorf("HTTP error from OCSP responder: %s", httpResponse.Status)
}
if contentType := httpResponse.Header.Get("Content-Type"); contentType != "application/ocsp-response" {
return nil, fmt.Errorf("HTTP response header has invalid Content-Type value %s", contentType)
}
return body, nil
}
// Contains information about when and why a certificate was revoked
type RevocationInfo struct {
Time time.Time
Reason int
}
// Given a certificate, its issuer, and an OCSP response, parse the response and
// return if it was revoked.
//
// cert can be a precertificate, but issuerCert must be the final certificate's issuer,
// not the precertificate's issuer.
//
// Returns [ErrUnknown] if the response is neither good nor revoked, or an error
// from [golang.org/x/crypto/ocsp.ParseResponseForCert]
func CheckResponse(cert *x509.Certificate, issuerCert *x509.Certificate, responseBytes []byte) (revoked bool, info RevocationInfo, err error) {
response, err := ocsp.ParseResponseForCert(responseBytes, cert, issuerCert)
if err != nil {
err = fmt.Errorf("error parsing OCSP response: %w", err)
return
}
if isSHA1(response.SignatureAlgorithm) && !response.ProducedAt.Before(time.Date(2022, time.June, 1, 0, 0, 0, 0, time.UTC)) {
err = fmt.Errorf("signed using SHA-1")
return
}
if response.Status == ocsp.Good {
revoked = false
} else if response.Status == ocsp.Revoked {
revoked = true
info.Time = response.RevokedAt
info.Reason = response.RevocationReason
} else {
err = ErrUnknown
}
return
}
func isSHA1(algo x509.SignatureAlgorithm) bool {
return algo == x509.SHA1WithRSA || algo == x509.ECDSAWithSHA1
}