From 72eb16c7ea1221ad80543c271d1ea5d39681159a Mon Sep 17 00:00:00 2001 From: Oleg Lomaka Date: Mon, 21 Aug 2023 06:46:24 -0400 Subject: [PATCH] Upgrade github.com/iden3/go-iden3-core to v2 and bump self version to v2 (#62) * Upgrade github.com/iden3/go-iden3-core to v2 and bump self version to v2. * Upgrade go-iden3-core to latest version * Modify NewPathFromDocument function to support nested fields * Pass MerklizeOption[s] to MerklizeJSONLD * Add merklizer options to ParseClaim method * add Iden3StateInfo to resolvement info * Sync with did resolver driver (#72) * add stateContractAddress field to Vm * Make FieldPathFromContext method for Options (#74) * Slot serialization (#75) * Remove ParseSlots public method. Should use ParseClaim and get slots from there. * jsonSchemaBytes parameter is non-neded in ParseClaim, we get @serialization data from @context of the credential. * find a credential type not only looking at crentialSubject.@type, but also looking at top level @type field * drop credentialType parameter from ParseClaim method, it should be calculated from VerifiableCredential document * make struct ParsedSlots private * Rewrite GetFieldSlotIndex function to work with @serialization attribute in json-ld schema * Upgrade go version * Rename @serialization to iden3_serialization to prevent safe mode warning about reserved keys * return error is MerklizedRootPosition is set for non-merklized claims * Move DocumentLoader to loaders module* Update README * Negative and big integers (#76) * Handle negative and big numbers in merklizer --------- Co-authored-by: vmidyllic <74898029+vmidyllic@users.noreply.github.com> Co-authored-by: Ilya --- .github/workflows/ci-lint.yaml | 4 +- .github/workflows/ci-test.yaml | 5 +- README.md | 12 +- go.mod | 4 +- go.sum | 4 +- json/parser.go | 445 ++++++++++++++---- json/parser_test.go | 281 +++++++++-- json/testdata/non-merklized-1.json-ld | 19 + json/testdata/schema-delivery-address.json-ld | 86 ++++ json/testdata/schema-merklization.json | 127 ----- json/testdata/schema-slots.json | 131 ------ json/validator_test.go | 2 +- {merklize => loaders}/document_loader.go | 2 +- loaders/http.go | 71 --- loaders/ipfs.go | 46 -- loaders/ipfs_test.go | 44 -- loaders/loader.go | 8 - merklize/merklize.go | 240 +++++++--- merklize/merklize_test.go | 262 ++++------- merklize/testdata/custom_schema.json | 12 + processor/json/processor.go | 2 +- processor/json/processor_test.go | 96 ++-- processor/json/testdata/schema.json | 15 + processor/processor.go | 70 ++- testing/http.go | 106 +++++ utils/claims.go | 49 +- verifiable/credential.go | 7 +- verifiable/credential_test.go | 52 ++ verifiable/did_doc.go | 48 +- verifiable/proof.go | 2 +- verifiable/schema.go | 3 + 31 files changed, 1277 insertions(+), 978 deletions(-) create mode 100644 json/testdata/non-merklized-1.json-ld create mode 100644 json/testdata/schema-delivery-address.json-ld delete mode 100644 json/testdata/schema-merklization.json delete mode 100644 json/testdata/schema-slots.json rename {merklize => loaders}/document_loader.go (99%) delete mode 100644 loaders/http.go delete mode 100644 loaders/ipfs.go delete mode 100644 loaders/ipfs_test.go delete mode 100644 loaders/loader.go create mode 100644 processor/json/testdata/schema.json create mode 100644 testing/http.go diff --git a/.github/workflows/ci-lint.yaml b/.github/workflows/ci-lint.yaml index e72fc2f..6436757 100644 --- a/.github/workflows/ci-lint.yaml +++ b/.github/workflows/ci-lint.yaml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: 1.20.1 + go-version: 1.21.0 - uses: golangci/golangci-lint-action@v3 with: - version: v1.51.2 + version: v1.52.2 diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index 517a57a..97d314d 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -13,8 +13,9 @@ jobs: matrix: containers: - 1.18.10-bullseye - - 1.19.6-bullseye - - 1.20.1-bullseye + - 1.19.12-bullseye + - 1.20.7-bullseye + - 1.21.0-bullseye runs-on: ubuntu-latest container: golang:${{ matrix.containers }} steps: diff --git a/README.md b/README.md index 99179d2..dc437a7 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,8 @@ Repository of claim schema vocabulary: https://github.com/iden3/claim-schema-voc The library includes three main components of any processor: -1. Schema Loaders -2. Data Validators -3. Data Parsers - -**Schema loader's** purpose is to load schema (JSON / JSON-LD) from a given address. - -Implemented loaders: - -- [x] HTTP loaders -- [x] IPFS loader +1. Data Validators +2. Data Parsers **Schemas:** diff --git a/go.mod b/go.mod index e7d190e..675a921 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ -module github.com/iden3/go-schema-processor +module github.com/iden3/go-schema-processor/v2 go 1.18 require ( - github.com/iden3/go-iden3-core v1.0.2 + github.com/iden3/go-iden3-core/v2 v2.0.0-20230519124718-42b31ff46f37 github.com/iden3/go-iden3-crypto v0.0.15 github.com/iden3/go-merkletree-sql/v2 v2.0.4 github.com/ipfs/go-ipfs-api v0.6.0 diff --git a/go.sum b/go.sum index dbec8a5..cff9783 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/iden3/go-iden3-core v1.0.2 h1:HwNDFeqcUv4ybZj5tH+58JKWKarn/qqBpNCqTLxGP0Y= -github.com/iden3/go-iden3-core v1.0.2/go.mod h1:X4PjlJG8OsEQEsSbzzYqqAk2olYGZ2nuGqiUPyEYjOo= +github.com/iden3/go-iden3-core/v2 v2.0.0-20230519124718-42b31ff46f37 h1:JPH0tMr8geJO0vLhwTZkopHqm35ARCjCIKNab9rc6QI= +github.com/iden3/go-iden3-core/v2 v2.0.0-20230519124718-42b31ff46f37/go.mod h1:L9PxhWPvoS9qTb3inEkZBm1RpjHBt+VTwvxssdzbAdw= github.com/iden3/go-iden3-crypto v0.0.15 h1:4MJYlrot1l31Fzlo2sF56u7EVFeHHJkxGXXZCtESgK4= github.com/iden3/go-iden3-crypto v0.0.15/go.mod h1:dLpM4vEPJ3nDHzhWFXDjzkn1qHoBeOT/3UEhXsEsP3E= github.com/iden3/go-merkletree-sql/v2 v2.0.4 h1:Dp089P3YNX1BE8+T1tKQHWTtnk84Y/Kr7ZAGTqwscoY= diff --git a/json/parser.go b/json/parser.go index 8d8a1bf..6bba31f 100644 --- a/json/parser.go +++ b/json/parser.go @@ -4,41 +4,37 @@ import ( "context" "encoding/json" "fmt" - - core "github.com/iden3/go-iden3-core" - "github.com/iden3/go-schema-processor/processor" - "github.com/iden3/go-schema-processor/utils" - "github.com/iden3/go-schema-processor/verifiable" + "strings" + + core "github.com/iden3/go-iden3-core/v2" + "github.com/iden3/go-iden3-core/v2/w3c" + "github.com/iden3/go-schema-processor/v2/merklize" + "github.com/iden3/go-schema-processor/v2/processor" + "github.com/iden3/go-schema-processor/v2/utils" + "github.com/iden3/go-schema-processor/v2/verifiable" + "github.com/piprate/json-gold/ld" "github.com/pkg/errors" ) -// SerializationSchema Common JSON -type SerializationSchema struct { - IndexDataSlotA string `json:"indexDataSlotA"` - IndexDataSlotB string `json:"indexDataSlotB"` - ValueDataSlotA string `json:"valueDataSlotA"` - ValueDataSlotB string `json:"valueDataSlotB"` -} - -// SchemaMetadata is metadata of json schema -type SchemaMetadata struct { - Uris map[string]interface{} `json:"uris"` - Serialization *SerializationSchema `json:"serialization"` -} - -type Schema struct { - Metadata *SchemaMetadata `json:"$metadata"` - Schema string `json:"$schema"` - Type string `json:"type"` -} +const ( + credentialSubjectKey = "credentialSubject" + //nolint:gosec // G101: this is not a hardcoded credential + credentialSubjectFullKey = "https://www.w3.org/2018/credentials#credentialSubject" + //nolint:gosec // G101: this is not a hardcoded credential + verifiableCredentialFullKey = "https://www.w3.org/2018/credentials#VerifiableCredential" + typeFullKey = "@type" + contextFullKey = "@context" + serializationFullKey = "iden3_serialization" +) // Parser can parse claim data according to specification type Parser struct { } // ParseClaim creates Claim object from W3CCredential -func (s Parser) ParseClaim(ctx context.Context, credential verifiable.W3CCredential, credentialType string, - jsonSchemaBytes []byte, opts *processor.CoreClaimOptions) (*core.Claim, error) { +func (s Parser) ParseClaim(ctx context.Context, + credential verifiable.W3CCredential, + opts *processor.CoreClaimOptions) (*core.Claim, error) { if opts == nil { opts = &processor.CoreClaimOptions{ @@ -51,13 +47,34 @@ func (s Parser) ParseClaim(ctx context.Context, credential verifiable.W3CCredent } } + mz, err := credential.Merklize(ctx, opts.MerklizerOpts...) + if err != nil { + return nil, err + } + + credentialType, err := findCredentialType(mz) + if err != nil { + return nil, err + } + subjectID := credential.CredentialSubject["id"] - slots, err := s.ParseSlots(credential, jsonSchemaBytes) + slots, err := s.parseSlots(mz, credential, credentialType) if err != nil { return nil, err } + if slots.isZero() { + if opts.MerklizedRootPosition == verifiable.CredentialMerklizedRootPositionNone { + opts.MerklizedRootPosition = verifiable.CredentialMerklizedRootPositionIndex + } + } else { + if opts.MerklizedRootPosition != verifiable.CredentialMerklizedRootPositionNone { + return nil, errors.New( + "merklized root position is not supported for non-merklized claims") + } + } + claim, err := core.NewClaim( utils.CreateSchemaHash([]byte(credentialType)), core.WithIndexDataBytes(slots.IndexA, slots.IndexB), @@ -75,17 +92,23 @@ func (s Parser) ParseClaim(ctx context.Context, credential verifiable.W3CCredent claim.SetExpirationDate(*credential.Expiration) } if subjectID != nil { - var did *core.DID - did, err = core.ParseDID(fmt.Sprintf("%v", subjectID)) + var did *w3c.DID + did, err = w3c.ParseDID(fmt.Sprintf("%v", subjectID)) + if err != nil { + return nil, err + } + + var id core.ID + id, err = core.IDFromDID(*did) if err != nil { return nil, err } switch opts.SubjectPosition { case "", verifiable.CredentialSubjectPositionIndex: - claim.SetIndexID(did.ID) + claim.SetIndexID(id) case verifiable.CredentialSubjectPositionValue: - claim.SetValueID(did.ID) + claim.SetValueID(id) default: return nil, errors.New("unknown subject position") } @@ -93,24 +116,17 @@ func (s Parser) ParseClaim(ctx context.Context, credential verifiable.W3CCredent switch opts.MerklizedRootPosition { case verifiable.CredentialMerklizedRootPositionIndex: - mkRoot, err := credential.Merklize(ctx, opts.MerklizerOpts...) - if err != nil { - return nil, err - } - err = claim.SetIndexMerklizedRoot(mkRoot.Root().BigInt()) + err = claim.SetIndexMerklizedRoot(mz.Root().BigInt()) if err != nil { return nil, err } case verifiable.CredentialMerklizedRootPositionValue: - mkRoot, err := credential.Merklize(ctx, opts.MerklizerOpts...) - if err != nil { - return nil, err - } - err = claim.SetValueMerklizedRoot(mkRoot.Root().BigInt()) + err = claim.SetValueMerklizedRoot(mz.Root().BigInt()) if err != nil { return nil, err } case verifiable.CredentialMerklizedRootPositionNone: + // Slots where filled earlier. Nothing to do here. break default: return nil, errors.New("unknown merklized root position") @@ -119,106 +135,333 @@ func (s Parser) ParseClaim(ctx context.Context, credential verifiable.W3CCredent return claim, nil } -// ParseSlots converts payload to claim slots using provided schema -func (s Parser) ParseSlots(credential verifiable.W3CCredential, schemaBytes []byte) (processor.ParsedSlots, error) { +func getSerializationAttrFromParsedContext(ldCtx *ld.Context, + tp string) (string, error) { - var schema Schema + termDef, ok := ldCtx.AsMap()["termDefinitions"] + if !ok { + return "", errors.New("types now found in context") + } - err := json.Unmarshal(schemaBytes, &schema) + termDefM, ok := termDef.(map[string]any) + if !ok { + return "", errors.New("terms definitions is not of correct type") + } + + for typeName, typeDef := range termDefM { + typeDefM, ok := typeDef.(map[string]any) + if !ok { + // not a type + continue + } + typeCtx, ok := typeDefM[contextFullKey] + if !ok { + // not a type + continue + } + typeCtxM, ok := typeCtx.(map[string]any) + if !ok { + return "", errors.New("type @context is not of correct type") + } + typeID, _ := typeDefM["@id"].(string) + if typeName != tp && typeID != tp { + continue + } + + serStr, _ := typeCtxM[serializationFullKey].(string) + return serStr, nil + } + + return "", nil +} + +// Get `iden3_serialization` attr definition from context document either using +// type name like DeliverAddressMultiTestForked or by type id like +// urn:uuid:ac2ede19-b3b9-454d-b1a9-a7b3d5763100. +func getSerializationAttr(credential verifiable.W3CCredential, + opts *ld.JsonLdOptions, tp string) (string, error) { + + ldCtx, err := ld.NewContext(nil, opts).Parse(anySlice(credential.Context)) if err != nil { - return processor.ParsedSlots{}, err + return "", err + } + + return getSerializationAttrFromParsedContext(ldCtx, tp) +} + +type slotsPaths struct { + indexAPath string + indexBPath string + valueAPath string + valueBPath string +} + +func (p slotsPaths) isEmpty() bool { + return p.indexAPath == "" && p.indexBPath == "" && + p.valueAPath == "" && p.valueBPath == "" +} + +func parseSerializationAttr(serAttr string) (slotsPaths, error) { + prefix := "iden3:v1:" + if !strings.HasPrefix(serAttr, prefix) { + return slotsPaths{}, + errors.New("serialization attribute does not have correct prefix") + } + parts := strings.Split(serAttr[len(prefix):], "&") + if len(parts) > 4 { + return slotsPaths{}, + errors.New("serialization attribute has too many parts") + } + var paths slotsPaths + for _, part := range parts { + kv := strings.Split(part, "=") + if len(kv) != 2 { + return slotsPaths{}, errors.New( + "serialization attribute part does not have correct format") + } + switch kv[0] { + case "slotIndexA": + paths.indexAPath = kv[1] + case "slotIndexB": + paths.indexBPath = kv[1] + case "slotValueA": + paths.valueAPath = kv[1] + case "slotValueB": + paths.valueBPath = kv[1] + default: + return slotsPaths{}, + errors.New("unknown serialization attribute slot") + } + } + return paths, nil +} + +func fillSlot(slotData []byte, mz *merklize.Merklizer, path string) error { + if path == "" { + return nil } - if schema.Metadata != nil && schema.Metadata.Serialization != nil { - return s.assignSlots(credential.CredentialSubject, *schema.Metadata.Serialization) + path = credentialSubjectKey + "." + path + p, err := mz.ResolveDocPath(path) + if err != nil { + return errors.Wrapf(err, "field not found in credential %s", path) } - return processor.ParsedSlots{ - IndexA: make([]byte, 0, 32), - IndexB: make([]byte, 0, 32), - ValueA: make([]byte, 0, 32), - ValueB: make([]byte, 0, 32), - }, nil + entry, err := mz.Entry(p) + if errors.Is(err, merklize.ErrorEntryNotFound) { + return errors.Wrapf(err, "field not found in credential %s", path) + } else if err != nil { + return err + } + intVal, err := entry.ValueMtEntry() + if err != nil { + return err + } + + bytesVal := utils.SwapEndianness(intVal.Bytes()) + copy(slotData, bytesVal) + return nil } -// GetFieldSlotIndex return index of slot from 0 to 7 (each claim has by default 8 slots) -func (s Parser) GetFieldSlotIndex(field string, schemaBytes []byte) (int, error) { +func findCredentialType(mz *merklize.Merklizer) (string, error) { + opts := mz.Options() + + // try to look into credentialSubject.@type to get type of credentials + path1, err := opts.NewPath(credentialSubjectFullKey, typeFullKey) + if err == nil { + var e any + e, err = mz.RawValue(path1) + if err == nil { + tp, ok := e.(string) + if ok { + return tp, nil + } + } + } - var schema Schema + // if type of credentials not found in credentialSubject.@type, loop at + // top level @types if it contains two elements: type we are looking for + // and "VerifiableCredential" type. + path2, err := opts.NewPath(typeFullKey) + if err != nil { + return "", err + } - err := json.Unmarshal(schemaBytes, &schema) + e, err := mz.RawValue(path2) if err != nil { - return 0, err + return "", err } - if schema.Metadata == nil || schema.Metadata.Serialization == nil { - return -1, errors.New("serialization info is not set") + eArr, ok := e.([]any) + if !ok { + return "", fmt.Errorf("top level @type expected to be an array") + } + topLevelTypes, err := toStringSlice(eArr) + if err != nil { + return "", err + } + if len(topLevelTypes) != 2 { + return "", fmt.Errorf("top level @type expected to be of length 2") } - switch field { - case schema.Metadata.Serialization.IndexDataSlotA: - return 2, nil - case schema.Metadata.Serialization.IndexDataSlotB: - return 3, nil - case schema.Metadata.Serialization.ValueDataSlotA: - return 6, nil - case schema.Metadata.Serialization.ValueDataSlotB: - return 7, nil + switch verifiableCredentialFullKey { + case topLevelTypes[0]: + return topLevelTypes[1], nil + case topLevelTypes[1]: + return topLevelTypes[0], nil default: - return -1, errors.Errorf("field `%s` not specified in serialization info", field) + return "", fmt.Errorf( + "@type(s) are expected to contain VerifiableCredential type") + } +} + +// parsedSlots is struct that represents iden3 claim specification +type parsedSlots struct { + IndexA, IndexB []byte + ValueA, ValueB []byte +} + +func (s parsedSlots) isZero() bool { + return isZero(s.IndexA) && isZero(s.IndexB) && + isZero(s.ValueA) && isZero(s.ValueB) +} + +func isZero[T ~byte](in []T) bool { + for _, v := range in { + if v != 0 { + return false + } } + return true } -// assignSlots assigns index and value fields to specific slot according array order -func (s Parser) assignSlots(data map[string]interface{}, schema SerializationSchema) (processor.ParsedSlots, error) { +// parseSlots converts payload to claim slots using provided schema +func (s Parser) parseSlots(mz *merklize.Merklizer, + credential verifiable.W3CCredential, + credentialType string) (parsedSlots, error) { + + slots := parsedSlots{ + IndexA: make([]byte, 32), + IndexB: make([]byte, 32), + ValueA: make([]byte, 32), + ValueB: make([]byte, 32), + } + + jsonLDOpts := mz.Options().JSONLDOptions() + serAttr, err := getSerializationAttr(credential, jsonLDOpts, + credentialType) + if err != nil { + return slots, err + } + + if serAttr == "" { + return slots, nil + } + + sPaths, err := parseSerializationAttr(serAttr) + if err != nil { + return slots, err + } - var err error - result := processor.ParsedSlots{ - IndexA: make([]byte, 0, 32), - IndexB: make([]byte, 0, 32), - ValueA: make([]byte, 0, 32), - ValueB: make([]byte, 0, 32), + if sPaths.isEmpty() { + return slots, nil } - result.IndexA, err = fillSlot(data, schema.IndexDataSlotA) + err = fillSlot(slots.IndexA, mz, sPaths.indexAPath) if err != nil { - return result, err + return slots, err } - result.IndexB, err = fillSlot(data, schema.IndexDataSlotB) + err = fillSlot(slots.IndexB, mz, sPaths.indexBPath) if err != nil { - return result, err + return slots, err } - result.ValueA, err = fillSlot(data, schema.ValueDataSlotB) + err = fillSlot(slots.ValueA, mz, sPaths.valueAPath) if err != nil { - return result, err + return slots, err } - result.ValueB, err = fillSlot(data, schema.ValueDataSlotB) + err = fillSlot(slots.ValueB, mz, sPaths.valueBPath) if err != nil { - return result, err + return slots, err } - return result, nil + return slots, nil +} + +// convert from the slice of concrete type to the slice of interface{} +func anySlice[T any](in []T) []any { + if in == nil { + return nil + } + s := make([]any, len(in)) + for i := range in { + s[i] = in[i] + } + return s } -func fillSlot(data map[string]interface{}, fieldName string) ([]byte, error) { - slot := make([]byte, 0, 32) +// GetFieldSlotIndex return index of slot from 0 to 7 (each claim has by default 8 slots) +func (s Parser) GetFieldSlotIndex(field string, typeName string, + schemaBytes []byte) (int, error) { + + var ctxDoc any + err := json.Unmarshal(schemaBytes, &ctxDoc) + if err != nil { + return -1, err + } - if fieldName == "" { - return slot, nil + ctxDocM, ok := ctxDoc.(map[string]any) + if !ok { + return -1, errors.New("document is not an object") } - field, ok := data[fieldName] + + ctxDoc, ok = ctxDocM[contextFullKey] if !ok { - return slot, errors.Errorf("%s field is not in data", fieldName) + return -1, errors.New("document has no @context") } - byteValue, err := utils.FieldToByteArray(field) + + ldCtx, err := ld.NewContext(nil, nil).Parse(ctxDoc) if err != nil { - return nil, err + return -1, err } - if utils.DataFillsSlot(slot, byteValue) { - slot = append(slot, byteValue...) - } else { - return nil, processor.ErrSlotsOverflow + + serAttr, err := getSerializationAttrFromParsedContext(ldCtx, typeName) + if err != nil { + return -1, err + } + if serAttr == "" { + return -1, errors.Errorf( + "field `%s` not specified in serialization info", field) + } + + sPaths, err := parseSerializationAttr(serAttr) + if err != nil { + return -1, err + } + + switch field { + case sPaths.indexAPath: + return 2, nil + case sPaths.indexBPath: + return 3, nil + case sPaths.valueAPath: + return 6, nil + case sPaths.valueBPath: + return 7, nil + default: + return -1, errors.Errorf( + "field `%s` not specified in serialization info", field) + } +} + +func toStringSlice(in []any) ([]string, error) { + out := make([]string, len(in)) + for i, v := range in { + s, ok := v.(string) + if !ok { + return nil, fmt.Errorf("element #%v is not a string", i) + } + out[i] = s } - return slot, nil + return out, nil } diff --git a/json/parser_test.go b/json/parser_test.go index 7283062..3258d50 100644 --- a/json/parser_test.go +++ b/json/parser_test.go @@ -4,41 +4,102 @@ import ( "context" "encoding/json" "os" + "strings" "testing" - core "github.com/iden3/go-iden3-core" - "github.com/iden3/go-schema-processor/merklize" - "github.com/iden3/go-schema-processor/processor" - "github.com/iden3/go-schema-processor/verifiable" + core "github.com/iden3/go-iden3-core/v2" + "github.com/iden3/go-schema-processor/v2/merklize" + "github.com/iden3/go-schema-processor/v2/processor" + tst "github.com/iden3/go-schema-processor/v2/testing" + "github.com/iden3/go-schema-processor/v2/verifiable" + "github.com/piprate/json-gold/ld" "github.com/stretchr/testify/require" ) -func TestParser_ParseSlots(t *testing.T) { +func TestParser_parseSlots(t *testing.T) { + defer tst.MockHTTPClient(t, + map[string]string{ + "https://www.w3.org/2018/credentials/v1": "../merklize/testdata/httpresp/credentials-v1.jsonld", + "https://example.com/schema-delivery-address.json-ld": "testdata/schema-delivery-address.json-ld", + }, + tst.IgnoreUntouchedURLs())() - credentialBytes, err := os.ReadFile("testdata/credential-non-merklized.json") + credentialBytes, err := os.ReadFile("testdata/non-merklized-1.json-ld") require.NoError(t, err) var credential verifiable.W3CCredential - err = json.Unmarshal(credentialBytes, &credential) require.NoError(t, err) - schemaBytes, err := os.ReadFile("testdata/schema-slots.json") + nullSlot := make([]byte, 32) + ctx := context.Background() + + mz, err := credential.Merklize(ctx) require.NoError(t, err) - parser := Parser{} - slots, err := parser.ParseSlots(credential, schemaBytes) + credentialType, err := findCredentialType(mz) + require.NoError(t, err) + parser := Parser{} + slots, err := parser.parseSlots(mz, credential, credentialType) require.NoError(t, err) - require.NotEmpty(t, slots.IndexA) - require.NotEmpty(t, slots.IndexB) - require.Empty(t, slots.ValueA) - require.Empty(t, slots.ValueB) + require.NotEqual(t, nullSlot, slots.IndexA) + require.Equal(t, nullSlot, slots.IndexB) + require.Equal(t, nullSlot, slots.ValueA) + require.NotEqual(t, nullSlot, slots.ValueB) +} + +func TestGetSerializationAttr(t *testing.T) { + defer tst.MockHTTPClient(t, + map[string]string{ + "https://www.w3.org/2018/credentials/v1": "../merklize/testdata/httpresp/credentials-v1.jsonld", + "https://example.com/schema-delivery-address.json-ld": "testdata/schema-delivery-address.json-ld", + }, + tst.IgnoreUntouchedURLs())() + + vc := verifiable.W3CCredential{ + Context: []string{ + "https://www.w3.org/2018/credentials/v1", + "https://example.com/schema-delivery-address.json-ld", + }, + } + options := ld.NewJsonLdOptions("") + + t.Run("by type name", func(t *testing.T) { + serAttr, err := getSerializationAttr(vc, options, + "DeliverAddressMultiTestForked") + require.NoError(t, err) + require.Equal(t, + "iden3:v1:slotIndexA=price&slotValueB=postalProviderInformation.insured", + serAttr) + }) + + t.Run("by type id", func(t *testing.T) { + serAttr, err := getSerializationAttr(vc, options, + "urn:uuid:ac2ede19-b3b9-454d-b1a9-a7b3d5763100") + require.NoError(t, err) + require.Equal(t, + "iden3:v1:slotIndexA=price&slotValueB=postalProviderInformation.insured", + serAttr) + }) + + t.Run("unknown type", func(t *testing.T) { + serAttr, err := getSerializationAttr(vc, options, "bla-bla") + require.NoError(t, err) + require.Equal(t, "", serAttr) + }) } -func TestParser_ParseClaimWithDataSlots(t *testing.T) { - credentialBytes, err := os.ReadFile("testdata/credential-non-merklized.json") +func TestParser_ParseClaimWithDataSlots(t *testing.T) { + defer tst.MockHTTPClient(t, + map[string]string{ + "https://www.w3.org/2018/credentials/v1": "../merklize/testdata/httpresp/credentials-v1.jsonld", + "https://example.com/schema-delivery-address.json-ld": "testdata/schema-delivery-address.json-ld", + }, + tst.IgnoreUntouchedURLs())() + + credentialBytes, err := os.ReadFile("testdata/non-merklized-1.json-ld") require.NoError(t, err) var credential verifiable.W3CCredential @@ -46,13 +107,8 @@ func TestParser_ParseClaimWithDataSlots(t *testing.T) { err = json.Unmarshal(credentialBytes, &credential) require.NoError(t, err) - schemaBytes, err := os.ReadFile("testdata/schema-slots.json") - require.NoError(t, err) - parser := Parser{} - credentialType := "Test" - opts := processor.CoreClaimOptions{ RevNonce: 127366661, Version: 0, @@ -61,32 +117,25 @@ func TestParser_ParseClaimWithDataSlots(t *testing.T) { Updatable: true, } - claim, err := parser.ParseClaim(context.Background(), credential, credentialType, schemaBytes, &opts) + claim, err := parser.ParseClaim(context.Background(), credential, &opts) require.NoError(t, err) index, value := claim.RawSlots() require.NotEmpty(t, index[2]) - require.NotEmpty(t, index[3]) + require.Empty(t, index[3]) require.Empty(t, value[2]) - require.Empty(t, value[3]) + require.NotEmpty(t, value[3]) - did := credential.CredentialSubject["id"].(string) - idFromClaim, err := claim.GetID() - require.NoError(t, err) - didFromClaim, err := core.ParseDIDFromID(idFromClaim) - require.NoError(t, err) - _, err = core.ParseDIDFromID(idFromClaim) - require.NoError(t, err) - require.Equal(t, did, didFromClaim.String()) + _, err = claim.GetID() + require.EqualError(t, err, "ID is not set") require.Equal(t, opts.Updatable, claim.GetFlagUpdatable()) - exp, _ := claim.GetExpirationDate() - require.Equal(t, credential.Expiration.Unix(), exp.Unix()) - + _, ok := claim.GetExpirationDate() + require.False(t, ok) } -func TestParser_ParseClaimWithMerklizedRoot(t *testing.T) { +func TestParser_ParseClaimWithMerklizedRoot(t *testing.T) { credentialBytes, err := os.ReadFile("testdata/credential-merklized.json") require.NoError(t, err) @@ -95,13 +144,8 @@ func TestParser_ParseClaimWithMerklizedRoot(t *testing.T) { err = json.Unmarshal(credentialBytes, &credential) require.NoError(t, err) - schemaBytes, err := os.ReadFile("testdata/schema-merklization.json") - require.NoError(t, err) - parser := Parser{} - credentialType := "Test" - opts := processor.CoreClaimOptions{ RevNonce: 127366661, Version: 0, @@ -109,7 +153,7 @@ func TestParser_ParseClaimWithMerklizedRoot(t *testing.T) { MerklizedRootPosition: verifiable.CredentialMerklizedRootPositionIndex, Updatable: true, } - claim, err := parser.ParseClaim(context.Background(), credential, credentialType, schemaBytes, &opts) + claim, err := parser.ParseClaim(context.Background(), credential, &opts) require.NoError(t, err) index, value := claim.RawSlots() @@ -158,12 +202,159 @@ func TestParser_ParseClaimWithMerklizedRoot(t *testing.T) { } func Test_GetFieldSlotIndex(t *testing.T) { - schemaBytes, err := os.ReadFile("testdata/schema-slots.json") + contextBytes, err := os.ReadFile("testdata/schema-delivery-address.json-ld") require.NoError(t, err) parser := Parser{} - slotIndex, err := parser.GetFieldSlotIndex("birthday", schemaBytes) - require.NoError(t, err) + slotIndex, err := parser.GetFieldSlotIndex("price", + "DeliverAddressMultiTestForked", contextBytes) + require.NoError(t, err) require.Equal(t, 2, slotIndex) + + slotIndex, err = parser.GetFieldSlotIndex( + "postalProviderInformation.insured", "DeliverAddressMultiTestForked", + contextBytes) + require.NoError(t, err) + require.Equal(t, 7, slotIndex) +} + +func TestFindCredentialType(t *testing.T) { + mockHTTP := func(t testing.TB) func() { + return tst.MockHTTPClient(t, + map[string]string{ + "https://www.w3.org/2018/credentials/v1": "../merklize/testdata/httpresp/credentials-v1.jsonld", + "https://example.com/schema-delivery-address.json-ld": "testdata/schema-delivery-address.json-ld", + }, + // requests are cached, so we don't check them on second and + // further runs + tst.IgnoreUntouchedURLs(), + ) + } + + ctx := context.Background() + + t.Run("type from internal field", func(t *testing.T) { + defer mockHTTP(t)() + rdr := strings.NewReader(` +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://example.com/schema-delivery-address.json-ld" + ], + "@type": [ + "VerifiableCredential", + "DeliverAddressMultiTestForked" + ], + "credentialSubject": { + "isPostalProvider": false, + "postalProviderInformation": { + "insured": true, + "weight": "1.3" + }, + "price": "123.52", + "type": "DeliverAddressMultiTestForked" + } +}`) + mz, err := merklize.MerklizeJSONLD(ctx, rdr) + require.NoError(t, err) + typeID, err := findCredentialType(mz) + require.NoError(t, err) + require.Equal(t, "urn:uuid:ac2ede19-b3b9-454d-b1a9-a7b3d5763100", typeID) + }) + + t.Run("type from top level", func(t *testing.T) { + defer mockHTTP(t)() + rdr := strings.NewReader(` +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://example.com/schema-delivery-address.json-ld" + ], + "@type": [ + "VerifiableCredential", + "DeliverAddressMultiTestForked" + ], + "credentialSubject": { + "isPostalProvider": false, + "postalProviderInformation": { + "insured": true, + "weight": "1.3" + }, + "price": "123.52" + } +}`) + mz, err := merklize.MerklizeJSONLD(ctx, rdr) + require.NoError(t, err) + typeID, err := findCredentialType(mz) + require.NoError(t, err) + require.Equal(t, "urn:uuid:ac2ede19-b3b9-454d-b1a9-a7b3d5763100", typeID) + }) + + t.Run("type from top level when internal incorrect", func(t *testing.T) { + defer mockHTTP(t)() + rdr := strings.NewReader(` +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://example.com/schema-delivery-address.json-ld" + ], + "@type": [ + "VerifiableCredential", + "DeliverAddressMultiTestForked" + ], + "credentialSubject": { + "isPostalProvider": false, + "postalProviderInformation": { + "insured": true, + "weight": "1.3" + }, + "price": "123.52", + "type": ["EcdsaSecp256k1Signature2019", "EcdsaSecp256r1Signature2019"] + } +}`) + mz, err := merklize.MerklizeJSONLD(ctx, rdr) + require.NoError(t, err) + typeID, err := findCredentialType(mz) + require.NoError(t, err) + require.Equal(t, "urn:uuid:ac2ede19-b3b9-454d-b1a9-a7b3d5763100", typeID) + }) + + t.Run("unexpected top level 1", func(t *testing.T) { + defer mockHTTP(t)() + rdr := strings.NewReader(` +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://example.com/schema-delivery-address.json-ld" + ], + "@type": [ + "VerifiableCredential", + "DeliverAddressMultiTestForked", + "EcdsaSecp256k1Signature2019" + ] +}`) + mz, err := merklize.MerklizeJSONLD(ctx, rdr) + require.NoError(t, err) + _, err = findCredentialType(mz) + require.EqualError(t, err, "top level @type expected to be of length 2") + }) + + t.Run("unexpected top level 2", func(t *testing.T) { + defer mockHTTP(t)() + rdr := strings.NewReader(` +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://example.com/schema-delivery-address.json-ld" + ], + "@type": ["DeliverAddressMultiTestForked", "EcdsaSecp256k1Signature2019"] +}`) + mz, err := merklize.MerklizeJSONLD(ctx, rdr) + require.NoError(t, err) + _, err = findCredentialType(mz) + require.EqualError(t, err, + "@type(s) are expected to contain VerifiableCredential type") + }) + } diff --git a/json/testdata/non-merklized-1.json-ld b/json/testdata/non-merklized-1.json-ld new file mode 100644 index 0000000..453cac1 --- /dev/null +++ b/json/testdata/non-merklized-1.json-ld @@ -0,0 +1,19 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://example.com/schema-delivery-address.json-ld" + ], + "type": [ + "VerifiableCredential", + "DeliverAddressMultiTestForked" + ], + "credentialSubject": { + "type": "DeliverAddressMultiTestForked", + "price": "123.52", + "isPostalProvider": false, + "postalProviderInformation": { + "insured": true, + "weight": "1.3" + } + } +} diff --git a/json/testdata/schema-delivery-address.json-ld b/json/testdata/schema-delivery-address.json-ld new file mode 100644 index 0000000..900553b --- /dev/null +++ b/json/testdata/schema-delivery-address.json-ld @@ -0,0 +1,86 @@ +{ + "@context": [ + { + "@protected": true, + "@version": 1.1, + "id": "@id", + "type": "@type", + "DeliverAddressMultiTestForked": { + "@context": { + "@propagate": true, + "@protected": true, + "iden3_serialization": "iden3:v1:slotIndexA=price&slotValueB=postalProviderInformation.insured", + "polygon-vocab": "urn:uuid:77157331-176d-4b84-814b-98ac52a1b870#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "operatorId": { + "@id": "polygon-vocab:operatorId", + "@type": "xsd:integer" + }, + "country": { + "@id": "polygon-vocab:country", + "@type": "xsd:string" + }, + "price": { + "@id": "polygon-vocab:price", + "@type": "xsd:double" + }, + "deliveryTime": { + "@id": "polygon-vocab:deliveryTime", + "@type": "xsd:dateTime" + }, + "isPostalProvider": { + "@id": "polygon-vocab:isPostalProvider", + "@type": "xsd:boolean" + }, + "postalProviderInformation": { + "@context": { + "insured": { + "@id": "polygon-vocab:insured", + "@type": "xsd:boolean" + }, + "weight": { + "@id": "polygon-vocab:weight", + "@type": "xsd:double" + }, + "name": { + "@id": "polygon-vocab:name", + "@type": "xsd:string" + }, + "officeNo": { + "@id": "polygon-vocab:officeNo", + "@type": "xsd:integer" + }, + "expectedExpirationDate": { + "@id": "polygon-vocab:expectedExpirationDate", + "@type": "xsd:dateTime" + }, + "isPerishable": { + "@id": "polygon-vocab:isPerishable", + "@type": "xsd:boolean" + } + }, + "@id": "polygon-vocab:postalProviderInformation" + }, + "homeAddress": { + "@context": { + "expectedFrom": { + "@id": "polygon-vocab:expectedFrom", + "@type": "xsd:dateTime" + }, + "line2": { + "@id": "polygon-vocab:line2", + "@type": "xsd:string" + }, + "line1": { + "@id": "polygon-vocab:line1", + "@type": "xsd:string" + } + }, + "@id": "polygon-vocab:homeAddress" + } + }, + "@id": "urn:uuid:ac2ede19-b3b9-454d-b1a9-a7b3d5763100" + } + } + ] +} diff --git a/json/testdata/schema-merklization.json b/json/testdata/schema-merklization.json deleted file mode 100644 index beb3895..0000000 --- a/json/testdata/schema-merklization.json +++ /dev/null @@ -1,127 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "$metadata": { - "uris": { - "jsonLdContext": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld", - "jsonSchema": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCAgeCredential-v3.json" - } - }, - "required": [ - "@context", - "id", - "type", - "issuanceDate", - "credentialSubject", - "credentialSchema", - "credentialStatus", - "issuer" - ], - "properties": { - "@context": { - "type": [ - "string", - "array", - "object" - ] - }, - "id": { - "type": "string" - }, - "type": { - "type": [ - "string", - "array" - ], - "items": { - "type": "string" - } - }, - "issuer": { - "type": [ - "string", - "object" - ], - "format": "uri", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "string", - "format": "uri" - } - } - }, - "issuanceDate": { - "type": "string", - "format": "date-time" - }, - "expirationDate": { - "type": "string", - "format": "date-time" - }, - "credentialSchema": { - "type": "object", - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string", - "format": "uri" - }, - "type": { - "type": "string" - } - } - }, - "subjectPosition": { - "type": "string", - "enum": [ - "none", - "index", - "value" - ] - }, - "merklizationRootPosition": { - "type": "string", - "enum": [ - "none", - "index", - "value" - ] - }, - "revNonce": { - "type": "integer" - }, - "version": { - "type": "integer" - }, - "updatable": { - "type": "boolean" - }, - "credentialSubject": { - "type": "object", - "required": [ - "id", - "birthday", - "documentType" - ], - "properties": { - "id": { - "title": "Credential Subject ID", - "type": "string", - "format": "uri" - }, - "birthday": { - "type": "integer" - }, - "documentType": { - "type": "integer" - } - } - } - } -} diff --git a/json/testdata/schema-slots.json b/json/testdata/schema-slots.json deleted file mode 100644 index 3879054..0000000 --- a/json/testdata/schema-slots.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "$metadata": { - "uris": { - "jsonLdContext": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld", - "jsonSchema": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCAgeCredential-v2.json" - }, - "serialization": { - "indexDataSlotA": "birthday", - "indexDataSlotB": "documentType" - } - }, - "required": [ - "@context", - "id", - "type", - "issuanceDate", - "credentialSubject", - "credentialSchema", - "credentialStatus", - "issuer" - ], - "properties": { - "@context": { - "type": [ - "string", - "array", - "object" - ] - }, - "id": { - "type": "string" - }, - "type": { - "type": [ - "string", - "array" - ], - "items": { - "type": "string" - } - }, - "issuer": { - "type": [ - "string", - "object" - ], - "format": "uri", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "string", - "format": "uri" - } - } - }, - "issuanceDate": { - "type": "string", - "format": "date-time" - }, - "expirationDate": { - "type": "string", - "format": "date-time" - }, - "credentialSchema": { - "type": "object", - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string", - "format": "uri" - }, - "type": { - "type": "string" - } - } - }, - "subjectPosition": { - "type": "string", - "enum": [ - "none", - "index", - "value" - ] - }, - "merklizationRootPosition": { - "type": "string", - "enum": [ - "none", - "index", - "value" - ] - }, - "revNonce": { - "type": "integer" - }, - "version": { - "type": "integer" - }, - "updatable": { - "type": "boolean" - }, - "credentialSubject": { - "type": "object", - "required": [ - "id", - "birthday", - "documentType" - ], - "properties": { - "id": { - "title": "Credential Subject ID", - "type": "string", - "format": "uri" - }, - "birthday": { - "type": "integer" - }, - "documentType": { - "type": "integer" - } - } - } - } -} diff --git a/json/validator_test.go b/json/validator_test.go index 2c28162..6e04be5 100644 --- a/json/validator_test.go +++ b/json/validator_test.go @@ -3,7 +3,7 @@ package json import ( "testing" - "github.com/iden3/go-schema-processor/verifiable" + "github.com/iden3/go-schema-processor/v2/verifiable" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/merklize/document_loader.go b/loaders/document_loader.go similarity index 99% rename from merklize/document_loader.go rename to loaders/document_loader.go index c8f64fc..a424482 100644 --- a/merklize/document_loader.go +++ b/loaders/document_loader.go @@ -1,4 +1,4 @@ -package merklize +package loaders import ( "errors" diff --git a/loaders/http.go b/loaders/http.go deleted file mode 100644 index 482cbe5..0000000 --- a/loaders/http.go +++ /dev/null @@ -1,71 +0,0 @@ -package loaders - -import ( - "context" - "io" - "log" - "net/http" - "net/url" - "strings" - "time" - - "github.com/pkg/errors" -) - -// ErrorURLEmpty is empty url error -var ErrorURLEmpty = errors.New("URL is empty") - -// HTTP is loader for http / https schemas -type HTTP struct { - URL string -} - -// Load loads schema by url -func (l HTTP) Load(ctx context.Context) (schema []byte, extension string, err error) { - - if l.URL == "" { - return nil, "", ErrorURLEmpty - } - // parse schema url - u, err := url.Parse(l.URL) - if err != nil { - return nil, "", err - } - // get a file extension - segments := strings.Split(u.Path, "/") - extension = segments[len(segments)-1][strings.Index(segments[len(segments)-1], ".")+1:] - - req, err := http.NewRequest(http.MethodGet, u.String(), http.NoBody) - if err != nil { - log.Fatal(err) - } - newCtx, cancel := context.WithTimeout(ctx, 30*time.Second) - defer cancel() - req = req.WithContext(newCtx) - c := &http.Client{} - resp, err := c.Do(req) - - if err != nil { - return nil, "", errors.WithMessage(err, "http request failed") - } - - defer func() { - if err2 := resp.Body.Close(); err2 != nil { - if err == nil { - err = err2 - } - } - }() - - if resp.StatusCode != http.StatusOK { - return nil, "", errors.Errorf("request failed with status code %v", - resp.StatusCode) - } - - // We Read the response body on the line below. - schema, err = io.ReadAll(resp.Body) - if err != nil { - return nil, "", err - } - return schema, extension, err -} diff --git a/loaders/ipfs.go b/loaders/ipfs.go deleted file mode 100644 index ff1deac..0000000 --- a/loaders/ipfs.go +++ /dev/null @@ -1,46 +0,0 @@ -package loaders - -import ( - "bytes" - "context" - - shell "github.com/ipfs/go-ipfs-api" - "github.com/pkg/errors" -) - -// ErrorCIDEEmpty is for error when CID is empty -var ErrorCIDEEmpty = errors.New("CID is empty") - -// IPFS loader for fetching schema -type IPFS struct { - URL string - CID string -} - -// Load method IPFS implementation -func (l IPFS) Load(ctx context.Context) (schema []byte, extension string, err error) { - - if l.URL == "" { - return nil, "", ErrorURLEmpty - } - - if l.CID == "" { - return nil, "", ErrorCIDEEmpty - } - - sh := shell.NewShell(l.URL) - - data, err := sh.Cat(l.CID) - - if err != nil { - return nil, "", err - } - - buf := new(bytes.Buffer) - _, err = buf.ReadFrom(data) - if err != nil { - return nil, "", err - } - - return buf.Bytes(), "json-ld", nil -} diff --git a/loaders/ipfs_test.go b/loaders/ipfs_test.go deleted file mode 100644 index 2483f54..0000000 --- a/loaders/ipfs_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package loaders - -import ( - "bytes" - "encoding/json" - "fmt" - "os" - "testing" - - shell "github.com/ipfs/go-ipfs-api" - "github.com/stretchr/testify/assert" -) - -var str = `{"id":"c0f6ac87-603e-44cd-8d83-0caeb458d50d","@context":["https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/iden3credential.json-ld","https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/auth.json-ld"],"@type":["Iden3Credential"],"expiration":"2361-03-21T21:14:48+02:00","updatable":false,"version":0,"rev_nonce":2034832188220019200,"credentialSubject":{"type":"AuthBJJCredential","x":"12747559771369266961976321746772881814229091957322087014312756428846389160887","y":"7732074634595480184356588475330446395691728690271550550016720788712795268212"},"credentialStatus":{"id":"http://localhost:8001/api/v1/identities/118VhAf6ng6J44FhNrGeYzSbJgGVmcpeXYFR2YTrZ6/claims/revocation/status/2034832188220019081","type":"SparseMerkleTreeProof"},"credentialSchema":{"@id":"https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/auth.json-ld","type":"JsonSchemaValidator2018"},"proof":[{"@type":"BJJSignature2021","issuer":"118VhAf6ng6J44FhNrGeYzSbJgGVmcpeXYFR2YTrZ6","h_index":"c89cf5b95157f091f2d8bf49bc1a57cd7988da83bbcd982a74c5e8c70e566403","h_value":"0262b2cd6b9ae44cd9a39045c9bb03ad4e1f056cb81d855f1fc4ef0cdf827912","created":1642518655,"issuer_mtp":{"@type":"Iden3SparseMerkleProof","issuer":"118VhAf6ng6J44FhNrGeYzSbJgGVmcpeXYFR2YTrZ6","h_index":"201a02eb979be695702ea37d930309d2965d803541be5f7b3900459b2fad8726","h_value":"0654da1d53ca201cb42b767a6f12265ff7a08720b88a82182e0f20702479d12d","state":{"claims_tree_root":"a5087cfa6f2c7c565d831327091533f09999133df1df51104d2ce6f8e4d90529","value":"dca344e95da517a301729d94b213298b9de96dfddaf7aad9423d918ea3208820"},"mtp":{"existence":true,"siblings":[]}},"verification_method":"2764e2d8241b18c217010ebf90bebb30240d32c33f3007f33e42d58680813123","proof_value":"c354eb1006534c59766ed8398d49a9a614312e430c5373ea493395db6369d49485e9a0d63f3bfe9fd157294ffbf706b6b7df7a8662a58fae0056a046af1caa04","proof_purpose":"Authentication"},{"@type":"Iden3SparseMerkleProof","issuer":"118VhAf6ng6J44FhNrGeYzSbJgGVmcpeXYFR2YTrZ6","h_index":"c89cf5b95157f091f2d8bf49bc1a57cd7988da83bbcd982a74c5e8c70e566403","h_value":"0262b2cd6b9ae44cd9a39045c9bb03ad4e1f056cb81d855f1fc4ef0cdf827912","state":{"tx_id":"0xf2e23524ab76cb4f371b921a214ff411d5d391962899a2afe20f356e3bdc0c71","block_timestamp":1642522496,"block_number":11837707,"claims_tree_root":"bebcaee8444e93b6e32855f54e9f617d5fd654570badce7d6bc649304169681d","revocation_tree_root":"0000000000000000000000000000000000000000000000000000000000000000","value":"2806aa9a045b2a5503b12f2979b2d19933e803fd3dd73d8ad40dc138bc9a582e"},"mtp":{"existence":true,"siblings":["0","0","0","18555164879275043542501047154170418730098376961920428892719505858997411121317"]}}]}` - -func UploadToIpfs() (string, error) { - - var m map[string]interface{} - sh := shell.NewShell(os.Getenv("IPFS_URL")) - err := json.Unmarshal([]byte(str), &m) - if err != nil { - return "", err - } - b, err := json.Marshal(m) - if err != nil { - return "", err - } - - cid, err := sh.Add(bytes.NewReader(b)) - if err != nil { - return "", err - } - - return cid, nil - -} - -func TestUpload(t *testing.T) { - - a, err := UploadToIpfs() - fmt.Println(a) - assert.Nil(t, err) - -} diff --git a/loaders/loader.go b/loaders/loader.go deleted file mode 100644 index 40613cf..0000000 --- a/loaders/loader.go +++ /dev/null @@ -1,8 +0,0 @@ -package loaders - -import "context" - -// Loader is basic interface for loaders -type Loader interface { - Load(ctx context.Context) (schema []byte, extension string, err error) -} diff --git a/merklize/merklize.go b/merklize/merklize.go index c077940..33aec8d 100644 --- a/merklize/merklize.go +++ b/merklize/merklize.go @@ -17,13 +17,14 @@ import ( "github.com/iden3/go-iden3-crypto/poseidon" "github.com/iden3/go-merkletree-sql/v2" "github.com/iden3/go-merkletree-sql/v2/db/memory" + "github.com/iden3/go-schema-processor/v2/loaders" shell "github.com/ipfs/go-ipfs-api" "github.com/piprate/json-gold/ld" ) var ( defaultHasher Hasher = PoseidonHasher{} - defaultDocumentLoader = NewDocumentLoader(nil, "") + defaultDocumentLoader = loaders.NewDocumentLoader(nil, "") numRE = regexp.MustCompile(`^\d+$`) ) @@ -34,6 +35,8 @@ var ( ErrorContextTypeIsEmpty = errors.New("ctxType is empty") // ErrorUnsupportedType is returned when type is not supported ErrorUnsupportedType = errors.New("unsupported type") + // ErrorEntryNotFound is returned when entry not found in merklized document + ErrorEntryNotFound = errors.New("entry not found") ) // SetHasher changes default hasher @@ -70,28 +73,45 @@ func (o Options) getDocumentLoader() ld.DocumentLoader { return defaultDocumentLoader } -func (o Options) getJSONLdOptions() *ld.JsonLdOptions { - docLoader := o.getDocumentLoader() - if docLoader == nil { - return nil - } - return &ld.JsonLdOptions{ - DocumentLoader: docLoader, - } +func (o Options) JSONLDOptions() *ld.JsonLdOptions { + return newJSONLDOptions(true, o.getDocumentLoader()) } func (o Options) NewPath(parts ...interface{}) (Path, error) { p := Path{hasher: o.getHasher()} - err := p.Append(parts) + err := p.Append(parts...) return p, err } func (o Options) PathFromContext(ctxBytes []byte, path string) (Path, error) { out := Path{hasher: o.getHasher()} - err := out.pathFromContext(ctxBytes, path, o.getJSONLdOptions()) + err := out.pathFromContext(ctxBytes, path, o.JSONLDOptions()) return out, err } +func (o Options) FieldPathFromContext(ctxBytes []byte, ctxType, fieldPath string) (Path, error) { + if ctxType == "" { + return Path{}, ErrorContextTypeIsEmpty + } + if fieldPath == "" { + return Path{}, ErrorFieldIsEmpty + } + + fullPath, err := o.PathFromContext(ctxBytes, fmt.Sprintf("%s.%s", ctxType, fieldPath)) + if err != nil { + return Path{}, err + } + + typePath, err := o.PathFromContext(ctxBytes, ctxType) + if err != nil { + return Path{}, err + } + + resPath := Path{parts: fullPath.parts[len(typePath.parts):], hasher: o.getHasher()} + + return resPath, nil +} + func (o Options) NewRDFEntry(key Path, value interface{}) (RDFEntry, error) { e := RDFEntry{ key: key, @@ -159,10 +179,7 @@ func NewPath(parts ...interface{}) (Path, error) { // NewPathFromContext parses context and do its best to generate full Path // from shortcut line field1.field2.field3... func NewPathFromContext(ctxBytes []byte, path string) (Path, error) { - defaultOpts := Options{} - var out = Path{hasher: defaultOpts.getHasher()} - err := out.pathFromContext(ctxBytes, path, defaultOpts.getJSONLdOptions()) - return out, err + return Options{}.PathFromContext(ctxBytes, path) } func NewPathFromDocument(docBytes []byte, path string) (Path, error) { @@ -171,27 +188,7 @@ func NewPathFromDocument(docBytes []byte, path string) (Path, error) { // NewFieldPathFromContext resolves field path without type path prefix func NewFieldPathFromContext(ctxBytes []byte, ctxType, fieldPath string) (Path, error) { - - if ctxType == "" { - return Path{}, ErrorContextTypeIsEmpty - } - if fieldPath == "" { - return Path{}, ErrorFieldIsEmpty - } - - fullPath, err := NewPathFromContext(ctxBytes, fmt.Sprintf("%s.%s", ctxType, fieldPath)) - if err != nil { - return Path{}, err - } - - typePath, err := NewPathFromContext(ctxBytes, ctxType) - if err != nil { - return Path{}, err - } - - resPath := Path{parts: fullPath.parts[len(typePath.parts):], hasher: defaultHasher} - - return resPath, nil + return Options{}.FieldPathFromContext(ctxBytes, ctxType, fieldPath) } // TypeIDFromContext returns @id attribute for type from JSON-LD context @@ -204,7 +201,7 @@ func (o Options) TypeIDFromContext(ctxBytes []byte, return "", err } - ldCtx, err := ld.NewContext(nil, o.getJSONLdOptions()). + ldCtx, err := ld.NewContext(nil, o.JSONLDOptions()). Parse(ctxObj["@context"]) if err != nil { return "", err @@ -245,7 +242,7 @@ func (o Options) TypeFromContext(ctxBytes []byte, path string) (string, error) { return "", err } - ldCtx, err := ld.NewContext(nil, o.getJSONLdOptions()). + ldCtx, err := ld.NewContext(nil, o.JSONLDOptions()). Parse(ctxObj["@context"]) if err != nil { return "", err @@ -266,7 +263,6 @@ func (o Options) TypeFromContext(ctxBytes []byte, path string) (string, error) { nextCtx, ok := m["@context"] if ok { - var err error ldCtx, err = ldCtx.Parse(nextCtx) if err != nil { return "", nil @@ -380,7 +376,7 @@ func (o Options) pathFromDocument(ldCtx *ld.Context, docObj interface{}, } if ldCtx == nil { - ldCtx = ld.NewContext(nil, o.getJSONLdOptions()) + ldCtx = ld.NewContext(nil, o.JSONLDOptions()) } var err error @@ -529,7 +525,7 @@ func (p *Path) Prepend(parts ...interface{}) error { type RDFEntry struct { key Path - // valid types are: int64, string, bool, time.Time + // valid types are: int64, string, bool, time.Time, *big.Int value any datatype string hasher Hasher @@ -547,6 +543,9 @@ type Value interface { IsInt64() bool AsInt64() (int64, error) + IsBigInt() bool + AsBigInt() (*big.Int, error) + IsBool() bool AsBool() (bool, error) } @@ -554,7 +553,7 @@ type Value interface { var ErrIncorrectType = errors.New("incorrect type") type value struct { - // valid types are: int64, string, bool, time.Time + // valid types are: int64, string, bool, time.Time, *big.Int value any hasher Hasher } @@ -562,7 +561,7 @@ type value struct { // NewValue creates new Value func NewValue(hasher Hasher, val any) (Value, error) { switch val.(type) { - case int64, string, bool, time.Time: + case int64, string, bool, time.Time, *big.Int: default: return nil, ErrIncorrectType } @@ -634,6 +633,21 @@ func (v *value) AsBool() (bool, error) { return b, nil } +// IsBigInt returns true is value is of type *big.Int +func (v *value) IsBigInt() bool { + _, ok := v.value.(*big.Int) + return ok +} + +// AsBigInt returns *big.Int value or error if value is not of type *big.Int +func (v *value) AsBigInt() (*big.Int, error) { + i, ok := v.value.(*big.Int) + if !ok { + return nil, ErrIncorrectType + } + return i, nil +} + func NewRDFEntry(key Path, value any) (RDFEntry, error) { return Options{}.NewRDFEntry(key, value) } @@ -1062,6 +1076,10 @@ func EntriesFromRDFWithHasher(ds *ld.RDFDataset, return nil, errors.New("@default graph not found in dataset") } + if hasher == nil { + hasher = defaultHasher + } + rs, err := newRelationship(ds, hasher) if err != nil { return nil, err @@ -1087,7 +1105,8 @@ func EntriesFromRDFWithHasher(ds *ld.RDFDataset, if qo == nil { return errors.New("object Literal is nil") } - e.value, err = convertStringToXSDValue(qo.Datatype, qo.Value) + e.value, err = convertStringToXSDValue(qo.Datatype, qo.Value, + hasher.Prime()) if err != nil { return err } @@ -1151,7 +1170,7 @@ func valueToHash(h Hasher, datatype string, value any) (*big.Int, error) { if err != nil { return nil, err } - xsdValue, err := convertStringToXSDValue(datatype, v) + xsdValue, err := convertStringToXSDValue(datatype, v, h.Prime()) if err != nil { return nil, err } @@ -1268,8 +1287,44 @@ func uintToXSDDoubleStr[T allUInts](v T) (string, error) { return out, nil } -func convertStringToXSDValue(datatype string, - value string) (resultValue interface{}, err error) { +func intFromStr(s string) (*big.Int, error) { + var r = new(big.Rat) + _, ok := r.SetString(s) + if !ok { + return nil, fmt.Errorf("can't parse number: %v", s) + } + + if !r.IsInt() { + return nil, fmt.Errorf("integer has fractional part: %v", s) + } + + return r.Num(), nil +} + +// return included minimum and included maximum values for integers by XSD type +func minMaxByXSDType(xsdType string, + prime *big.Int) (*big.Int, *big.Int, error) { + switch xsdType { + case ld.XSDNS + "positiveInteger": + return big.NewInt(1), new(big.Int).Sub(prime, big.NewInt(1)), nil + case ld.XSDNS + "nonNegativeInteger": + return big.NewInt(0), new(big.Int).Sub(prime, big.NewInt(1)), nil + case ld.XSDInteger: + minVal, maxVal := minMaxFromPrime(prime) + return minVal, maxVal, nil + case ld.XSDNS + "negativeInteger": + minVal, _ := minMaxFromPrime(prime) + return minVal, big.NewInt(-1), nil + case ld.XSDNS + "nonPositiveInteger": + minVal, _ := minMaxFromPrime(prime) + return minVal, big.NewInt(0), nil + default: + return nil, nil, fmt.Errorf("unsupported XSD type: %s", xsdType) + } +} + +func convertStringToXSDValue(datatype string, value string, + maxFieldValue *big.Int) (resultValue interface{}, err error) { switch datatype { case ld.XSDBoolean: @@ -1282,31 +1337,37 @@ func convertStringToXSDValue(datatype string, err = errors.New("incorrect boolean value") } - case ld.XSDInteger, + case ld.XSDNS + "positiveInteger", ld.XSDNS + "nonNegativeInteger", - ld.XSDNS + "nonPositiveInteger", + ld.XSDInteger, ld.XSDNS + "negativeInteger", - ld.XSDNS + "positiveInteger": + ld.XSDNS + "nonPositiveInteger": - var r = new(big.Rat) - _, ok := r.SetString(value) - if !ok { - err = fmt.Errorf("can't parse number: %v", value) + var i *big.Int + i, err = intFromStr(value) + if err != nil { break } - if !r.IsInt() { - err = fmt.Errorf("integer has fractional part: %v", value) + var minVal, maxVal *big.Int + minVal, maxVal, err = minMaxByXSDType(datatype, maxFieldValue) + if err != nil { break } - i := r.Num() - if !i.IsInt64() { - err = fmt.Errorf("integer is too big for int64: %v", i.String()) + if i.Cmp(maxVal) > 0 { + err = fmt.Errorf("integer exceeds maximum value: %v", + i.String()) break } - resultValue = i.Int64() + if i.Cmp(minVal) < 0 { + err = fmt.Errorf("integer is below minimum value: %v", + i.String()) + break + } + + resultValue = i case ld.XSDNS + "dateTime": if dateRE.MatchString(value) { @@ -1554,11 +1615,7 @@ func MerklizeJSONLD(ctx context.Context, in io.Reader, } proc := ld.NewJsonLdProcessor() - options := ld.NewJsonLdOptions("") - options.Algorithm = ld.AlgorithmURDNA2015 - options.SafeMode = mz.safeMode - options.DocumentLoader = mz.getDocumentLoader() - + options := newJSONLDOptions(mz.safeMode, mz.getDocumentLoader()) normDoc, err := proc.Normalize(obj, options) if err != nil { return nil, err @@ -1593,14 +1650,14 @@ func MerklizeJSONLD(ctx context.Context, in io.Reader, return mz, err } -func (m *Merklizer) entry(path Path) (RDFEntry, error) { +func (m *Merklizer) Entry(path Path) (RDFEntry, error) { key, err := path.MtEntry() if err != nil { return RDFEntry{}, err } e, ok := m.entries[key.String()] if !ok { - return RDFEntry{}, errors.New("entry not found") + return RDFEntry{}, ErrorEntryNotFound } return e, nil @@ -1613,7 +1670,7 @@ func (m *Merklizer) getDocumentLoader() ld.DocumentLoader { if m.ipfsCli == nil && m.ipfsGW == "" { return defaultDocumentLoader } - return NewDocumentLoader(m.ipfsCli, m.ipfsGW) + return loaders.NewDocumentLoader(m.ipfsCli, m.ipfsGW) } func rvExtractObjField(obj any, field string) (any, error) { @@ -1686,7 +1743,7 @@ func (m *Merklizer) RawValue(path Path) (any, error) { // JSONLDType returns the JSON-LD type of the given path. If there is no literal // by this path, it returns an error. func (m *Merklizer) JSONLDType(path Path) (string, error) { - entry, err := m.entry(path) + entry, err := m.Entry(path) if err != nil { return "", err } @@ -1709,6 +1766,13 @@ func (m *Merklizer) ResolveDocPath(path string) (Path, error) { return realPath, nil } +func (m *Merklizer) Options() Options { + return Options{ + Hasher: m.hasher, + DocumentLoader: m.getDocumentLoader(), + } +} + // Proof generate and return Proof and Value by the given Path. // If the path is not found, it returns nil as value interface. func (m *Merklizer) Proof(ctx context.Context, @@ -1729,7 +1793,7 @@ func (m *Merklizer) Proof(ctx context.Context, entry, ok := m.entries[keyHash.String()] if !ok { return nil, nil, errors.New( - "[assertion] no entry found while existence is true") + "[assertion] no Entry found while existence is true") } value, err = NewValue(m.hasher, entry.value) if err != nil { @@ -1772,6 +1836,8 @@ func mkValueMtEntry(h Hasher, v interface{}) (*big.Int, error) { return mkValueString(h, et) case time.Time: return mkValueTime(h, et) + case *big.Int: + return mkValueBigInt(h, et) default: return nil, fmt.Errorf("unexpected value type: %T", v) } @@ -1810,6 +1876,24 @@ func mkValueTime(h Hasher, val time.Time) (*big.Int, error) { return x, nil } +func mkValueBigInt(h Hasher, val *big.Int) (*big.Int, error) { + if val.Cmp(h.Prime()) >= 0 { + return nil, fmt.Errorf("value is too big: %v", val.String()) + } + if val.Cmp(big.NewInt(0)) < 0 { + minValue, _ := minMaxFromPrime(h.Prime()) + + if val.Cmp(minValue) < 0 { + return nil, fmt.Errorf("value is too small: %v", + val.String()) + } + + return new(big.Int).Add(val, h.Prime()), nil + } + + return val, nil +} + // assert consistency of dataset and validate that only // quads we support contains in dataset. func assertDatasetConsistency(ds *ld.RDFDataset) error { @@ -1849,3 +1933,19 @@ func assertDatasetConsistency(ds *ld.RDFDataset) error { } return nil } + +func newJSONLDOptions(safeMode bool, docLoader ld.DocumentLoader) *ld.JsonLdOptions { + options := ld.NewJsonLdOptions("") + options.Algorithm = ld.AlgorithmURDNA2015 + options.SafeMode = safeMode + options.DocumentLoader = docLoader + return options +} + +func minMaxFromPrime(primeVal *big.Int) (*big.Int, *big.Int) { + maxValue := new(big.Int).Div(primeVal, big.NewInt(2)) + minValue := new(big.Int).Add( + new(big.Int).Sub(maxValue, primeVal), + big.NewInt(1)) + return minValue, maxValue +} diff --git a/merklize/merklize_test.go b/merklize/merklize_test.go index 93b0489..0d8f0ff 100644 --- a/merklize/merklize_test.go +++ b/merklize/merklize_test.go @@ -5,17 +5,12 @@ import ( "context" "encoding/json" "fmt" - "io" - "log" "math" "math/big" - "net/http" - "net/http/httptest" "os" "reflect" "strconv" "strings" - "sync" "testing" "text/template" "time" @@ -24,6 +19,8 @@ import ( "github.com/iden3/go-iden3-crypto/poseidon" "github.com/iden3/go-merkletree-sql/v2" "github.com/iden3/go-merkletree-sql/v2/db/memory" + "github.com/iden3/go-schema-processor/v2/loaders" + tst "github.com/iden3/go-schema-processor/v2/testing" shell "github.com/ipfs/go-ipfs-api" "github.com/piprate/json-gold/ld" "github.com/stretchr/testify/assert" @@ -249,37 +246,6 @@ func mkPath(parts ...interface{}) Path { return p } -//nolint:deadcode,unused // use for generation of wantEntries -func printEntriesRepresentation(entries []RDFEntry) { - for _, e := range entries { - var pathParts []string - for _, p := range e.key.parts { - switch p2 := p.(type) { - case string: - pathParts = append(pathParts, `"`+p2+`"`) - case int: - pathParts = append(pathParts, strconv.Itoa(p2)) - default: - panic(p) - } - } - - var value string - switch v2 := e.value.(type) { - case string: - value = `"` + v2 + `"` - case int64: - value = `int64(` + strconv.FormatInt(v2, 10) + `)` - default: - panic(fmt.Sprintf("%[1]T -- %[1]v", e.value)) - } - fmt.Println("{") - fmt.Printf("key: mkPath(%v),\n", strings.Join(pathParts, ",")) - fmt.Printf("value: %v,\n", value) - fmt.Println("},") - } -} - func TestEntriesFromRDF_multigraph(t *testing.T) { dataset := getDataset(t, multigraphDoc2) @@ -312,7 +278,9 @@ func TestEntriesFromRDF_multigraph(t *testing.T) { "https://github.com/iden3/claim-schema-vocab/blob/main/proofs/Iden3SparseMerkleTreeProof-v2.md#issuerData", "https://github.com/iden3/claim-schema-vocab/blob/main/proofs/Iden3SparseMerkleTreeProof-v2.md#state", "https://github.com/iden3/claim-schema-vocab/blob/main/proofs/Iden3SparseMerkleTreeProof-v2.md#blockTimestamp"), - value: int64(123), + + value: big.NewInt(123), + datatype: "http://www.w3.org/2001/XMLSchema#integer", }, { @@ -324,7 +292,9 @@ func TestEntriesFromRDF_multigraph(t *testing.T) { key: mkPath("https://www.w3.org/2018/credentials#verifiableCredential", 1, "https://github.com/iden3/claim-schema-vocab/blob/main/credentials/kyc.md#birthday"), - value: int64(19960424), + + value: big.NewInt(19960424), + datatype: "http://www.w3.org/2001/XMLSchema#integer", }, } @@ -513,8 +483,10 @@ func TestEntriesFromRDF(t *testing.T) { datatype: "http://www.w3.org/2001/XMLSchema#string", }, { - key: mkPath("http://schema.org/identifier"), - value: int64(83627465), + key: mkPath("http://schema.org/identifier"), + + value: big.NewInt(83627465), + datatype: "http://www.w3.org/2001/XMLSchema#integer", }, { @@ -779,13 +751,6 @@ func logDataset(in *ld.RDFDataset) { } } -//nolint:deadcode,unused //reason: used in debugging -func logEntries(es []RDFEntry) { - for i, e := range es { - log.Printf("Entry %v: %v => %v", i, fmtPath(e.key), e.value) - } -} - //nolint:deadcode,unused //reason: used in debugging func fmtPath(p Path) string { var parts []string @@ -1002,9 +967,9 @@ func TestExistenceProof(t *testing.T) { require.NoError(t, err) require.True(t, p.Existence) - i, err := v.AsInt64() + i, err := v.AsBigInt() require.NoError(t, err) - require.Equal(t, int64(19960424), i) + require.Equal(t, 0, i.Cmp(big.NewInt(19960424)), i) } func TestExistenceProofIPFS(t *testing.T) { @@ -1024,9 +989,9 @@ func TestExistenceProofIPFS(t *testing.T) { require.NoError(t, err) require.True(t, p.Existence) - i, err := v.AsInt64() + i, err := v.AsBigInt() require.NoError(t, err) - require.Equal(t, int64(1), i) + require.Equal(t, 0, big.NewInt(1).Cmp(i)) } func findQuadByObject(t testing.TB, ds *ld.RDFDataset, value any) *ld.Quad { @@ -1298,12 +1263,17 @@ func TestHashValues_FromDocument(t *testing.T) { ctxBytes, err := os.ReadFile("testdata/kyc_schema.json-ld") require.NoError(t, err) + ctxBytes2, err := os.ReadFile("testdata/custom_schema.json") + require.NoError(t, err) + tests := []struct { name string + ctxBytes []byte pathToField string datatype string value interface{} wantHash string + wantHashErr string }{ { name: "xsd:integer", @@ -1389,17 +1359,84 @@ func TestHashValues_FromDocument(t *testing.T) { value: float64(19960424), wantHash: "19960424", }, + { + name: "max value for positive integer", + ctxBytes: ctxBytes2, + pathToField: "TestType1.positiveNumber", + datatype: "http://www.w3.org/2001/XMLSchema#positiveInteger", + value: "21888242871839275222246405745257275088548364400416034343698204186575808495616", + wantHash: "21888242871839275222246405745257275088548364400416034343698204186575808495616", + }, + { + name: "max value for positive integer - too large error", + ctxBytes: ctxBytes2, + pathToField: "TestType1.positiveNumber", + datatype: "http://www.w3.org/2001/XMLSchema#positiveInteger", + value: "21888242871839275222246405745257275088548364400416034343698204186575808495617", + wantHashErr: "integer exceeds maximum value: 21888242871839275222246405745257275088548364400416034343698204186575808495617", + }, + { + name: "max value for positive integer - negative error", + ctxBytes: ctxBytes2, + pathToField: "TestType1.positiveNumber", + datatype: "http://www.w3.org/2001/XMLSchema#positiveInteger", + value: "-100500", + wantHashErr: "integer is below minimum value: -100500", + }, + { + name: "max value for integer", + pathToField: "KYCCountryOfResidenceCredential.countryCode", + datatype: "http://www.w3.org/2001/XMLSchema#integer", + value: "10944121435919637611123202872628637544274182200208017171849102093287904247808", + wantHash: "10944121435919637611123202872628637544274182200208017171849102093287904247808", + }, + { + name: "max value for integer - too large error", + pathToField: "KYCCountryOfResidenceCredential.countryCode", + datatype: "http://www.w3.org/2001/XMLSchema#integer", + value: "10944121435919637611123202872628637544274182200208017171849102093287904247809", + wantHashErr: "integer exceeds maximum value: 10944121435919637611123202872628637544274182200208017171849102093287904247809", + }, + { + name: "max value for integer - -1", + pathToField: "KYCCountryOfResidenceCredential.countryCode", + datatype: "http://www.w3.org/2001/XMLSchema#integer", + value: "-1", + wantHash: "21888242871839275222246405745257275088548364400416034343698204186575808495616", + }, + { + name: "max value for integer - minimum value", + pathToField: "KYCCountryOfResidenceCredential.countryCode", + datatype: "http://www.w3.org/2001/XMLSchema#integer", + value: "-10944121435919637611123202872628637544274182200208017171849102093287904247808", + wantHash: "10944121435919637611123202872628637544274182200208017171849102093287904247809", + }, + { + name: "max value for integer - too small error", + pathToField: "KYCCountryOfResidenceCredential.countryCode", + datatype: "http://www.w3.org/2001/XMLSchema#integer", + value: "-10944121435919637611123202872628637544274182200208017171849102093287904247809", + wantHashErr: "integer is below minimum value: -10944121435919637611123202872628637544274182200208017171849102093287904247809", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - actualType, err := TypeFromContext(ctxBytes, tt.pathToField) + ldContext := ctxBytes + if tt.ctxBytes != nil { + ldContext = tt.ctxBytes + } + actualType, err := TypeFromContext(ldContext, tt.pathToField) require.NoError(t, err) require.Equal(t, tt.datatype, actualType) actualHash, err := HashValue(tt.datatype, tt.value) - require.NoError(t, err) - require.Equal(t, tt.wantHash, actualHash.String()) + if tt.wantHashErr == "" { + require.NoError(t, err) + require.Equal(t, tt.wantHash, actualHash.String()) + } else { + require.EqualError(t, err, tt.wantHashErr) + } }) } } @@ -1846,9 +1883,9 @@ func TestIPFSContext(t *testing.T) { t.Run("no ipfs client", func(t *testing.T) { // ignoreUntouchedURLs is used because we can check IPFS schema // before HTTP and do not touch a mocked request - defer mockHTTPClient(t, map[string]string{ + defer tst.MockHTTPClient(t, map[string]string{ "https://www.w3.org/2018/credentials/v1": "testdata/httpresp/credentials-v1.jsonld", - }, ignoreUntouchedURLs())() + }, tst.IgnoreUntouchedURLs())() _, err = MerklizeJSONLD(ctx, bytes.NewReader(b.Bytes())) require.ErrorContains(t, err, @@ -1856,7 +1893,7 @@ func TestIPFSContext(t *testing.T) { }) t.Run("with ipfs client", func(t *testing.T) { - defer mockHTTPClient(t, map[string]string{ + defer tst.MockHTTPClient(t, map[string]string{ "https://www.w3.org/2018/credentials/v1": "testdata/httpresp/credentials-v1.jsonld", })() @@ -1869,14 +1906,14 @@ func TestIPFSContext(t *testing.T) { }) t.Run("with default ipfs client", func(t *testing.T) { - defer mockHTTPClient(t, map[string]string{ + defer tst.MockHTTPClient(t, map[string]string{ "https://www.w3.org/2018/credentials/v1": "testdata/httpresp/credentials-v1.jsonld", })() oldDocLoader := defaultDocumentLoader t.Cleanup(func() { SetDocumentLoader(oldDocLoader) }) - docLoader := NewDocumentLoader(ipfsCli, "") + docLoader := loaders.NewDocumentLoader(ipfsCli, "") SetDocumentLoader(docLoader) mz, err2 := MerklizeJSONLD(ctx, bytes.NewReader(b.Bytes())) @@ -1888,7 +1925,7 @@ func TestIPFSContext(t *testing.T) { // If both IPFS client and gateway URL are provided, the client is used. t.Run("with ipfs client and gateway URL", func(t *testing.T) { - defer mockHTTPClient(t, map[string]string{ + defer tst.MockHTTPClient(t, map[string]string{ "https://www.w3.org/2018/credentials/v1": "testdata/httpresp/credentials-v1.jsonld", })() @@ -1903,7 +1940,7 @@ func TestIPFSContext(t *testing.T) { t.Run("with ipfs gateway", func(t *testing.T) { ipfsGW := "http://ipfs.io" - defer mockHTTPClient(t, map[string]string{ + defer tst.MockHTTPClient(t, map[string]string{ "https://www.w3.org/2018/credentials/v1": "testdata/httpresp/credentials-v1.jsonld", ipfsGW + "/ipfs/QmdP4MZkESEabRVB322r2xWm7TCi7LueMNWMJawYmSy7hp": "testdata/ipfs/citizenship-v1.jsonld", ipfsGW + "/ipfs/Qmbp4kwoHULnmK71abrxdksjPH5sAjxSAXU5PEp2XRMFNw/dir2/bbs-v2.jsonld": "testdata/ipfs/dir1/dir2/bbs-v2.jsonld", @@ -1918,11 +1955,11 @@ func TestIPFSContext(t *testing.T) { }) t.Run("with document loader", func(t *testing.T) { - defer mockHTTPClient(t, map[string]string{ + defer tst.MockHTTPClient(t, map[string]string{ "https://www.w3.org/2018/credentials/v1": "testdata/httpresp/credentials-v1.jsonld", })() - docLoader := NewDocumentLoader(ipfsCli, "") + docLoader := loaders.NewDocumentLoader(ipfsCli, "") mz, err2 := MerklizeJSONLD(ctx, bytes.NewReader(b.Bytes()), WithDocumentLoader(docLoader)) require.NoError(t, err2) @@ -1937,7 +1974,7 @@ func TestIPFSContext(t *testing.T) { SetDocumentLoader(oldDefaultDocumentLoader) }) - docLoader := NewDocumentLoader(ipfsCli, "") + docLoader := loaders.NewDocumentLoader(ipfsCli, "") SetDocumentLoader(docLoader) in := "credentialSubject.1.testNewTypeInt" @@ -1954,7 +1991,7 @@ func TestIPFSContext(t *testing.T) { }) t.Run("NewPathFromDocument with document loader option", func(t *testing.T) { - docLoader := NewDocumentLoader(ipfsCli, "") + docLoader := loaders.NewDocumentLoader(ipfsCli, "") opts := Options{DocumentLoader: docLoader} in := "credentialSubject.1.testNewTypeInt" @@ -1971,98 +2008,3 @@ func TestIPFSContext(t *testing.T) { }) } - -type mockedRouterTripper struct { - t testing.TB - routes map[string]string - seenURLsM sync.Mutex - seenURLs map[string]struct{} -} - -func (m *mockedRouterTripper) RoundTrip( - request *http.Request) (*http.Response, error) { - - urlStr := request.URL.String() - routerKey := urlStr - rr := httptest.NewRecorder() - var postData []byte - if request.Method == http.MethodPost { - var err error - postData, err = io.ReadAll(request.Body) - if err != nil { - http.Error(rr, err.Error(), http.StatusInternalServerError) - - httpResp := rr.Result() - httpResp.Request = request - return httpResp, nil - } - if len(postData) > 0 { - routerKey += "%%%" + string(postData) - } - } - - respFile, ok := m.routes[routerKey] - if !ok { - var requestBodyStr = string(postData) - if requestBodyStr == "" { - m.t.Errorf("unexpected http request: %v", urlStr) - } else { - m.t.Errorf("unexpected http request: %v\nBody: %v", - urlStr, requestBodyStr) - } - rr2 := httptest.NewRecorder() - rr2.WriteHeader(http.StatusNotFound) - httpResp := rr2.Result() - httpResp.Request = request - return httpResp, nil - } - - m.seenURLsM.Lock() - if m.seenURLs == nil { - m.seenURLs = make(map[string]struct{}) - } - m.seenURLs[routerKey] = struct{}{} - m.seenURLsM.Unlock() - - http.ServeFile(rr, request, respFile) - - rr2 := rr.Result() - rr2.Request = request - return rr2, nil -} - -type mockHTTPClientOptions struct { - ignoreUntouchedURLs bool -} - -type mockHTTPClientOption func(*mockHTTPClientOptions) - -func ignoreUntouchedURLs() mockHTTPClientOption { - return func(opts *mockHTTPClientOptions) { - opts.ignoreUntouchedURLs = true - } -} - -func mockHTTPClient(t testing.TB, routes map[string]string, - opts ...mockHTTPClientOption) func() { - - var op mockHTTPClientOptions - for _, o := range opts { - o(&op) - } - - oldRoundTripper := http.DefaultTransport - transport := &mockedRouterTripper{t: t, routes: routes} - http.DefaultTransport = transport - return func() { - http.DefaultTransport = oldRoundTripper - - if !op.ignoreUntouchedURLs { - for u := range routes { - _, ok := transport.seenURLs[u] - assert.True(t, ok, - "found a URL in routes that we did not touch: %v", u) - } - } - } -} diff --git a/merklize/testdata/custom_schema.json b/merklize/testdata/custom_schema.json index 0124a3c..83f082e 100644 --- a/merklize/testdata/custom_schema.json +++ b/merklize/testdata/custom_schema.json @@ -232,6 +232,18 @@ } }, + "TestType1": { + "@id": "https://example.com/TestType1", + "@context": { + "@version": 1.1, + "xsd": "http://www.w3.org/2001/XMLSchema#", + "positiveNumber": { + "@id": "https://example.com/TestType1#positiveNumber", + "@type": "xsd:positiveInteger" + } + } + }, + "proof": {"@id": "https://w3id.org/security#proof", "@type": "@id", "@container": "@graph"} } } diff --git a/processor/json/processor.go b/processor/json/processor.go index e66d593..acc42c6 100644 --- a/processor/json/processor.go +++ b/processor/json/processor.go @@ -1,7 +1,7 @@ package json import ( - "github.com/iden3/go-schema-processor/processor" + "github.com/iden3/go-schema-processor/v2/processor" ) // Processor is set of tool for claim processing diff --git a/processor/json/processor_test.go b/processor/json/processor_test.go index d3737f4..3bce0c2 100644 --- a/processor/json/processor_test.go +++ b/processor/json/processor_test.go @@ -2,90 +2,80 @@ package json import ( "context" - commonJSON "encoding/json" "testing" - json "github.com/iden3/go-schema-processor/json" - "github.com/iden3/go-schema-processor/processor" - - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" + "github.com/iden3/go-schema-processor/v2/json" + "github.com/iden3/go-schema-processor/v2/loaders" + "github.com/iden3/go-schema-processor/v2/processor" + tst "github.com/iden3/go-schema-processor/v2/testing" + "github.com/stretchr/testify/require" ) -type MockLoader struct { -} - -func (l MockLoader) Load(ctx context.Context) (schema []byte, extension string, err error) { - return []byte(`{"type":"object","required":["documentType","birthday"],"properties":{"documentType":{"type":"integer"},"birthday":{"type":"integer"}}}`), "json", nil - -} - func TestInit(t *testing.T) { + defer tst.MockHTTPClient(t, map[string]string{ + "https://example.com/schema.json": "testdata/schema.json", + }, tst.IgnoreUntouchedURLs())() - loader := MockLoader{} + loader := loaders.NewDocumentLoader(nil, "") validator := json.Validator{} parser := json.Parser{} - jsonProcessor := New(processor.WithValidator(validator), processor.WithParser(parser), processor.WithSchemaLoader(loader)) - errLoaderNotDefined := errors.New("loader is not defined") - - _, _, err := jsonProcessor.Load(context.Background()) - - notDefinedError := errors.Is(errLoaderNotDefined, err) - assert.Equal(t, false, notDefinedError) + jsonProcessor := New(processor.WithValidator(validator), + processor.WithParser(parser), processor.WithDocumentLoader(loader)) + ctx := context.Background() + _, err := jsonProcessor.Load(ctx, "https://example.com/schema.json") + require.NoError(t, err) } func TestValidator(t *testing.T) { + defer tst.MockHTTPClient(t, map[string]string{ + "https://example.com/schema.json": "testdata/schema.json", + }, tst.IgnoreUntouchedURLs())() - loader := MockLoader{} + loader := loaders.NewDocumentLoader(nil, "") validator := json.Validator{} parser := json.Parser{} - jsonProcessor := New(processor.WithValidator(validator), processor.WithParser(parser), processor.WithSchemaLoader(loader)) + jsonProcessor := New(processor.WithValidator(validator), + processor.WithParser(parser), processor.WithDocumentLoader(loader)) - schema, ext, err := jsonProcessor.Load(context.Background()) + ctx := context.Background() + schema, err := jsonProcessor.Load(ctx, "https://example.com/schema.json") + require.NoError(t, err) + require.NotEmpty(t, schema) - assert.Nil(t, err) - assert.Equal(t, ext, "json") - assert.NotEmpty(t, schema) - - data := make(map[string]interface{}) - data["birthday"] = 1 - data["documentType"] = 1 - - dataBytes, err := commonJSON.Marshal(data) - assert.Nil(t, err) + dataBytes := []byte(`{ + "birthday": 1, + "documentType": 1 +}`) err = jsonProcessor.ValidateData(dataBytes, schema) - - assert.Nil(t, err) - + require.NoError(t, err) } func TestValidatorWithInvalidField(t *testing.T) { + defer tst.MockHTTPClient(t, map[string]string{ + "https://example.com/schema.json": "testdata/schema.json", + }, tst.IgnoreUntouchedURLs())() - loader := MockLoader{} + loader := loaders.NewDocumentLoader(nil, "") validator := json.Validator{} parser := json.Parser{} - jsonProcessor := New(processor.WithValidator(validator), processor.WithParser(parser), processor.WithSchemaLoader(loader)) + jsonProcessor := New(processor.WithValidator(validator), + processor.WithParser(parser), processor.WithDocumentLoader(loader)) - schema, ext, err := jsonProcessor.Load(context.Background()) + schema, err := jsonProcessor.Load(context.Background(), + "https://example.com/schema.json") - assert.Nil(t, err) - assert.Equal(t, ext, "json") - assert.NotEmpty(t, schema) + require.NoError(t, err) + require.NotEmpty(t, schema) - data := make(map[string]interface{}) - data["documentType"] = 1 - - dataBytes, err := commonJSON.Marshal(data) - assert.Nil(t, err) + dataBytes := []byte(`{ + "documentType": 1 +}`) err = jsonProcessor.ValidateData(dataBytes, schema) - - assert.NotNil(t, err) - assert.Containsf(t, err.Error(), "missing properties: 'birthday'", "expected error containing %q, got %s", "missing properties: 'birthDayYear'", err) - + require.ErrorContains(t, err, "missing properties: 'birthday'") } diff --git a/processor/json/testdata/schema.json b/processor/json/testdata/schema.json new file mode 100644 index 0000000..44ab2f7 --- /dev/null +++ b/processor/json/testdata/schema.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "required": [ + "documentType", + "birthday" + ], + "properties": { + "documentType": { + "type": "integer" + }, + "birthday": { + "type": "integer" + } + } +} \ No newline at end of file diff --git a/processor/processor.go b/processor/processor.go index 89ebe5a..c63f707 100644 --- a/processor/processor.go +++ b/processor/processor.go @@ -2,18 +2,20 @@ package processor import ( "context" + "encoding/json" - core "github.com/iden3/go-iden3-core" - "github.com/iden3/go-schema-processor/merklize" - "github.com/iden3/go-schema-processor/verifiable" + core "github.com/iden3/go-iden3-core/v2" + "github.com/iden3/go-schema-processor/v2/merklize" + "github.com/iden3/go-schema-processor/v2/verifiable" + "github.com/piprate/json-gold/ld" "github.com/pkg/errors" ) // Processor is set of tool for claim processing type Processor struct { - Validator Validator - SchemaLoader SchemaLoader - Parser Parser + Validator Validator + DocumentLoader ld.DocumentLoader + Parser Parser } // Validator is interface to validate data and documents @@ -21,22 +23,11 @@ type Validator interface { ValidateData(data, schema []byte) error } -// SchemaLoader is interface to load schema -type SchemaLoader interface { - Load(ctx context.Context) (schema []byte, extension string, err error) -} - -// ParsedSlots is struct that represents iden3 claim specification -type ParsedSlots struct { - IndexA, IndexB []byte - ValueA, ValueB []byte -} - // Parser is an interface to parse claim slots type Parser interface { - ParseClaim(ctx context.Context, credential verifiable.W3CCredential, credentialType string, jsonSchemaBytes []byte, options *CoreClaimOptions) (*core.Claim, error) - ParseSlots(credential verifiable.W3CCredential, schemaBytes []byte) (ParsedSlots, error) - GetFieldSlotIndex(field string, schema []byte) (int, error) + ParseClaim(ctx context.Context, credential verifiable.W3CCredential, + options *CoreClaimOptions) (*core.Claim, error) + GetFieldSlotIndex(field string, typeName string, schema []byte) (int, error) } // CoreClaimOptions is params for core claim parsing @@ -53,8 +44,6 @@ var ( errParserNotDefined = errors.New("parser is not defined") errLoaderNotDefined = errors.New("loader is not defined") errValidatorNotDefined = errors.New("validator is not defined") - // ErrSlotsOverflow thrown on claim slot overflow - ErrSlotsOverflow = errors.New("slots overflow") ) // Opt returns configuration options for processor suite @@ -67,10 +56,10 @@ func WithValidator(s Validator) Opt { } } -// WithSchemaLoader return new options -func WithSchemaLoader(s SchemaLoader) Opt { +// WithDocumentLoader return new options +func WithDocumentLoader(s ld.DocumentLoader) Opt { return func(opts *Processor) { - opts.SchemaLoader = s + opts.DocumentLoader = s } } @@ -90,35 +79,36 @@ func InitProcessorOptions(processor *Processor, opts ...Opt) *Processor { } // Load will load a schema by given url. -func (s *Processor) Load(ctx context.Context) (schema []byte, extension string, err error) { - if s.SchemaLoader == nil { - return nil, "", errLoaderNotDefined +func (s *Processor) Load(ctx context.Context, url string) (schema []byte, err error) { + if s.DocumentLoader == nil { + return nil, errLoaderNotDefined } - return s.SchemaLoader.Load(ctx) -} - -// ParseSlots will serialize input data to index and value fields. -func (s *Processor) ParseSlots(credential verifiable.W3CCredential, schema []byte) (ParsedSlots, error) { - if s.Parser == nil { - return ParsedSlots{}, errParserNotDefined + doc, err := s.DocumentLoader.LoadDocument(url) + if err != nil { + return nil, err } - return s.Parser.ParseSlots(credential, schema) + return json.Marshal(doc.Document) } // ParseClaim will serialize input data to index and value fields. -func (s *Processor) ParseClaim(ctx context.Context, credential verifiable.W3CCredential, credentialType string, jsonSchemaBytes []byte, opts *CoreClaimOptions) (*core.Claim, error) { +func (s *Processor) ParseClaim(ctx context.Context, + credential verifiable.W3CCredential, + opts *CoreClaimOptions) (*core.Claim, error) { + if s.Parser == nil { return nil, errParserNotDefined } - return s.Parser.ParseClaim(ctx, credential, credentialType, jsonSchemaBytes, opts) + return s.Parser.ParseClaim(ctx, credential, opts) } // GetFieldSlotIndex returns index of slot for specified field according to schema -func (s *Processor) GetFieldSlotIndex(field string, schema []byte) (int, error) { +func (s *Processor) GetFieldSlotIndex(field string, typeName string, + schema []byte) (int, error) { + if s.Parser == nil { return 0, errParserNotDefined } - return s.Parser.GetFieldSlotIndex(field, schema) + return s.Parser.GetFieldSlotIndex(field, typeName, schema) } // ValidateData will validate a claim content by given schema. diff --git a/testing/http.go b/testing/http.go new file mode 100644 index 0000000..d9f9273 --- /dev/null +++ b/testing/http.go @@ -0,0 +1,106 @@ +package testing + +import ( + "io" + "net/http" + "net/http/httptest" + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +type mockedRouterTripper struct { + t testing.TB + routes map[string]string + seenURLsM sync.Mutex + seenURLs map[string]struct{} +} + +func (m *mockedRouterTripper) RoundTrip( + request *http.Request) (*http.Response, error) { + + urlStr := request.URL.String() + routerKey := urlStr + rr := httptest.NewRecorder() + var postData []byte + if request.Method == http.MethodPost { + var err error + postData, err = io.ReadAll(request.Body) + if err != nil { + http.Error(rr, err.Error(), http.StatusInternalServerError) + + httpResp := rr.Result() + httpResp.Request = request + return httpResp, nil + } + if len(postData) > 0 { + routerKey += "%%%" + string(postData) + } + } + + respFile, ok := m.routes[routerKey] + if !ok { + var requestBodyStr = string(postData) + if requestBodyStr == "" { + m.t.Errorf("unexpected http request: %v", urlStr) + } else { + m.t.Errorf("unexpected http request: %v\nBody: %v", + urlStr, requestBodyStr) + } + rr2 := httptest.NewRecorder() + rr2.WriteHeader(http.StatusNotFound) + httpResp := rr2.Result() + httpResp.Request = request + return httpResp, nil + } + + m.seenURLsM.Lock() + if m.seenURLs == nil { + m.seenURLs = make(map[string]struct{}) + } + m.seenURLs[routerKey] = struct{}{} + m.seenURLsM.Unlock() + + http.ServeFile(rr, request, respFile) + + rr2 := rr.Result() + rr2.Request = request + return rr2, nil +} + +type mockHTTPClientOptions struct { + ignoreUntouchedURLs bool +} + +type MockHTTPClientOption func(*mockHTTPClientOptions) + +func IgnoreUntouchedURLs() MockHTTPClientOption { + return func(opts *mockHTTPClientOptions) { + opts.ignoreUntouchedURLs = true + } +} + +func MockHTTPClient(t testing.TB, routes map[string]string, + opts ...MockHTTPClientOption) func() { + + var op mockHTTPClientOptions + for _, o := range opts { + o(&op) + } + + oldRoundTripper := http.DefaultTransport + transport := &mockedRouterTripper{t: t, routes: routes} + http.DefaultTransport = transport + return func() { + http.DefaultTransport = oldRoundTripper + + if !op.ignoreUntouchedURLs { + for u := range routes { + _, ok := transport.seenURLs[u] + assert.True(t, ok, + "found a URL in routes that we did not touch: %v", u) + } + } + } +} diff --git a/utils/claims.go b/utils/claims.go index 2f65a16..c2c4119 100644 --- a/utils/claims.go +++ b/utils/claims.go @@ -1,11 +1,7 @@ package utils import ( - "fmt" - "math/big" - - core "github.com/iden3/go-iden3-core" - "github.com/pkg/errors" + core "github.com/iden3/go-iden3-core/v2" ) const ( @@ -27,49 +23,6 @@ const ( MerklizedRootPositionNone = "" ) -var q *big.Int - -// nolint //reason - needed -func init() { - qString := "21888242871839275222246405745257275088548364400416034343698204186575808495617" - var ok bool - q, ok = new(big.Int).SetString(qString, 10) - if !ok { - panic(fmt.Sprintf("Bad base 10 string %s", qString)) - } -} - -// FieldToByteArray convert fields to byte representation based on type -func FieldToByteArray(field interface{}) ([]byte, error) { - - var bigIntField *big.Int - var ok bool - - switch v := field.(type) { - case string: - bigIntField, ok = new(big.Int).SetString(v, 10) - if !ok { - return nil, errors.New("can't convert string to big int") - } - case float64: - stringField := fmt.Sprintf("%.0f", v) - bigIntField, ok = new(big.Int).SetString(stringField, 10) - if !ok { - return nil, errors.New("can't convert string to big int") - } - default: - return nil, errors.New("field type is not supported") - } - return SwapEndianness(bigIntField.Bytes()), nil -} - -// DataFillsSlot checks if newData fills into slot capacity () -func DataFillsSlot(slot, newData []byte) bool { - slot = append(slot, newData...) - a := new(big.Int).SetBytes(SwapEndianness(slot)) - return a.Cmp(q) == -1 -} - // SwapEndianness swaps the endianness of the value encoded in buf. If buf is // Big-Endian, the result will be Little-Endian and vice-versa. func SwapEndianness(buf []byte) []byte { diff --git a/verifiable/credential.go b/verifiable/credential.go index 32eeb47..7929ccd 100644 --- a/verifiable/credential.go +++ b/verifiable/credential.go @@ -6,15 +6,16 @@ import ( "encoding/json" "time" - core "github.com/iden3/go-iden3-core" + core "github.com/iden3/go-iden3-core/v2" mt "github.com/iden3/go-merkletree-sql/v2" - "github.com/iden3/go-schema-processor/merklize" + "github.com/iden3/go-schema-processor/v2/merklize" "github.com/pkg/errors" ) // W3CCredential is struct that represents claim json-ld document type W3CCredential struct { - ID string `json:"id"` + ID string `json:"id,omitempty"` + Context []string `json:"@context"` Type []string `json:"type"` Expiration *time.Time `json:"expirationDate,omitempty"` diff --git a/verifiable/credential_test.go b/verifiable/credential_test.go index 2073349..4bba09c 100644 --- a/verifiable/credential_test.go +++ b/verifiable/credential_test.go @@ -1,11 +1,14 @@ package verifiable import ( + "context" "encoding/json" + "os" "testing" "time" mt "github.com/iden3/go-merkletree-sql/v2" + tst "github.com/iden3/go-schema-processor/v2/testing" "github.com/stretchr/testify/require" ) @@ -174,3 +177,52 @@ func TestW3CCredential_JSONUnmarshal(t *testing.T) { } require.Equal(t, want, vc) } + +func TestW3CCredential_MerklizationWithEmptyID(t *testing.T) { + defer tst.MockHTTPClient(t, map[string]string{ + "https://www.w3.org/2018/credentials/v1": "../merklize/testdata/httpresp/credentials-v1.jsonld", + "https://example.com/schema-delivery-address.json-ld": "../json/testdata/schema-delivery-address.json-ld", + })() + + vcData, err := os.ReadFile("../json/testdata/non-merklized-1.json-ld") + require.NoError(t, err) + var vc W3CCredential + err = json.Unmarshal(vcData, &vc) + require.NoError(t, err) + + want := W3CCredential{ + ID: "", + Context: []string{ + "https://www.w3.org/2018/credentials/v1", + "https://example.com/schema-delivery-address.json-ld", + }, + Type: []string{ + "VerifiableCredential", + "DeliverAddressMultiTestForked", + }, + CredentialSubject: map[string]any{ + "type": "DeliverAddressMultiTestForked", + "price": "123.52", + "isPostalProvider": false, + "postalProviderInformation": map[string]any{ + "insured": true, + "weight": "1.3", + }, + }, + CredentialStatus: nil, + Issuer: "", + CredentialSchema: CredentialSchema{ + ID: "", + Type: "", + }, + } + require.Equal(t, want, vc) + + ctx := context.Background() + mz, err := vc.Merklize(ctx) + require.NoError(t, err) + path, err := mz.ResolveDocPath("credentialSubject.price") + require.NoError(t, err) + _, err = mz.Entry(path) + require.NoError(t, err) +} diff --git a/verifiable/did_doc.go b/verifiable/did_doc.go index 11788df..d134421 100644 --- a/verifiable/did_doc.go +++ b/verifiable/did_doc.go @@ -49,15 +49,17 @@ type DeviceMetadata struct { // CommonVerificationMethod DID doc verification method. type CommonVerificationMethod struct { - ID string `json:"id"` - Type string `json:"type"` - Controller string `json:"controller"` - PublicKeyJwk map[string]interface{} `json:"publicKeyJwk"` - PublicKeyMultibase string `json:"publicKeyMultibase,omitempty"` - PublicKeyHex string `json:"publicKeyHex,omitempty"` - PublicKeyBase58 string `json:"publicKeyBase58,omitempty"` - EthereumAddress string `json:"ethereumAddress,omitempty"` - BlockchainAccountID string `json:"blockchainAccountId,omitempty"` + ID string `json:"id"` + Type string `json:"type"` + Controller string `json:"controller"` + PublicKeyJwk map[string]interface{} `json:"publicKeyJwk,omitempty"` + PublicKeyMultibase string `json:"publicKeyMultibase,omitempty"` + PublicKeyHex string `json:"publicKeyHex,omitempty"` + PublicKeyBase58 string `json:"publicKeyBase58,omitempty"` + EthereumAddress string `json:"ethereumAddress,omitempty"` + BlockchainAccountID string `json:"blockchainAccountId,omitempty"` + StateContractAddress string `json:"stateContractAddress,omitempty"` + IdentityState } type Authentication struct { @@ -104,3 +106,31 @@ func (a *Authentication) MarshalJSON() ([]byte, error) { return json.Marshal(a) } } + +// StateInfo is information about identity state +type StateInfo struct { + ID string `json:"id"` + State string `json:"state"` + ReplacedByState string `json:"replacedByState"` + CreatedAtTimestamp string `json:"createdAtTimestamp"` + ReplacedAtTimestamp string `json:"replacedAtTimestamp"` + CreatedAtBlock string `json:"createdAtBlock"` + ReplacedAtBlock string `json:"replacedAtBlock"` +} + +// GistInfo representation state of gist root. +type GistInfo struct { + Root string `json:"root"` + ReplacedByRoot string `json:"replacedByRoot"` + CreatedAtTimestamp string `json:"createdAtTimestamp"` + ReplacedAtTimestamp string `json:"replacedAtTimestamp"` + CreatedAtBlock string `json:"createdAtBlock"` + ReplacedAtBlock string `json:"replacedAtBlock"` +} + +// IdentityState representation all info about identity. +type IdentityState struct { + Published *bool `json:"published,omitempty"` + Info *StateInfo `json:"info,omitempty"` + Global *GistInfo `json:"global,omitempty"` +} diff --git a/verifiable/proof.go b/verifiable/proof.go index 0a2a8c2..626ce47 100644 --- a/verifiable/proof.go +++ b/verifiable/proof.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" - core "github.com/iden3/go-iden3-core" + core "github.com/iden3/go-iden3-core/v2" "github.com/iden3/go-iden3-crypto/babyjub" mt "github.com/iden3/go-merkletree-sql/v2" ) diff --git a/verifiable/schema.go b/verifiable/schema.go index 993ec6b..5f88e05 100644 --- a/verifiable/schema.go +++ b/verifiable/schema.go @@ -104,6 +104,9 @@ const ( }, "ethereumAddress": { "type": "string" + }, + "stateContractAddress": { + "type": "string" } }, "required": [