package metadata import ( "crypto/x509" "encoding/base64" "errors" "fmt" "io" "net/http" "strings" "time" "github.com/golang-jwt/jwt/v5" "github.com/mitchellh/mapstructure" "github.com/sonr-io/common/webauthn/revoke" ) // NewDecoder returns a new metadata decoder. func NewDecoder(opts ...DecoderOption) (decoder *Decoder, err error) { decoder = &Decoder{ client: &http.Client{}, parser: jwt.NewParser(), hook: mapstructure.ComposeDecodeHookFunc(), } for _, opt := range opts { if err = opt(decoder); err != nil { return nil, fmt.Errorf("failed to apply decoder option: %w", err) } } if decoder.root == "" { decoder.root = ProductionMDSRoot } return decoder, nil } // Decoder handles decoding and specialized parsing of the metadata blob. type Decoder struct { client *http.Client parser *jwt.Parser hook mapstructure.DecodeHookFunc root string ignoreEntryParsingErrors bool } // Parse handles parsing of the raw JSON values of the metadata blob. Should be used after using [Decoder.Decode] or // [Decoder.DecodeBytes]. func (d *Decoder) Parse(payload *PayloadJSON) (metadata *Metadata, err error) { metadata = &Metadata{ Parsed: Parsed{ LegalHeader: payload.LegalHeader, Number: payload.Number, }, } if metadata.Parsed.NextUpdate, err = time.Parse(time.DateOnly, payload.NextUpdate); err != nil { return nil, fmt.Errorf( "error occurred parsing next update value '%s': %w", payload.NextUpdate, err, ) } var parsed Entry for _, entry := range payload.Entries { if parsed, err = entry.Parse(); err != nil { metadata.Unparsed = append(metadata.Unparsed, EntryError{ Error: err, EntryJSON: entry, }) continue } metadata.Parsed.Entries = append(metadata.Parsed.Entries, parsed) } if n := len(metadata.Unparsed); n != 0 && !d.ignoreEntryParsingErrors { return metadata, fmt.Errorf( "error occurred parsing metadata: %d entries had errors during parsing", n, ) } return metadata, nil } // Decode the blob from an [io.Reader]. This function will close the [io.ReadCloser] after completing. func (d *Decoder) Decode(r io.Reader) (payload *PayloadJSON, err error) { bytes, err := io.ReadAll(r) if err != nil { return nil, err } return d.DecodeBytes(bytes) } // DecodeBytes handles decoding raw bytes. If you have a read closer it's suggested to use [Decoder.Decode]. func (d *Decoder) DecodeBytes(bytes []byte) (payload *PayloadJSON, err error) { var token *jwt.Token if token, err = d.parser.Parse(string(bytes), func(token *jwt.Token) (any, error) { // 2. If the x5u attribute is present in the JWT Header, then if _, ok := token.Header[HeaderX509URI].([]any); ok { // never seen an x5u here, although it is in the spec return nil, errors.New("x5u encountered in header of metadata TOC payload") } // 3. If the x5u attribute is missing, the chain should be retrieved from the x5c attribute. var ( x5c, chain []any ok, valid bool ) if x5c, ok = token.Header[HeaderX509Certificate].([]any); !ok { // If that attribute is missing as well, Metadata TOC signing trust anchor is considered the TOC signing certificate chain. chain = []any{d.root} } else { chain = x5c } // The certificate chain MUST be verified to properly chain to the metadata TOC signing trust anchor. if valid, err = validateChain(d.root, chain); !valid || err != nil { return nil, err } // Chain validated, extract the TOC signing certificate from the chain. Create a buffer large enough to hold the // certificate bytes. o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string)))) var ( n int cert *x509.Certificate ) // Decode the base64 certificate into the buffer. if n, err = base64.StdEncoding.Decode(o, []byte(chain[0].(string))); err != nil { return nil, err } // Parse the certificate from the buffer. if cert, err = x509.ParseCertificate(o[:n]); err != nil { return nil, err } // 4. Verify the signature of the Metadata TOC object using the TOC signing certificate chain // jwt.Parse() uses the TOC signing certificate public key internally to verify the signature. return cert.PublicKey, err }); err != nil { return nil, err } var decoder *mapstructure.Decoder payload = &PayloadJSON{} if decoder, err = mapstructure.NewDecoder(&mapstructure.DecoderConfig{ Metadata: nil, Result: payload, DecodeHook: d.hook, TagName: "json", }); err != nil { return nil, err } if err = decoder.Decode(token.Claims); err != nil { return payload, err } return payload, nil } // DecoderOption is a representation of a function that can set options within a decoder. type DecoderOption func(decoder *Decoder) (err error) // WithIgnoreEntryParsingErrors is a DecoderOption which ignores errors when parsing individual entries. The values for // these entries will exist as an unparsed entry. func WithIgnoreEntryParsingErrors() DecoderOption { return func(decoder *Decoder) (err error) { decoder.ignoreEntryParsingErrors = true return nil } } // WithRootCertificate overrides the root certificate used to validate the authenticity of the metadata payload. func WithRootCertificate(value string) DecoderOption { return func(decoder *Decoder) (err error) { decoder.root = value return nil } } func validateChain(root string, chain []any) (bool, error) { oRoot := make([]byte, base64.StdEncoding.DecodedLen(len(root))) nRoot, err := base64.StdEncoding.Decode(oRoot, []byte(root)) if err != nil { return false, err } rootcert, err := x509.ParseCertificate(oRoot[:nRoot]) if err != nil { return false, err } roots := x509.NewCertPool() roots.AddCert(rootcert) o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[1].(string)))) n, err := base64.StdEncoding.Decode(o, []byte(chain[1].(string))) if err != nil { return false, err } intcert, err := x509.ParseCertificate(o[:n]) if err != nil { return false, err } if revoked, ok := revoke.VerifyCertificate(intcert); !ok { issuer := intcert.IssuingCertificateURL if issuer != nil { return false, errCRLUnavailable } } else if revoked { return false, errIntermediateCertRevoked } ints := x509.NewCertPool() ints.AddCert(intcert) l := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string)))) n, err = base64.StdEncoding.Decode(l, []byte(chain[0].(string))) if err != nil { return false, err } leafcert, err := x509.ParseCertificate(l[:n]) if err != nil { return false, err } if revoked, ok := revoke.VerifyCertificate(leafcert); !ok { return false, errCRLUnavailable } else if revoked { return false, errLeafCertRevoked } opts := x509.VerifyOptions{ Roots: roots, Intermediates: ints, } _, err = leafcert.Verify(opts) return err == nil, err } func mdsParseX509Certificate(value string) (certificate *x509.Certificate, err error) { var n int raw := make([]byte, base64.StdEncoding.DecodedLen(len(value))) if n, err = base64.StdEncoding.Decode(raw, []byte(strings.TrimSpace(value))); err != nil { return nil, fmt.Errorf( "error occurred parsing *x509.certificate: error occurred decoding base64 data: %w", err, ) } if certificate, err = x509.ParseCertificate(raw[:n]); err != nil { return nil, err } return certificate, nil } func mdsParseTimePointer(format, value string) (parsed *time.Time, err error) { if value == "" { return nil, nil } var p time.Time if p, err = time.Parse(format, value); err != nil { return nil, err } return &p, nil }