github.com/zmap/zcrypto@v0.0.0-20240512203510-0fef58d9a9db/x509/revocation/google/google.go (about)

     1  package google
     2  
     3  import (
     4  	"archive/zip"
     5  	"bytes"
     6  	"encoding/binary"
     7  	"encoding/hex"
     8  	"encoding/json"
     9  	"encoding/xml"
    10  	"errors"
    11  	"io/ioutil"
    12  	"math/big"
    13  	"net/http"
    14  	"net/url"
    15  
    16  	"github.com/zmap/zcrypto/x509"
    17  )
    18  
    19  // Provider specifies CRLSet provider interface
    20  type Provider interface {
    21  	FetchAndParse() (*CRLSet, error)
    22  }
    23  
    24  // CRLSet - data structure for storing CRLSet data, used by methods below
    25  type CRLSet struct {
    26  	Version      string
    27  	IssuerLists  map[string]*IssuerList
    28  	Sequence     int
    29  	NumParents   int
    30  	BlockedSPKIs []string
    31  }
    32  
    33  // IssuerList - list of revoked certificate entries for a given issuer
    34  type IssuerList struct {
    35  	SPKIHash string // SHA256 of Issuer SPKI
    36  	Entries  []*Entry
    37  }
    38  
    39  // Entry - entry for a single certificate
    40  type Entry struct {
    41  	SerialNumber *big.Int
    42  }
    43  
    44  // defaultProvider provides default Provider
    45  type defaultProvider struct {
    46  	requestURL string
    47  }
    48  
    49  // NewProvider returns default Provider
    50  func NewProvider(requestURL string) Provider {
    51  	return &defaultProvider{
    52  		requestURL: requestURL,
    53  	}
    54  }
    55  
    56  // FetchAndParse - fetch from distribution point, parse to CRLSet struct as defined above
    57  func FetchAndParse() (*CRLSet, error) {
    58  	return NewProvider(VersionRequestURL()).FetchAndParse()
    59  }
    60  
    61  // FetchAndParse - fetch from distribution point, parse to CRLSet struct as defined above
    62  func (p *defaultProvider) FetchAndParse() (*CRLSet, error) {
    63  	crlSetReader, version, err := Fetch(p.requestURL)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	return Parse(crlSetReader, version)
    68  }
    69  
    70  // Check - Given a parsed CRLSet, check if a given cert is present
    71  func (crlSet *CRLSet) Check(cert *x509.Certificate, issuerSPKIHash string) *Entry {
    72  	// check for BlockedSPKIs first
    73  	for _, spki := range crlSet.BlockedSPKIs {
    74  		if issuerSPKIHash == spki {
    75  			return &Entry{
    76  				SerialNumber: cert.SerialNumber,
    77  			}
    78  		}
    79  	}
    80  
    81  	issuersRevokedCerts := crlSet.IssuerLists[issuerSPKIHash]
    82  	if issuersRevokedCerts == nil { // no entries for this issuer
    83  		return nil
    84  	}
    85  	for _, entry := range issuersRevokedCerts.Entries {
    86  		if entry.SerialNumber.Cmp(cert.SerialNumber) == 0 {
    87  			return entry
    88  		} // cert not found if for loop completes
    89  	}
    90  	return nil
    91  }
    92  
    93  // Implementation details below - includes home-baked parsing of Google Update data,
    94  // originally found at https://github.com/agl/crlset-tools
    95  
    96  // Types for Google Update Data - used as wrapper for CRLSet
    97  type update struct {
    98  	XMLName xml.Name    `xml:"gupdate"`
    99  	Apps    []updateApp `xml:"app"`
   100  }
   101  
   102  type updateApp struct {
   103  	AppID       string `xml:"appid,attr"`
   104  	UpdateCheck updateCheck
   105  }
   106  
   107  type updateCheck struct {
   108  	XMLName xml.Name `xml:"updatecheck"`
   109  	URL     string   `xml:"codebase,attr"`
   110  	Version string   `xml:"version,attr"`
   111  }
   112  
   113  // crlSetAppID is the hex(ish) encoded public key hash of the key that signs
   114  // the CRL sets.
   115  const crlSetAppID = "hfnkpimlhhgieaddgfemjhofmfblmnib"
   116  
   117  // VersionRequestURL returns a URL from which the current CRLSet version
   118  // information can be fetched.
   119  func VersionRequestURL() string {
   120  	args := url.Values(make(map[string][]string))
   121  	args.Add("x", "id="+crlSetAppID+"&v=&uc"+"&acceptformat=crx3")
   122  
   123  	return (&url.URL{
   124  		Scheme:   "https",
   125  		Host:     "clients2.google.com",
   126  		Path:     "/service/update2/crx",
   127  		RawQuery: args.Encode(),
   128  	}).String()
   129  }
   130  
   131  // CRXHeader reflects the binary header of a CRX file.
   132  type CRXHeader struct {
   133  	Magic     [4]byte
   134  	Version   uint32
   135  	HeaderLen uint32
   136  }
   137  
   138  // ZipReader is a small wrapper around a []byte which implements ReadAt.
   139  type ZipReader []byte
   140  
   141  // ReadAt - Implementation of ReadAt for ZipReader App
   142  func (z ZipReader) ReadAt(p []byte, pos int64) (int, error) {
   143  	if int(pos) < 0 {
   144  		return 0, nil
   145  	}
   146  	return copy(p, []byte(z)[int(pos):]), nil
   147  }
   148  
   149  // Fetch returns reader to be passed to Parse
   150  func Fetch(url string) ([]byte, string, error) {
   151  	resp, err := http.Get(url)
   152  	if err != nil {
   153  		err = errors.New("Failed to get current version: " + err.Error())
   154  		return nil, "", err
   155  	}
   156  
   157  	var reply update
   158  	bodyBytes, err := ioutil.ReadAll(resp.Body)
   159  	resp.Body.Close()
   160  	if err != nil {
   161  		err = errors.New("Failed to read version reply: " + err.Error())
   162  		return nil, "", err
   163  	}
   164  	if err = xml.Unmarshal(bodyBytes, &reply); err != nil {
   165  		err = errors.New("Failed to parse version reply: " + err.Error())
   166  		return nil, "", err
   167  	}
   168  
   169  	var crxURL, version string
   170  	for _, app := range reply.Apps {
   171  		if app.AppID == crlSetAppID {
   172  			crxURL = app.UpdateCheck.URL
   173  			version = app.UpdateCheck.Version
   174  			break
   175  		}
   176  	}
   177  
   178  	if len(crxURL) == 0 {
   179  		err = errors.New("Failed to parse Omaha response")
   180  		return nil, version, err
   181  	}
   182  
   183  	resp, err = http.Get(crxURL)
   184  	if err != nil {
   185  		err = errors.New("Failed to get CRX: " + err.Error())
   186  		return nil, version, err
   187  	}
   188  	defer resp.Body.Close()
   189  
   190  	// zip needs to seek around, so we read the whole reply into memory.
   191  	crxBytes, err := ioutil.ReadAll(resp.Body)
   192  	if err != nil {
   193  		err = errors.New("Failed to download CRX: " + err.Error())
   194  		return nil, version, err
   195  	}
   196  	crx := bytes.NewBuffer(crxBytes)
   197  
   198  	var header CRXHeader
   199  	if err = binary.Read(crx, binary.LittleEndian, &header); err != nil {
   200  		err = errors.New("Failed to parse CRX header: " + err.Error())
   201  		return nil, version, err
   202  	}
   203  
   204  	if !bytes.Equal(header.Magic[:], []byte("Cr24")) || int(header.HeaderLen) < 0 {
   205  		err = errors.New("Downloaded file doesn't look like a CRX")
   206  		return nil, version, err
   207  	}
   208  
   209  	protoHeader := crx.Next(int(header.HeaderLen))
   210  	if len(protoHeader) != int(header.HeaderLen) {
   211  		err = errors.New("Downloaded file doesn't look like a CRX")
   212  		return nil, version, err
   213  	}
   214  
   215  	zipBytes := crx.Bytes()
   216  	zipReader := ZipReader(crx.Bytes())
   217  
   218  	z, err := zip.NewReader(zipReader, int64(len(zipBytes)))
   219  	if err != nil {
   220  		err = errors.New("Failed to parse ZIP file: " + err.Error())
   221  		return nil, version, err
   222  	}
   223  
   224  	var crlFile *zip.File
   225  	for _, file := range z.File {
   226  		if file.Name == "crl-set" {
   227  			crlFile = file
   228  			break
   229  		}
   230  	}
   231  
   232  	if crlFile == nil {
   233  		err = errors.New("Downloaded CRX didn't contain a CRLSet")
   234  		return nil, version, err
   235  	}
   236  
   237  	crlSetReader, err := crlFile.Open()
   238  	if err != nil {
   239  		err = errors.New("Failed to open crl-set in ZIP: " + err.Error())
   240  		return nil, version, err
   241  	}
   242  
   243  	raw, err := ioutil.ReadAll(crlSetReader)
   244  	if err != nil {
   245  		return nil, version, err
   246  	}
   247  
   248  	return raw, version, nil
   249  }
   250  
   251  // CRLSetHeader is used to parse the JSON header found in CRLSet files.
   252  type CRLSetHeader struct {
   253  	Sequence     int
   254  	NumParents   int
   255  	BlockedSPKIs []string
   256  }
   257  
   258  // RawEntry - structure for a raw CRLSet entry
   259  type RawEntry struct {
   260  	SPKIHash   [32]byte // SHA256 of Issuer SPKI
   261  	NumSerials uint32
   262  	Serials    []RawCRLSetSerial
   263  }
   264  
   265  // RawCRLSetSerial - structure of certificate serial number in a raw CRLSet entry
   266  type RawCRLSetSerial struct {
   267  	Len         uint8
   268  	SerialBytes []byte
   269  }
   270  
   271  // Parse - given a reader for a raw byte stream for a CRLSet,
   272  // parse the file into a usable CRLSet struct instance.
   273  // DUE TO THE DIFFICULTY OF RETRIEVING A CRLSET, IT IS HIGHLY RECOMMENDED
   274  // TO JUST USE THE FetchAndParseCRLSet FUNCTION PROVIDED ABOVE
   275  func Parse(in []byte, version string) (*CRLSet, error) {
   276  	header, remainingBytes, err := getHeader(in)
   277  	if err != nil {
   278  		return nil, err
   279  	}
   280  
   281  	rest := bytes.NewReader(remainingBytes)
   282  
   283  	crlSet := CRLSet{}
   284  	crlSet.IssuerLists = map[string]*IssuerList{}
   285  	crlSet.Sequence = header.Sequence
   286  	crlSet.Version = version
   287  	crlSet.NumParents = header.NumParents
   288  	crlSet.BlockedSPKIs = header.BlockedSPKIs
   289  
   290  	for rest.Len() > 0 {
   291  		rawEntry := RawEntry{}
   292  		issuerList := IssuerList{}
   293  		err := binary.Read(rest, binary.LittleEndian, &rawEntry.SPKIHash)
   294  		if err != nil {
   295  			return nil, err
   296  		}
   297  
   298  		issuerList.SPKIHash = hex.EncodeToString(rawEntry.SPKIHash[:])
   299  		crlSet.IssuerLists[issuerList.SPKIHash] = &issuerList
   300  
   301  		err = binary.Read(rest, binary.LittleEndian, &rawEntry.NumSerials)
   302  		if err != nil {
   303  			return nil, err
   304  		}
   305  
   306  		for i := uint32(0); i < rawEntry.NumSerials; i++ {
   307  			if rest.Len() < 1 {
   308  				err = errors.New("CRLSet truncated at serial length")
   309  				return nil, err
   310  			}
   311  			serial := RawCRLSetSerial{}
   312  			entry := Entry{}
   313  			issuerList.Entries = append(issuerList.Entries, &entry)
   314  			err = binary.Read(rest, binary.LittleEndian, &serial.Len)
   315  			if err != nil {
   316  				return nil, err
   317  			}
   318  
   319  			if rest.Len() < int(serial.Len) {
   320  				err = errors.New("CRLSet truncated at serial")
   321  				return nil, err
   322  			}
   323  
   324  			serialBytes := make([]byte, serial.Len)
   325  			err = binary.Read(rest, binary.LittleEndian, &serialBytes)
   326  			if err != nil {
   327  				return nil, err
   328  			}
   329  			serialNumber := new(big.Int)
   330  			serialNumber.SetBytes(serialBytes)
   331  			entry.SerialNumber = serialNumber
   332  		}
   333  	}
   334  
   335  	return &crlSet, nil
   336  }
   337  
   338  // internal method for parsing header when parsing a CRLSet
   339  func getHeader(c []byte) (header CRLSetHeader, rest []byte, err error) {
   340  	if len(c) < 2 {
   341  		err = errors.New("CRLSet truncated at header length")
   342  		return
   343  	}
   344  
   345  	headerLen := int(binary.LittleEndian.Uint16(c[0:]))
   346  	c = c[2:]
   347  
   348  	if len(c) < headerLen {
   349  		err = errors.New("CRLSet truncated at header")
   350  		return
   351  	}
   352  	headerBytes := c[:headerLen]
   353  	c = c[headerLen:]
   354  
   355  	if err = json.Unmarshal(headerBytes, &header); err != nil {
   356  		err = errors.New("Failed to parse header: " + err.Error())
   357  		return
   358  	}
   359  
   360  	return header, c, nil
   361  }