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 }