ARI: Implement GET portion of draft-ietf-acme-ari-00 (#6322)
Update our ACME Renewal Info implementation to parse the CertID-based request format specified in the current version of the draft specification. Part of #6033
This commit is contained in:
		
							parent
							
								
									f98d74c14d
								
							
						
					
					
						commit
						73b72e8fa2
					
				| 
						 | 
				
			
			@ -3,22 +3,33 @@
 | 
			
		|||
package integration
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto"
 | 
			
		||||
	"crypto/ecdsa"
 | 
			
		||||
	"crypto/elliptic"
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"crypto/x509/pkix"
 | 
			
		||||
	"encoding/asn1"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math/big"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/letsencrypt/boulder/core"
 | 
			
		||||
	"github.com/letsencrypt/boulder/test"
 | 
			
		||||
	ocsp_helper "github.com/letsencrypt/boulder/test/ocsp/helper"
 | 
			
		||||
	"golang.org/x/crypto/ocsp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// certID matches the ASN.1 structure of the CertID sequence defined by RFC6960.
 | 
			
		||||
type certID struct {
 | 
			
		||||
	HashAlgorithm  pkix.AlgorithmIdentifier
 | 
			
		||||
	IssuerNameHash []byte
 | 
			
		||||
	IssuerKeyHash  []byte
 | 
			
		||||
	SerialNumber   *big.Int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestARI(t *testing.T) {
 | 
			
		||||
	t.Parallel()
 | 
			
		||||
	// This test is gated on the ServeRenewalInfo feature flag.
 | 
			
		||||
| 
						 | 
				
			
			@ -44,17 +55,25 @@ func TestARI(t *testing.T) {
 | 
			
		|||
	// Leverage OCSP to get components of ARI request path.
 | 
			
		||||
	issuer, err := ocsp_helper.GetIssuer(cert)
 | 
			
		||||
	test.AssertNotError(t, err, "failed to get issuer cert")
 | 
			
		||||
	ocspReqBytes, err := ocsp.CreateRequest(cert, issuer, nil)
 | 
			
		||||
	ocspReqBytes, err := ocsp.CreateRequest(cert, issuer, &ocsp.RequestOptions{Hash: crypto.SHA256})
 | 
			
		||||
	test.AssertNotError(t, err, "failed to build ocsp request")
 | 
			
		||||
	ocspReq, err := ocsp.ParseRequest(ocspReqBytes)
 | 
			
		||||
	test.AssertNotError(t, err, "failed to parse ocsp request")
 | 
			
		||||
 | 
			
		||||
	// Make ARI request.
 | 
			
		||||
	pathBytes, err := asn1.Marshal(certID{
 | 
			
		||||
		pkix.AlgorithmIdentifier{ // SHA256
 | 
			
		||||
			Algorithm:  asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1},
 | 
			
		||||
			Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
 | 
			
		||||
		},
 | 
			
		||||
		ocspReq.IssuerNameHash,
 | 
			
		||||
		ocspReq.IssuerKeyHash,
 | 
			
		||||
		cert.SerialNumber,
 | 
			
		||||
	})
 | 
			
		||||
	test.AssertNotError(t, err, "failed to marshal certID")
 | 
			
		||||
	url := fmt.Sprintf(
 | 
			
		||||
		"http://boulder:4001/get/draft-aaron-ari/renewalInfo/%s/%s/%s",
 | 
			
		||||
		hex.EncodeToString(ocspReq.IssuerKeyHash),
 | 
			
		||||
		hex.EncodeToString(ocspReq.IssuerNameHash),
 | 
			
		||||
		core.SerialToString(cert.SerialNumber),
 | 
			
		||||
		"http://boulder:4001/get/draft-ietf-acme-ari-00/renewalInfo/%s",
 | 
			
		||||
		base64.RawURLEncoding.EncodeToString(pathBytes),
 | 
			
		||||
	)
 | 
			
		||||
	resp, err := http.Get(url)
 | 
			
		||||
	test.AssertNotError(t, err, "ARI request should have succeeded")
 | 
			
		||||
| 
						 | 
				
			
			@ -73,17 +92,25 @@ func TestARI(t *testing.T) {
 | 
			
		|||
	// Get ARI path components.
 | 
			
		||||
	issuer, err = ocsp_helper.GetIssuer(cert)
 | 
			
		||||
	test.AssertNotError(t, err, "failed to get issuer cert")
 | 
			
		||||
	ocspReqBytes, err = ocsp.CreateRequest(cert, issuer, nil)
 | 
			
		||||
	ocspReqBytes, err = ocsp.CreateRequest(cert, issuer, &ocsp.RequestOptions{Hash: crypto.SHA256})
 | 
			
		||||
	test.AssertNotError(t, err, "failed to build ocsp request")
 | 
			
		||||
	ocspReq, err = ocsp.ParseRequest(ocspReqBytes)
 | 
			
		||||
	test.AssertNotError(t, err, "failed to parse ocsp request")
 | 
			
		||||
 | 
			
		||||
	// Make ARI request.
 | 
			
		||||
	pathBytes, err = asn1.Marshal(certID{
 | 
			
		||||
		pkix.AlgorithmIdentifier{ // SHA256
 | 
			
		||||
			Algorithm:  asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1},
 | 
			
		||||
			Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
 | 
			
		||||
		},
 | 
			
		||||
		ocspReq.IssuerNameHash,
 | 
			
		||||
		ocspReq.IssuerKeyHash,
 | 
			
		||||
		cert.SerialNumber,
 | 
			
		||||
	})
 | 
			
		||||
	test.AssertNotError(t, err, "failed to marshal certID")
 | 
			
		||||
	url = fmt.Sprintf(
 | 
			
		||||
		"http://boulder:4001/get/draft-aaron-ari/renewalInfo/%s/%s/%s",
 | 
			
		||||
		hex.EncodeToString(ocspReq.IssuerKeyHash),
 | 
			
		||||
		hex.EncodeToString(ocspReq.IssuerNameHash),
 | 
			
		||||
		core.SerialToString(cert.SerialNumber),
 | 
			
		||||
		"http://boulder:4001/get/draft-ietf-acme-ari-00/renewalInfo/%s",
 | 
			
		||||
		base64.RawURLEncoding.EncodeToString(pathBytes),
 | 
			
		||||
	)
 | 
			
		||||
	resp, err = http.Get(url)
 | 
			
		||||
	test.AssertNotError(t, err, "ARI request should have succeeded")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										47
									
								
								wfe2/wfe.go
								
								
								
								
							
							
						
						
									
										47
									
								
								wfe2/wfe.go
								
								
								
								
							| 
						 | 
				
			
			@ -3,10 +3,14 @@ package wfe2
 | 
			
		|||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/x509"
 | 
			
		||||
	"crypto/x509/pkix"
 | 
			
		||||
	"encoding/asn1"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"encoding/pem"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math/big"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
| 
						 | 
				
			
			@ -66,7 +70,7 @@ const (
 | 
			
		|||
	getCertPath      = getAPIPrefix + "cert/"
 | 
			
		||||
 | 
			
		||||
	// Draft or likely-to-change paths
 | 
			
		||||
	renewalInfoPath = getAPIPrefix + "draft-aaron-ari/renewalInfo/"
 | 
			
		||||
	renewalInfoPath = getAPIPrefix + "draft-ietf-acme-ari-00/renewalInfo/"
 | 
			
		||||
 | 
			
		||||
	// Non-ACME paths
 | 
			
		||||
	aiaIssuerPath = "/aia/issuer/"
 | 
			
		||||
| 
						 | 
				
			
			@ -2252,6 +2256,14 @@ func (wfe *WebFrontEndImpl) FinalizeOrder(ctx context.Context, logEvent *web.Req
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// certID matches the ASN.1 structure of the CertID sequence defined by RFC6960.
 | 
			
		||||
type certID struct {
 | 
			
		||||
	HashAlgorithm  pkix.AlgorithmIdentifier
 | 
			
		||||
	IssuerNameHash []byte
 | 
			
		||||
	IssuerKeyHash  []byte
 | 
			
		||||
	SerialNumber   *big.Int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RenewalInfo is used to get information about the suggested renewal window
 | 
			
		||||
// for the given certificate. It only accepts unauthenticated GET requests.
 | 
			
		||||
func (wfe *WebFrontEndImpl) RenewalInfo(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) {
 | 
			
		||||
| 
						 | 
				
			
			@ -2260,16 +2272,30 @@ func (wfe *WebFrontEndImpl) RenewalInfo(ctx context.Context, logEvent *web.Reque
 | 
			
		|||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	uid := strings.SplitN(request.URL.Path, "/", 3)
 | 
			
		||||
	if len(uid) != 3 {
 | 
			
		||||
		wfe.sendError(response, logEvent, probs.Malformed("Path did not include exactly issuerKeyHash, issuerNameHash, and serialNumber"), nil)
 | 
			
		||||
	// The path prefix has already been stripped, so request.URL.Path here is just
 | 
			
		||||
	// the base64url-encoded DER CertID sequence.
 | 
			
		||||
	der, err := base64.RawURLEncoding.DecodeString(request.URL.Path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		wfe.sendError(response, logEvent, probs.Malformed("Path was not base64url-encoded: %w", err), nil)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// For now, discard issuerKeyHash and issuerNameHash, because *we* know
 | 
			
		||||
	// (Boulder implementation-specific) that we do not re-use the same serial
 | 
			
		||||
	// number across multiple different issuers.
 | 
			
		||||
	serial := uid[2]
 | 
			
		||||
	var id certID
 | 
			
		||||
	rest, err := asn1.Unmarshal(der, &id)
 | 
			
		||||
	if err != nil || len(rest) != 0 {
 | 
			
		||||
		wfe.sendError(response, logEvent, probs.Malformed("Path was not a DER-encoded CertID sequence: %w", err), nil)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Verify that the hash algorithm is SHA-256, so people don't use SHA-1 here.
 | 
			
		||||
	if !id.HashAlgorithm.Algorithm.Equal(asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}) {
 | 
			
		||||
		wfe.sendError(response, logEvent, probs.Malformed("Request used hash algorithm other than SHA-256"), nil)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// We can do all of our processing based just on the serial, because Boulder
 | 
			
		||||
	// does not re-use the same serial across multiple issuers.
 | 
			
		||||
	serial := core.SerialToString(id.SerialNumber)
 | 
			
		||||
	if !core.ValidSerial(serial) {
 | 
			
		||||
		wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), nil)
 | 
			
		||||
		return
 | 
			
		||||
| 
						 | 
				
			
			@ -2289,6 +2315,11 @@ func (wfe *WebFrontEndImpl) RenewalInfo(ctx context.Context, logEvent *web.Reque
 | 
			
		|||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO(#6033): Consider parsing cert.Der, using that to get its IssuerNameID,
 | 
			
		||||
	// using that to look up the actual issuer cert in wfe.issuerCertificates,
 | 
			
		||||
	// using that to compute the actual issuerNameHash and issuerKeyHash, and
 | 
			
		||||
	// comparing those to the ones in the request.
 | 
			
		||||
 | 
			
		||||
	// This is a very simple renewal calculation: Calculate a point 2/3rds of the
 | 
			
		||||
	// way through the validity period, then give a 2-day window around that.
 | 
			
		||||
	validity := time.Unix(0, cert.Expires).Add(time.Second).Sub(time.Unix(0, cert.Issued))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,9 @@ import (
 | 
			
		|||
	"crypto/rand"
 | 
			
		||||
	"crypto/rsa"
 | 
			
		||||
	"crypto/x509"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"crypto/x509/pkix"
 | 
			
		||||
	"encoding/asn1"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"encoding/pem"
 | 
			
		||||
	"errors"
 | 
			
		||||
| 
						 | 
				
			
			@ -3513,36 +3515,80 @@ func TestARI(t *testing.T) {
 | 
			
		|||
	test.AssertNotError(t, err, "failed to load test issuer")
 | 
			
		||||
 | 
			
		||||
	// Take advantage of OCSP to build the issuer hashes.
 | 
			
		||||
	ocspReqBytes, err := ocsp.CreateRequest(cert, issuer, nil)
 | 
			
		||||
	ocspReqBytes, err := ocsp.CreateRequest(cert, issuer, &ocsp.RequestOptions{Hash: crypto.SHA256})
 | 
			
		||||
	test.AssertNotError(t, err, "failed to create ocsp request")
 | 
			
		||||
	ocspReq, err := ocsp.ParseRequest(ocspReqBytes)
 | 
			
		||||
	test.AssertNotError(t, err, "failed to parse ocsp request")
 | 
			
		||||
 | 
			
		||||
	// Ensure that a correct query results in a 200.
 | 
			
		||||
	path := fmt.Sprintf(
 | 
			
		||||
		"%s/%s/%s",
 | 
			
		||||
		hex.EncodeToString(ocspReq.IssuerKeyHash),
 | 
			
		||||
		hex.EncodeToString(ocspReq.IssuerNameHash),
 | 
			
		||||
		core.SerialToString(cert.SerialNumber),
 | 
			
		||||
	)
 | 
			
		||||
	req, event := makeGet(path, renewalInfoPath)
 | 
			
		||||
	idBytes, err := asn1.Marshal(certID{
 | 
			
		||||
		pkix.AlgorithmIdentifier{ // SHA256
 | 
			
		||||
			Algorithm:  asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1},
 | 
			
		||||
			Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
 | 
			
		||||
		},
 | 
			
		||||
		ocspReq.IssuerNameHash,
 | 
			
		||||
		ocspReq.IssuerKeyHash,
 | 
			
		||||
		cert.SerialNumber,
 | 
			
		||||
	})
 | 
			
		||||
	test.AssertNotError(t, err, "failed to marshal certID")
 | 
			
		||||
	req, event := makeGet(base64.RawURLEncoding.EncodeToString(idBytes), renewalInfoPath)
 | 
			
		||||
	resp := httptest.NewRecorder()
 | 
			
		||||
	wfe.RenewalInfo(context.Background(), event, resp, req)
 | 
			
		||||
	test.AssertEquals(t, resp.Code, 200)
 | 
			
		||||
	test.AssertEquals(t, resp.Code, http.StatusOK)
 | 
			
		||||
	test.AssertEquals(t, resp.Header().Get("Retry-After"), "21600")
 | 
			
		||||
 | 
			
		||||
	// Ensure that a mangled query (wrong serial) results in a 404.
 | 
			
		||||
	path = fmt.Sprintf(
 | 
			
		||||
		"%s/%s/%s",
 | 
			
		||||
		hex.EncodeToString(ocspReq.IssuerKeyHash),
 | 
			
		||||
		hex.EncodeToString(ocspReq.IssuerNameHash),
 | 
			
		||||
		core.SerialToString(big.NewInt(0).Add(cert.SerialNumber, big.NewInt(1))),
 | 
			
		||||
	)
 | 
			
		||||
	req, event = makeGet(path, renewalInfoPath)
 | 
			
		||||
	// Ensure that a query for a non-existent serial results in a 404.
 | 
			
		||||
	idBytes, err = asn1.Marshal(certID{
 | 
			
		||||
		pkix.AlgorithmIdentifier{ // SHA256
 | 
			
		||||
			Algorithm:  asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1},
 | 
			
		||||
			Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
 | 
			
		||||
		},
 | 
			
		||||
		ocspReq.IssuerNameHash,
 | 
			
		||||
		ocspReq.IssuerKeyHash,
 | 
			
		||||
		big.NewInt(0).Add(cert.SerialNumber, big.NewInt(1)),
 | 
			
		||||
	})
 | 
			
		||||
	test.AssertNotError(t, err, "failed to marshal certID")
 | 
			
		||||
	req, event = makeGet(base64.RawURLEncoding.EncodeToString(idBytes), renewalInfoPath)
 | 
			
		||||
	resp = httptest.NewRecorder()
 | 
			
		||||
	wfe.RenewalInfo(context.Background(), event, resp, req)
 | 
			
		||||
	test.AssertEquals(t, resp.Code, 404)
 | 
			
		||||
	test.AssertEquals(t, resp.Code, http.StatusNotFound)
 | 
			
		||||
	test.AssertEquals(t, resp.Header().Get("Retry-After"), "")
 | 
			
		||||
 | 
			
		||||
	// Ensure that a query with a bad hash algorithm fails.
 | 
			
		||||
	idBytes, err = asn1.Marshal(certID{
 | 
			
		||||
		pkix.AlgorithmIdentifier{ // SHA-1
 | 
			
		||||
			Algorithm:  asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26},
 | 
			
		||||
			Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
 | 
			
		||||
		},
 | 
			
		||||
		ocspReq.IssuerNameHash,
 | 
			
		||||
		ocspReq.IssuerKeyHash,
 | 
			
		||||
		big.NewInt(0).Add(cert.SerialNumber, big.NewInt(1)),
 | 
			
		||||
	})
 | 
			
		||||
	test.AssertNotError(t, err, "failed to marshal certID")
 | 
			
		||||
	req, event = makeGet(base64.RawURLEncoding.EncodeToString(idBytes), renewalInfoPath)
 | 
			
		||||
	resp = httptest.NewRecorder()
 | 
			
		||||
	wfe.RenewalInfo(context.Background(), event, resp, req)
 | 
			
		||||
	test.AssertEquals(t, resp.Code, http.StatusBadRequest)
 | 
			
		||||
 | 
			
		||||
	// Ensure that a query with a non-CertID path fails.
 | 
			
		||||
	req, event = makeGet(base64.RawURLEncoding.EncodeToString(ocspReqBytes), renewalInfoPath)
 | 
			
		||||
	resp = httptest.NewRecorder()
 | 
			
		||||
	wfe.RenewalInfo(context.Background(), event, resp, req)
 | 
			
		||||
	test.AssertEquals(t, resp.Code, http.StatusBadRequest)
 | 
			
		||||
 | 
			
		||||
	// Ensure that a query with a non-Base64URL path (including one in the old
 | 
			
		||||
	// request path style, which included slashes) fails.
 | 
			
		||||
	req, event = makeGet(
 | 
			
		||||
		fmt.Sprintf(
 | 
			
		||||
			"%s/%s/%s",
 | 
			
		||||
			base64.RawURLEncoding.EncodeToString(ocspReq.IssuerNameHash),
 | 
			
		||||
			base64.RawURLEncoding.EncodeToString(ocspReq.IssuerKeyHash),
 | 
			
		||||
			base64.RawURLEncoding.EncodeToString(cert.SerialNumber.Bytes()),
 | 
			
		||||
		),
 | 
			
		||||
		renewalInfoPath)
 | 
			
		||||
	resp = httptest.NewRecorder()
 | 
			
		||||
	wfe.RenewalInfo(context.Background(), event, resp, req)
 | 
			
		||||
	test.AssertEquals(t, resp.Code, http.StatusBadRequest)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO(#6011): Remove once TLS 1.0 and 1.1 support is gone.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue