package microsoft

import (
	"bytes"
	"encoding/binary"
	"errors"
	"io"
	"io/ioutil"
	"math/big"

	"github.com/zmap/zcrypto/x509"
	"github.com/zmap/zcrypto/x509/pkix"
)

// distribution point http://ctldl.windowsupdate.com/msdownload/update/v3/static/trustedr/en/disallowedcert.sst

// DisallowedCerts - data structure for storing certificates revoked by
// Microsoft's intermediate revocation mechanism, disallowedcert.sst
type DisallowedCerts struct {
	IssuerLists map[string]*IssuerList
}

// IssuerList - List of revoked cert entries given issuer
type IssuerList struct {
	Issuer  pkix.Name
	Entries []*Entry
}

// Entry - Revocation Data for a single Certificate
type Entry struct {
	SerialNumber *big.Int
}

// Parse raw disallowedcert.sst and return an instance of DisallowedCerts,
// a struct for easy checking of revocation data for certs
func Parse(byteData []byte) (*DisallowedCerts, error) {
	return parse(byteData)
}

// Check - Given a parsed DisallowedCerts instance created by the Parse Func,
// check to see if a provided certificate has been revoked by this list
func Check(disallowed *DisallowedCerts, cert *x509.Certificate) *Entry {
	issuerStr := cert.Issuer.String()
	issuersRevokedCerts := disallowed.IssuerLists[issuerStr]
	if issuersRevokedCerts == nil { // no entries for this issuer
		return nil
	}
	for _, entry := range issuersRevokedCerts.Entries {
		if entry.SerialNumber.Cmp(cert.SerialNumber) == 0 {
			return entry
		} // cert not found if for loop completes
	}
	return nil
}

// Structs and methods below are original format for disallowedcert.sst,
// used for parsing raw data from the distribution point
// Implementation details below - includes home-baked parsing Microsoft sst format

// VBASigSerializedCertStore - The serialized digital certificate store specifies
//structures for storing a digital certificate store containing a single digital
// certificate and, optionally, a list of properties associated with the certificate.
type VBASigSerializedCertStore struct {
	Version          uint32  // must be 0x00000000
	Magic            [4]byte // must be CERT in ascii
	CertGroup        CertStoreCertificateGroup
	endMarkerElement EndElementMarkerEntry
}

// CertStoreCertificateGroup - Specifies a grouping of elements in a serialized
// digital certificate store that consists of zero or more properties of a
// certificate, and the serialized certificate itself.
type CertStoreCertificateGroup struct {
	elementList        []SerializedPropertyEntry
	certificateElement SerializedCertificateEntry
}

// SerializedPropertyEntry - Specifies an entry in a serialized digital
// certificate store that contains data for a property associated with a
// certificate in the store.
type SerializedPropertyEntry struct {
	ID           uint32 // MUST be less than or equal to 0x0000FFFF and MUST NOT be the value 0x00000000 or 0x00000020
	EncodingType uint32 // MUST be the value 0x00000001, which specifies ASN.1 encoding
	Length       uint32 // specifies the length of the value field
	Value        []byte
}

// SerializedCertificateEntry - Specifies an entry in a serialized digital
// certificate store that contains data for a property associated with a
// certificate in the store.
type SerializedCertificateEntry struct {
	ID           uint32 //MUST be 0x00000020
	EncodingType uint32 // MUST be the value 0x00000001, which specifies ASN.1 encoding
	Length       uint32 // specifies the length of the certificate field
	Certificate  []byte
}

// EndElementMarkerEntry - Specifies a special entry in a serialized digital
// certificate store that marks the end of the store.
type EndElementMarkerEntry struct {
	ID     uint32 // MUST be 0x00000000
	Marker uint64 // MUST be 0x0000000000000000
}

// Parse through a binary representation of a SST file,
// construct map
func parse(byteData []byte) (*DisallowedCerts, error) {
	bytesReader := bytes.NewReader(byteData)
	var certStore VBASigSerializedCertStore
	binary.Read(bytesReader, binary.LittleEndian, &certStore.Version)
	binary.Read(bytesReader, binary.LittleEndian, &certStore.Magic)

	if !bytes.Equal(certStore.Magic[:], []byte("CERT")) ||
		certStore.Version != 0 {
		err := errors.New("This file doesn't look like an SST")
		return nil, err
	}

	certs := [][]byte{}

	for { // read through element list and certificate elements
		var id uint32
		binary.Read(bytesReader, binary.LittleEndian, &id)
		if id == uint32(0) { // this is EndElementMarkerEntry, we have reached end of list
			break
		}
		var format uint32
		binary.Read(bytesReader, binary.LittleEndian, &format)
		var len uint32
		binary.Read(bytesReader, binary.LittleEndian, &len)
		if id == uint32(32) { // this is a SerializedCertificateEntry
			if format != uint32(1) {
				err := errors.New("SST does not use ASN1 encoding")
				return nil, err
			}
			certChain := make([]byte, len)
			binary.Read(bytesReader, binary.LittleEndian, &certChain)
			certs = append(certs, certChain)
		} else { // this is a SerializedPropertyEntry, so skip it
			io.CopyN(ioutil.Discard, bytesReader, int64(len)) // skip over value bytes
		}
	}

	disallowed := &DisallowedCerts{}
	disallowed.IssuerLists = map[string]*IssuerList{}

	for i := range certs {
		cert, _ := x509.ParseCertificate(certs[i])
		entry := &Entry{
			SerialNumber: cert.SerialNumber,
		}
		issuerStr := cert.Issuer.String()
		issuerList := disallowed.IssuerLists[issuerStr]
		if issuerList != nil { // if list already exists for this issuer, append
			issuerList.Entries = append(issuerList.Entries, entry)
		} else { // create new list for this issuer
			newList := &IssuerList{
				Issuer: cert.Issuer,
			}
			newList.Entries = append(newList.Entries, entry)
			disallowed.IssuerLists[issuerStr] = newList
		}
	}
	return disallowed, nil
}
