ocsp: Add server-side OCSP processing
The current content of the ocsp module is focused on the processing needed to implement an OCSP client: it only implements request generation and response parsing.
This change adds response generation and request parsing.
Change-Id: Idf6f4e69af504520f2b58340734e45cd92bb3d60
Reviewed-on: https://go-review.googlesource.com/3666
Reviewed-by: Adam Langley <agl@golang.org>
diff --git a/ocsp/ocsp.go b/ocsp/ocsp.go
index 0252b58..d38fe0f 100644
--- a/ocsp/ocsp.go
+++ b/ocsp/ocsp.go
@@ -9,10 +9,15 @@
import (
"crypto"
- _ "crypto/sha1"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
+ "errors"
"math/big"
"time"
)
@@ -38,6 +43,21 @@
SerialNumber *big.Int
}
+// https://tools.ietf.org/html/rfc2560#section-4.1.1
+type ocspRequest struct {
+ TBSRequest tbsRequest
+}
+
+type tbsRequest struct {
+ Version int `asn1:"explicit,tag:0,default:0"`
+ RequestorName pkix.RDNSequence `asn1:"explicit,tag:1,optional"`
+ RequestList []request
+}
+
+type request struct {
+ Cert certID
+}
+
type responseASN1 struct {
Status asn1.Enumerated
Response responseBytes `asn1:"explicit,tag:0"`
@@ -58,7 +78,7 @@
type responseData struct {
Raw asn1.RawContent
Version int `asn1:"optional,default:1,explicit,tag:0"`
- RequestorName pkix.RDNSequence `asn1:"optional,explicit,tag:1"`
+ ResponderName pkix.RDNSequence `asn1:"optional,explicit,tag:1"`
KeyHash []byte `asn1:"optional,explicit,tag:2"`
ProducedAt time.Time
Responses []singleResponse
@@ -93,38 +113,121 @@
oidSignatureECDSAWithSHA512 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 4}
)
+var hashOIDs = map[crypto.Hash]asn1.ObjectIdentifier{
+ crypto.SHA1: asn1.ObjectIdentifier([]int{1, 3, 14, 3, 2, 26}),
+ crypto.SHA256: asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 1}),
+ crypto.SHA384: asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 2}),
+ crypto.SHA512: asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 3}),
+}
+
+// TODO(rlb): This is also from crypto/x509, so same comment as AGL's below
+var signatureAlgorithmDetails = []struct {
+ algo x509.SignatureAlgorithm
+ oid asn1.ObjectIdentifier
+ pubKeyAlgo x509.PublicKeyAlgorithm
+ hash crypto.Hash
+}{
+ {x509.MD2WithRSA, oidSignatureMD2WithRSA, x509.RSA, crypto.Hash(0) /* no value for MD2 */},
+ {x509.MD5WithRSA, oidSignatureMD5WithRSA, x509.RSA, crypto.MD5},
+ {x509.SHA1WithRSA, oidSignatureSHA1WithRSA, x509.RSA, crypto.SHA1},
+ {x509.SHA256WithRSA, oidSignatureSHA256WithRSA, x509.RSA, crypto.SHA256},
+ {x509.SHA384WithRSA, oidSignatureSHA384WithRSA, x509.RSA, crypto.SHA384},
+ {x509.SHA512WithRSA, oidSignatureSHA512WithRSA, x509.RSA, crypto.SHA512},
+ {x509.DSAWithSHA1, oidSignatureDSAWithSHA1, x509.DSA, crypto.SHA1},
+ {x509.DSAWithSHA256, oidSignatureDSAWithSHA256, x509.DSA, crypto.SHA256},
+ {x509.ECDSAWithSHA1, oidSignatureECDSAWithSHA1, x509.ECDSA, crypto.SHA1},
+ {x509.ECDSAWithSHA256, oidSignatureECDSAWithSHA256, x509.ECDSA, crypto.SHA256},
+ {x509.ECDSAWithSHA384, oidSignatureECDSAWithSHA384, x509.ECDSA, crypto.SHA384},
+ {x509.ECDSAWithSHA512, oidSignatureECDSAWithSHA512, x509.ECDSA, crypto.SHA512},
+}
+
+// TODO(rlb): This is also from crypto/x509, so same comment as AGL's below
+func signingParamsForPublicKey(pub interface{}, requestedSigAlgo x509.SignatureAlgorithm) (hashFunc crypto.Hash, sigAlgo pkix.AlgorithmIdentifier, err error) {
+ var pubType x509.PublicKeyAlgorithm
+
+ switch pub := pub.(type) {
+ case *rsa.PublicKey:
+ pubType = x509.RSA
+ hashFunc = crypto.SHA256
+ sigAlgo.Algorithm = oidSignatureSHA256WithRSA
+ sigAlgo.Parameters = asn1.RawValue{
+ Tag: 5,
+ }
+
+ case *ecdsa.PublicKey:
+ pubType = x509.ECDSA
+
+ switch pub.Curve {
+ case elliptic.P224(), elliptic.P256():
+ hashFunc = crypto.SHA256
+ sigAlgo.Algorithm = oidSignatureECDSAWithSHA256
+ case elliptic.P384():
+ hashFunc = crypto.SHA384
+ sigAlgo.Algorithm = oidSignatureECDSAWithSHA384
+ case elliptic.P521():
+ hashFunc = crypto.SHA512
+ sigAlgo.Algorithm = oidSignatureECDSAWithSHA512
+ default:
+ err = errors.New("x509: unknown elliptic curve")
+ }
+
+ default:
+ err = errors.New("x509: only RSA and ECDSA keys supported")
+ }
+
+ if err != nil {
+ return
+ }
+
+ if requestedSigAlgo == 0 {
+ return
+ }
+
+ found := false
+ for _, details := range signatureAlgorithmDetails {
+ if details.algo == requestedSigAlgo {
+ if details.pubKeyAlgo != pubType {
+ err = errors.New("x509: requested SignatureAlgorithm does not match private key type")
+ return
+ }
+ sigAlgo.Algorithm, hashFunc = details.oid, details.hash
+ if hashFunc == 0 {
+ err = errors.New("x509: cannot sign with hash function requested")
+ return
+ }
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ err = errors.New("x509: unknown SignatureAlgorithm")
+ }
+
+ return
+}
+
// TODO(agl): this is taken from crypto/x509 and so should probably be exported
// from crypto/x509 or crypto/x509/pkix.
func getSignatureAlgorithmFromOID(oid asn1.ObjectIdentifier) x509.SignatureAlgorithm {
- switch {
- case oid.Equal(oidSignatureMD2WithRSA):
- return x509.MD2WithRSA
- case oid.Equal(oidSignatureMD5WithRSA):
- return x509.MD5WithRSA
- case oid.Equal(oidSignatureSHA1WithRSA):
- return x509.SHA1WithRSA
- case oid.Equal(oidSignatureSHA256WithRSA):
- return x509.SHA256WithRSA
- case oid.Equal(oidSignatureSHA384WithRSA):
- return x509.SHA384WithRSA
- case oid.Equal(oidSignatureSHA512WithRSA):
- return x509.SHA512WithRSA
- case oid.Equal(oidSignatureDSAWithSHA1):
- return x509.DSAWithSHA1
- case oid.Equal(oidSignatureDSAWithSHA256):
- return x509.DSAWithSHA256
- case oid.Equal(oidSignatureECDSAWithSHA1):
- return x509.ECDSAWithSHA1
- case oid.Equal(oidSignatureECDSAWithSHA256):
- return x509.ECDSAWithSHA256
- case oid.Equal(oidSignatureECDSAWithSHA384):
- return x509.ECDSAWithSHA384
- case oid.Equal(oidSignatureECDSAWithSHA512):
- return x509.ECDSAWithSHA512
+ for _, details := range signatureAlgorithmDetails {
+ if oid.Equal(details.oid) {
+ return details.algo
+ }
}
return x509.UnknownSignatureAlgorithm
}
+// TODO(rlb): This is not taken from crypto/x509, but it's of the same general form.
+func getHashAlgorithmFromOID(target asn1.ObjectIdentifier) crypto.Hash {
+ for hash, oid := range hashOIDs {
+ if oid.Equal(target) {
+ return hash
+ }
+ }
+ return crypto.Hash(0)
+}
+
// This is the exposed reflection of the internal OCSP structures.
const (
@@ -138,6 +241,14 @@
ServerFailed = iota
)
+// Request represents an OCSP request. See RFC 2560.
+type Request struct {
+ HashAlgorithm crypto.Hash
+ IssuerNameHash []byte
+ IssuerKeyHash []byte
+ SerialNumber *big.Int
+}
+
// Response represents an OCSP response. See RFC 2560.
type Response struct {
// Status is one of {Good, Revoked, Unknown, ServerFailed}
@@ -169,6 +280,37 @@
return string(p)
}
+// ParseRequest parses an OCSP request in DER form. It only supports
+// requests for a single certificate. Signed requests are not supported.
+// If a request includes a signature, it will result in a ParseError.
+func ParseRequest(bytes []byte) (*Request, error) {
+ var req ocspRequest
+ rest, err := asn1.Unmarshal(bytes, &req)
+ if err != nil {
+ return nil, err
+ }
+ if len(rest) > 0 {
+ return nil, ParseError("trailing data in OCSP request")
+ }
+
+ if len(req.TBSRequest.RequestList) == 0 {
+ return nil, ParseError("OCSP request contains no request body")
+ }
+ innerRequest := req.TBSRequest.RequestList[0]
+
+ hashFunc := getHashAlgorithmFromOID(innerRequest.Cert.HashAlgorithm.Algorithm)
+ if hashFunc == crypto.Hash(0) {
+ return nil, ParseError("OCSP request uses unknown hash function")
+ }
+
+ return &Request{
+ HashAlgorithm: hashFunc,
+ IssuerNameHash: innerRequest.Cert.NameHash,
+ IssuerKeyHash: innerRequest.Cert.IssuerKeyHash,
+ SerialNumber: innerRequest.Cert.SerialNumber,
+ }, nil
+}
+
// ParseResponse parses an OCSP response in DER form. It only supports
// responses for a single certificate. If the response contains a certificate
// then the signature over the response is checked. If issuer is not nil then
@@ -255,20 +397,6 @@
return ret, nil
}
-// https://tools.ietf.org/html/rfc2560#section-4.1.1
-type ocspRequest struct {
- TBSRequest tbsRequest
-}
-
-type tbsRequest struct {
- Version int `asn1:"explicit,tag:0,default:0"`
- RequestList []request
-}
-
-type request struct {
- Cert certID
-}
-
// RequestOptions contains options for constructing OCSP requests.
type RequestOptions struct {
// Hash contains the hash function that should be used when
@@ -293,16 +421,8 @@
// used. I took the following from
// http://msdn.microsoft.com/en-us/library/ff635603.aspx
var hashOID asn1.ObjectIdentifier
- switch hashFunc {
- case crypto.SHA1:
- hashOID = asn1.ObjectIdentifier([]int{1, 3, 14, 3, 2, 26})
- case crypto.SHA256:
- hashOID = asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 1})
- case crypto.SHA384:
- hashOID = asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 2})
- case crypto.SHA512:
- hashOID = asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 3})
- default:
+ hashOID, ok := hashOIDs[hashFunc]
+ if !ok {
return nil, x509.ErrUnsupportedAlgorithm
}
@@ -345,3 +465,109 @@
},
})
}
+
+// CreateResponse returns a DER-encoded OCSP response with the specified contents.
+// The fields in the response are populated as follows:
+//
+// The responder cert is used to populate the ResponderName field, and the certificate
+// itself is provided alongside the OCSP response signature.
+//
+// The issuer cert is used to puplate the IssuerNameHash and IssuerKeyHash fields.
+// (SHA-1 is used for the hash function; this is not configurable.)
+//
+// The template is used to populate the SerialNumber, RevocationStatus, RevokedAt,
+// RevocationReason, ThisUpdate, and NextUpdate fields.
+//
+// The ProducedAt date is automatically set to the current date, to the nearest minute.
+func CreateResponse(issuer, responderCert *x509.Certificate, template Response, priv crypto.Signer) ([]byte, error) {
+ var publicKeyInfo struct {
+ Algorithm pkix.AlgorithmIdentifier
+ PublicKey asn1.BitString
+ }
+ if _, err := asn1.Unmarshal(issuer.RawSubjectPublicKeyInfo, &publicKeyInfo); err != nil {
+ return nil, err
+ }
+
+ h := sha1.New()
+ h.Write(publicKeyInfo.PublicKey.RightAlign())
+ issuerKeyHash := h.Sum(nil)
+
+ h.Reset()
+ h.Write(issuer.RawSubject)
+ issuerNameHash := h.Sum(nil)
+
+ innerResponse := singleResponse{
+ CertID: certID{
+ HashAlgorithm: pkix.AlgorithmIdentifier{
+ Algorithm: hashOIDs[crypto.SHA1],
+ Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
+ },
+ NameHash: issuerNameHash,
+ IssuerKeyHash: issuerKeyHash,
+ SerialNumber: template.SerialNumber,
+ },
+ ThisUpdate: template.ThisUpdate.UTC(),
+ NextUpdate: template.NextUpdate.UTC(),
+ }
+
+ switch template.Status {
+ case Good:
+ innerResponse.Good = true
+ case Unknown:
+ innerResponse.Unknown = true
+ case Revoked:
+ innerResponse.Revoked = revokedInfo{
+ RevocationTime: template.RevokedAt,
+ Reason: template.RevocationReason,
+ }
+ }
+
+ tbsResponseData := responseData{
+ ResponderName: responderCert.Subject.ToRDNSequence(),
+ ProducedAt: time.Now().Truncate(time.Minute),
+ Responses: []singleResponse{innerResponse},
+ }
+
+ tbsResponseDataDER, err := asn1.Marshal(tbsResponseData)
+ if err != nil {
+ return nil, err
+ }
+
+ hashFunc, signatureAlgorithm, err := signingParamsForPublicKey(priv.Public(), template.SignatureAlgorithm)
+ if err != nil {
+ return nil, err
+ }
+
+ responseHash := hashFunc.New()
+ responseHash.Write(tbsResponseDataDER)
+ signature, err := priv.Sign(rand.Reader, responseHash.Sum(nil), hashFunc)
+ if err != nil {
+ return nil, err
+ }
+
+ response := basicResponse{
+ TBSResponseData: tbsResponseData,
+ SignatureAlgorithm: signatureAlgorithm,
+ Signature: asn1.BitString{
+ Bytes: signature,
+ BitLength: 8 * len(signature),
+ },
+ }
+ if template.Certificate != nil {
+ response.Certificates = []asn1.RawValue{
+ asn1.RawValue{FullBytes: template.Certificate.Raw},
+ }
+ }
+ responseDER, err := asn1.Marshal(response)
+ if err != nil {
+ return nil, err
+ }
+
+ return asn1.Marshal(responseASN1{
+ Status: ocspSuccess,
+ Response: responseBytes{
+ ResponseType: idPKIXOCSPBasic,
+ Response: responseDER,
+ },
+ })
+}
diff --git a/ocsp/ocsp_test.go b/ocsp/ocsp_test.go
index fcdf6fe..449588c 100644
--- a/ocsp/ocsp_test.go
+++ b/ocsp/ocsp_test.go
@@ -6,7 +6,11 @@
import (
"bytes"
+ "crypto"
+ "crypto/sha1"
"crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/asn1"
"encoding/hex"
"math/big"
"reflect"
@@ -89,9 +93,124 @@
t.Fatal(err)
}
- expected, _ := hex.DecodeString(ocspRequestHex)
- if !bytes.Equal(request, expected) {
- t.Errorf("got %x, wanted %x", request, expected)
+ expectedBytes, _ := hex.DecodeString(ocspRequestHex)
+ if !bytes.Equal(request, expectedBytes) {
+ t.Errorf("request: got %x, wanted %x", request, expectedBytes)
+ }
+
+ decodedRequest, err := ParseRequest(expectedBytes)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if decodedRequest.HashAlgorithm != crypto.SHA1 {
+ t.Errorf("request.HashAlgorithm: got %v, want %v", decodedRequest.HashAlgorithm, crypto.SHA1)
+ }
+
+ var publicKeyInfo struct {
+ Algorithm pkix.AlgorithmIdentifier
+ PublicKey asn1.BitString
+ }
+ _, err = asn1.Unmarshal(issuer.RawSubjectPublicKeyInfo, &publicKeyInfo)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ h := sha1.New()
+ h.Write(publicKeyInfo.PublicKey.RightAlign())
+ issuerKeyHash := h.Sum(nil)
+
+ h.Reset()
+ h.Write(issuer.RawSubject)
+ issuerNameHash := h.Sum(nil)
+
+ if got := decodedRequest.IssuerKeyHash; !bytes.Equal(got, issuerKeyHash) {
+ t.Errorf("request.IssuerKeyHash: got %x, want %x", got, issuerKeyHash)
+ }
+
+ if got := decodedRequest.IssuerNameHash; !bytes.Equal(got, issuerNameHash) {
+ t.Errorf("request.IssuerKeyHash: got %x, want %x", got, issuerNameHash)
+ }
+
+ if got := decodedRequest.SerialNumber; got.Cmp(cert.SerialNumber) != 0 {
+ t.Errorf("request.SerialNumber: got %x, want %x", got, cert.SerialNumber)
+ }
+}
+
+func TestOCSPResponse(t *testing.T) {
+ leafCert, _ := hex.DecodeString(leafCertHex)
+ leaf, err := x509.ParseCertificate(leafCert)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ issuerCert, _ := hex.DecodeString(issuerCertHex)
+ issuer, err := x509.ParseCertificate(issuerCert)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ responderCert, _ := hex.DecodeString(responderCertHex)
+ responder, err := x509.ParseCertificate(responderCert)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ responderPrivateKeyDER, _ := hex.DecodeString(responderPrivateKeyHex)
+ responderPrivateKey, err := x509.ParsePKCS1PrivateKey(responderPrivateKeyDER)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ producedAt := time.Now().Truncate(time.Minute)
+ thisUpdate := time.Date(2010, 7, 7, 15, 1, 5, 0, time.UTC)
+ nextUpdate := time.Date(2010, 7, 7, 18, 35, 17, 0, time.UTC)
+ template := Response{
+ Status: Revoked,
+ SerialNumber: leaf.SerialNumber,
+ ThisUpdate: thisUpdate,
+ NextUpdate: nextUpdate,
+ RevokedAt: thisUpdate,
+ RevocationReason: 1, // keyCompromise
+ Certificate: responder,
+ }
+
+ responseBytes, err := CreateResponse(issuer, responder, template, responderPrivateKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ resp, err := ParseResponse(responseBytes, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !reflect.DeepEqual(resp.ThisUpdate, template.ThisUpdate) {
+ t.Errorf("resp.ThisUpdate: got %d, want %d", resp.ThisUpdate, template.ThisUpdate)
+ }
+
+ if !reflect.DeepEqual(resp.NextUpdate, template.NextUpdate) {
+ t.Errorf("resp.NextUpdate: got %d, want %d", resp.NextUpdate, template.NextUpdate)
+ }
+
+ if !reflect.DeepEqual(resp.RevokedAt, template.RevokedAt) {
+ t.Errorf("resp.NextUpdate: got %d, want %d", resp.NextUpdate, template.NextUpdate)
+ }
+
+ if !reflect.DeepEqual(resp.ProducedAt, producedAt) {
+ t.Errorf("resp.NextUpdate: got %d, want %d", resp.NextUpdate, template.NextUpdate)
+ }
+
+ if resp.Status != template.Status {
+ t.Errorf("resp.Status: got %d, want %d", resp.Status, template.Status)
+ }
+
+ if resp.SerialNumber.Cmp(template.SerialNumber) != 0 {
+ t.Errorf("resp.SerialNumber: got %x, want %x", resp.SerialNumber, template.SerialNumber)
+ }
+
+ if resp.RevocationReason != template.RevocationReason {
+ t.Errorf("resp.RevocationReason: got %d, want %d", resp.RevocationReason, template.RevocationReason)
}
}
@@ -272,3 +391,63 @@
"f5d93b0ab598fab378b91ef22b4c62d5fdb27a1ddf33fd73f9a5d82d8c2aead1fcb028b6" +
"e94948134b838a1b487b24f738de6f4154b8ab576b06dfc7a2d4a9f6f136628088f28b75" +
"d68071"
+
+// Key and certificate for the OCSP responder were not taken from the Thawte
+// responder, since CreateResponse requires that we have the private key.
+// Instead, they were generated randomly.
+const responderPrivateKeyHex = "308204a40201000282010100e8155f2d3e6f2e8d14c62a788bd462f9f844e7a6977c83ef" +
+ "1099f0f6616ec5265b56f356e62c5400f0b06a2e7945a82752c636df32a895152d6074df" +
+ "1701dc6ccfbcbec75a70bd2b55ae2be7e6cad3b5fd4cd5b7790ab401a436d3f5f346074f" +
+ "fde8a99d5b723350f0a112076614b12ef79c78991b119453445acf2416ab0046b540db14" +
+ "c9fc0f27b8989ad0f63aa4b8aefc91aa8a72160c36307c60fec78a93d3fddf4259902aa7" +
+ "7e7332971c7d285b6a04f648993c6922a3e9da9adf5f81508c3228791843e5d49f24db2f" +
+ "1290bafd97e655b1049a199f652cd603c4fafa330c390b0da78fbbc67e8fa021cbd74eb9" +
+ "6222b12ace31a77dcf920334dc94581b02030100010282010100bcf0b93d7238bda329a8" +
+ "72e7149f61bcb37c154330ccb3f42a85c9002c2e2bdea039d77d8581cd19bed94078794e" +
+ "56293d601547fc4bf6a2f9002fe5772b92b21b254403b403585e3130cc99ccf08f0ef81a" +
+ "575b38f597ba4660448b54f44bfbb97072b5a2bf043bfeca828cf7741d13698e3f38162b" +
+ "679faa646b82abd9a72c5c7d722c5fc577a76d2c2daac588accad18516d1bbad10b0dfa2" +
+ "05cfe246b59e28608a43942e1b71b0c80498075121de5b900d727c31c42c78cf1db5c0aa" +
+ "5b491e10ea4ed5c0962aaf2ae025dd81fa4ce490d9d6b4a4465411d8e542fc88617e5695" +
+ "1aa4fc8ea166f2b4d0eb89ef17f2b206bd5f1014bf8fe0e71fe62f2cccf102818100f2dc" +
+ "ddf878d553286daad68bac4070a82ffec3dc4666a2750f47879eec913f91836f1d976b60" +
+ "daf9356e078446dafab5bd2e489e5d64f8572ba24a4ba4f3729b5e106c4dd831cc2497a7" +
+ "e6c7507df05cb64aeb1bbc81c1e340d58b5964cf39cff84ea30c29ec5d3f005ee1362698" +
+ "07395037955955655292c3e85f6187fa1f9502818100f4a33c102630840705f8c778a47b" +
+ "87e8da31e68809af981ac5e5999cf1551685d761cdf0d6520361b99aebd5777a940fa64d" +
+ "327c09fa63746fbb3247ec73a86edf115f1fe5c83598db803881ade71c33c6e956118345" +
+ "497b98b5e07bb5be75971465ec78f2f9467e1b74956ca9d4c7c3e314e742a72d8b33889c" +
+ "6c093a466cef0281801d3df0d02124766dd0be98349b19eb36a508c4e679e793ba0a8bef" +
+ "4d786888c1e9947078b1ea28938716677b4ad8c5052af12eb73ac194915264a913709a0b" +
+ "7b9f98d4a18edd781a13d49899f91c20dbd8eb2e61d991ba19b5cdc08893f5cb9d39e5a6" +
+ "0629ea16d426244673b1b3ee72bd30e41fac8395acac40077403de5efd028180050731dd" +
+ "d71b1a2b96c8d538ba90bb6b62c8b1c74c03aae9a9f59d21a7a82b0d572ef06fa9c807bf" +
+ "c373d6b30d809c7871df96510c577421d9860c7383fda0919ece19996b3ca13562159193" +
+ "c0c246471e287f975e8e57034e5136aaf44254e2650def3d51292474c515b1588969112e" +
+ "0a85cc77073e9d64d2c2fc497844284b02818100d71d63eabf416cf677401ebf965f8314" +
+ "120b568a57dd3bd9116c629c40dc0c6948bab3a13cc544c31c7da40e76132ef5dd3f7534" +
+ "45a635930c74326ae3df0edd1bfb1523e3aa259873ac7cf1ac31151ec8f37b528c275622" +
+ "48f99b8bed59fd4da2576aa6ee20d93a684900bf907e80c66d6e2261ae15e55284b4ed9d" +
+ "6bdaa059"
+
+const responderCertHex = "308202e2308201caa003020102020101300d06092a864886f70d01010b05003019311730" +
+ "150603550403130e4f43535020526573706f6e646572301e170d31353031333031353530" +
+ "33335a170d3136303133303135353033335a3019311730150603550403130e4f43535020" +
+ "526573706f6e64657230820122300d06092a864886f70d01010105000382010f00308201" +
+ "0a0282010100e8155f2d3e6f2e8d14c62a788bd462f9f844e7a6977c83ef1099f0f6616e" +
+ "c5265b56f356e62c5400f0b06a2e7945a82752c636df32a895152d6074df1701dc6ccfbc" +
+ "bec75a70bd2b55ae2be7e6cad3b5fd4cd5b7790ab401a436d3f5f346074ffde8a99d5b72" +
+ "3350f0a112076614b12ef79c78991b119453445acf2416ab0046b540db14c9fc0f27b898" +
+ "9ad0f63aa4b8aefc91aa8a72160c36307c60fec78a93d3fddf4259902aa77e7332971c7d" +
+ "285b6a04f648993c6922a3e9da9adf5f81508c3228791843e5d49f24db2f1290bafd97e6" +
+ "55b1049a199f652cd603c4fafa330c390b0da78fbbc67e8fa021cbd74eb96222b12ace31" +
+ "a77dcf920334dc94581b0203010001a3353033300e0603551d0f0101ff04040302078030" +
+ "130603551d25040c300a06082b06010505070309300c0603551d130101ff04023000300d" +
+ "06092a864886f70d01010b05000382010100718012761b5063e18f0dc44644d8e6ab8612" +
+ "31c15fd5357805425d82aec1de85bf6d3e30fce205e3e3b8b795bbe52e40a439286d2288" +
+ "9064f4aeeb150359b9425f1da51b3a5c939018555d13ac42c565a0603786a919328f3267" +
+ "09dce52c22ad958ecb7873b9771d1148b1c4be2efe80ba868919fc9f68b6090c2f33c156" +
+ "d67156e42766a50b5d51e79637b7e58af74c2a951b1e642fa7741fec982cc937de37eff5" +
+ "9e2005d5939bfc031589ca143e6e8ab83f40ee08cc20a6b4a95a318352c28d18528dcaf9" +
+ "66705de17afa19d6e8ae91ddf33179d16ebb6ac2c69cae8373d408ebf8c55308be6c04d9" +
+ "3a25439a94299a65a709756c7a3e568be049d5c38839"