149 lines
2.9 KiB
Go
149 lines
2.9 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto"
|
|
_ "crypto/sha256"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/letsencrypt/boulder/core"
|
|
)
|
|
|
|
// 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 createRequest(cert *x509.Certificate) ([]byte, error) {
|
|
if !crypto.SHA256.Available() {
|
|
return nil, x509.ErrUnsupportedAlgorithm
|
|
}
|
|
h := crypto.SHA256.New()
|
|
|
|
h.Write(cert.RawIssuer)
|
|
issuerNameHash := h.Sum(nil)
|
|
|
|
req := certID{
|
|
pkix.AlgorithmIdentifier{ // SHA256
|
|
Algorithm: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1},
|
|
Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
|
|
},
|
|
issuerNameHash,
|
|
cert.AuthorityKeyId,
|
|
cert.SerialNumber,
|
|
}
|
|
|
|
return asn1.Marshal(req)
|
|
}
|
|
|
|
func parseResponse(resp *http.Response) (*core.RenewalInfo, error) {
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var res core.RenewalInfo
|
|
err = json.Unmarshal(body, &res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &res, nil
|
|
}
|
|
|
|
func checkARI(baseURL string, certPath string) (*core.RenewalInfo, error) {
|
|
cert, err := core.LoadCert(certPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := createRequest(cert)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/%s", baseURL, base64.RawURLEncoding.EncodeToString(req))
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ri, err := parseResponse(resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ri, nil
|
|
}
|
|
|
|
func getARIURL(directory string) (string, error) {
|
|
resp, err := http.Get(directory)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var dir struct {
|
|
RenewalInfo string `json:"renewalInfo"`
|
|
}
|
|
err = json.Unmarshal(body, &dir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return dir.RenewalInfo, nil
|
|
}
|
|
|
|
func main() {
|
|
flag.Usage = func() {
|
|
fmt.Fprintf(os.Stderr, `
|
|
checkari [-url https://acme.api/directory] FILE [FILE]...
|
|
|
|
Tool for querying ARI. Provide a list of filenames for certificates in PEM
|
|
format, and this tool will query for and output the suggested renewal window
|
|
for each certificate.
|
|
|
|
`)
|
|
flag.PrintDefaults()
|
|
}
|
|
directory := flag.String("url", "https://acme-v02.api.letsencrypt.org/directory", "ACME server's Directory URL")
|
|
flag.Parse()
|
|
if len(flag.Args()) == 0 {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
ariPath, err := getARIURL(*directory)
|
|
if err != nil {
|
|
fmt.Println(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
for _, cert := range flag.Args() {
|
|
fmt.Printf("%s:\n", cert)
|
|
window, err := checkARI(ariPath, cert)
|
|
if err != nil {
|
|
fmt.Printf("\t%s\n", err)
|
|
} else {
|
|
fmt.Printf("\tRenew after : %s\n", window.SuggestedWindow.Start)
|
|
fmt.Printf("\tRenew before: %s\n", window.SuggestedWindow.End)
|
|
}
|
|
}
|
|
}
|