diff --git a/.github/workflows/ci-lint.yaml b/.github/workflows/ci-lint.yaml index 8025c99..798f1dc 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.21.2 + go-version: 1.21.6 - uses: golangci/golangci-lint-action@v3 with: - version: v1.54.2 + version: v1.55.1 diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index 7927c71..65a3a58 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -14,8 +14,8 @@ jobs: containers: - 1.18.10-bullseye - 1.19.13-bullseye - - 1.20.9-bullseye - - 1.21.2-bullseye + - 1.20.13-bookworm + - 1.21.6-bookworm runs-on: ubuntu-latest container: golang:${{ matrix.containers }} steps: diff --git a/go.mod b/go.mod index a79d962..d1c3b97 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 github.com/stretchr/testify v1.8.4 - golang.org/x/crypto v0.7.0 + golang.org/x/crypto v0.12.0 ) require ( @@ -23,6 +23,7 @@ require ( github.com/dchest/blake512 v1.0.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.8.0 // indirect + golang.org/x/sys v0.15.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index baf7afd..77a8eae 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,11 @@ github.com/iden3/go-iden3-crypto v0.0.15 h1:4MJYlrot1l31Fzlo2sF56u7EVFeHHJkxGXXZ 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= github.com/iden3/go-merkletree-sql/v2 v2.0.4/go.mod h1:kRhHKYpui5DUsry5RpveP6IC4XMe6iApdV9VChRYuEk= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= @@ -23,11 +28,12 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 h1:uIkTLo0AGRc8l7h5l9r+GcYi9qfVP github.com/santhosh-tekuri/jsonschema/v5 v5.3.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/verifiable/credential.go b/verifiable/credential.go index 1e10499..047272c 100644 --- a/verifiable/credential.go +++ b/verifiable/credential.go @@ -3,11 +3,17 @@ package verifiable import ( "bytes" "context" + "encoding/hex" "encoding/json" + "fmt" + "math/big" "time" core "github.com/iden3/go-iden3-core/v2" - mt "github.com/iden3/go-merkletree-sql/v2" + "github.com/iden3/go-iden3-core/v2/w3c" + "github.com/iden3/go-iden3-crypto/babyjub" + "github.com/iden3/go-iden3-crypto/poseidon" + "github.com/iden3/go-merkletree-sql/v2" "github.com/iden3/go-schema-processor/v2/merklize" "github.com/pkg/errors" ) @@ -29,6 +35,273 @@ type W3CCredential struct { DisplayMethod *DisplayMethod `json:"displayMethod,omitempty"` } +// VerifyProof verify credential proof +func (vc *W3CCredential) VerifyProof(ctx context.Context, proofType ProofType, + didResolver DIDResolver, opts ...W3CProofVerificationOpt) error { + + verifyConfig := w3CProofVerificationConfig{} + for _, o := range opts { + o(&verifyConfig) + } + + var credProof CredentialProof + for _, p := range vc.Proof { + if p.ProofType() == proofType { + credProof = p + break + } + } + if credProof == nil { + return ErrProofNotFound + } + + coreClaim, err := credProof.GetCoreClaim() + if err != nil { + return errors.New("can't get core claim") + } + + switch proofType { + case BJJSignatureProofType: + var proof BJJSignatureProof2021 + err = remarshalObj(&proof, credProof) + if err != nil { + return err + } + return verifyBJJSignatureProof(ctx, proof, coreClaim, didResolver, + verifyConfig) + case Iden3SparseMerkleTreeProofType: + var proof Iden3SparseMerkleTreeProof + err = remarshalObj(&proof, credProof) + if err != nil { + return err + } + return verifyIden3SparseMerkleTreeProof(ctx, proof, coreClaim, + didResolver) + default: + return ErrProofNotSupported + } +} + +func verifyBJJSignatureProof(ctx context.Context, proof BJJSignatureProof2021, + coreClaim *core.Claim, didResolver DIDResolver, + verifyConfig w3CProofVerificationConfig) error { + + // issuer's claim with public key + authClaim, err := proof.IssuerData.authClaim() + if err != nil { + return err + } + + // core claim's signature + sig, err := bjjSignatureFromHexString(proof.Signature) + if err != nil || sig == nil { + return err + } + + err = verifyClaimSignature(coreClaim, sig, authClaim) + if err != nil { + return err + } + + issuerDID, err := w3c.ParseDID(proof.IssuerData.ID) + if err != nil { + return err + } + + issuerStateHash, err := merkletree.NewHashFromHex(*proof.IssuerData.State.Value) + if err != nil { + return fmt.Errorf("invalid state formant: %v", err) + } + + issuerDID.Query = fmt.Sprintf("state=%s", issuerStateHash.Hex()) + + didDoc, err := didResolver.Resolve(ctx, issuerDID) + if err != nil { + return err + } + + vm, err := getIden3StateInfo2023FromDIDDocument(didDoc) + if err != nil { + return err + } + + // Published or genesis + if !*vm.IdentityState.Published { + var ( + isGenesisState bool + issuerID core.ID + ) + issuerID, err = core.IDFromDID(*issuerDID) + if err != nil { + return err + } + isGenesisState, err = core.CheckGenesisStateID(issuerID.BigInt(), issuerStateHash.BigInt()) + if err != nil { + return err + } + if !isGenesisState { + return errors.New("issuer state not published and not genesis") + } + } + + err = validateAuthClaimRevocation(ctx, proof.IssuerData, + verifyConfig.credStatusValidationOpts...) + if err != nil { + return err + } + + return err +} + +func verifyClaimSignature(claim *core.Claim, sig *babyjub.Signature, + authClaim *core.Claim) error { + + publicKey := publicKeyFromClaim(authClaim) + + // core claim hash + hi, hv, err := claim.HiHv() + if err != nil { + return err + } + + claimHash, err := poseidon.Hash([]*big.Int{hi, hv}) + if err != nil { + return err + } + + valid := publicKey.VerifyPoseidon(claimHash, sig) + if !valid { + return errors.New("claim signature validation failed") + } + return nil +} + +func publicKeyFromClaim(claim *core.Claim) *babyjub.PublicKey { + rawSlotInts := claim.RawSlotsAsInts() + var publicKey babyjub.PublicKey + publicKey.X = rawSlotInts[2] // Ax should be in indexSlotA + publicKey.Y = rawSlotInts[3] // Ay should be in indexSlotB + return &publicKey +} + +func validateAuthClaimRevocation(ctx context.Context, issuerData IssuerData, + opts ...CredentialStatusValidationOption) error { + credStatus, err := coerceCredentialStatus(issuerData.CredentialStatus) + if err != nil { + return err + } + + authClaim, err := issuerData.authClaim() + if err != nil { + return err + } + + if credStatus.RevocationNonce != authClaim.GetRevocationNonce() { + return fmt.Errorf("revocation nonce mismatch: credential revocation "+ + "nonce (%v) != auth claim revocation nonce (%v)", + credStatus.RevocationNonce, authClaim.GetRevocationNonce()) + } + + _, err = ValidateCredentialStatus(ctx, *credStatus, opts...) + return err +} + +func verifyIden3SparseMerkleTreeProof(ctx context.Context, + proof Iden3SparseMerkleTreeProof, coreClaim *core.Claim, + didResolver DIDResolver) error { + + var err error + + issuerDID, err := w3c.ParseDID(proof.IssuerData.ID) + if err != nil { + return err + } + + issuerStateHash, err := merkletree.NewHashFromHex(*proof.IssuerData.State.Value) + if err != nil { + return fmt.Errorf("invalid state formant: %v", err) + } + + issuerDID.Query = fmt.Sprintf("state=%s", issuerStateHash.Hex()) + + didDoc, err := didResolver.Resolve(ctx, issuerDID) + if err != nil { + return err + } + + vm, err := getIden3StateInfo2023FromDIDDocument(didDoc) + if err != nil { + return err + } + + // Published or genesis + if !*vm.IdentityState.Published { + var ( + isGenesisState bool + issuerID core.ID + ) + issuerID, err = core.IDFromDID(*issuerDID) + if err != nil { + return err + } + isGenesisState, err = core.CheckGenesisStateID(issuerID.BigInt(), issuerStateHash.BigInt()) + if err != nil { + return err + } + if !isGenesisState { + return errors.New("issuer state not published and not genesis") + } + } + + // 3. root from proof == issuerData.state.сlaimsTreeRoot + hi, hv, err := coreClaim.HiHv() + if err != nil { + return err + } + + rootFromProof, err := merkletree.RootFromProof(proof.MTP, hi, hv) + if err != nil { + return err + } + issuerClaimsTreeRoot, err := merkletree.NewHashFromHex(*proof.IssuerData.State.ClaimsTreeRoot) + if err != nil { + return fmt.Errorf("invalid state formant: %v", err) + } + + if rootFromProof.BigInt().Cmp(issuerClaimsTreeRoot.BigInt()) != 0 { + return errors.New("verifyIden3SparseMerkleTreeProof: root from proof not equal to issuer data claims tree root") + } + + return nil +} + +func bjjSignatureFromHexString(sigHex string) (*babyjub.Signature, error) { + signatureBytes, err := hex.DecodeString(sigHex) + if err != nil { + return nil, errors.WithStack(err) + } + var sig [64]byte + copy(sig[:], signatureBytes) + bjjSig, err := new(babyjub.Signature).Decompress(sig) + return bjjSig, errors.WithStack(err) +} + +func getIden3StateInfo2023FromDIDDocument(document DIDDocument) (*CommonVerificationMethod, error) { + var iden3StateInfo2023 *CommonVerificationMethod + for _, a := range document.VerificationMethod { + if a.Type == "Iden3StateInfo2023" { + a2 := a + iden3StateInfo2023 = &a2 + break + } + } + if iden3StateInfo2023 == nil { + return nil, errors.New("Issuer Iden3StateInfo2023 auth info not found") + } + + return iden3StateInfo2023, nil +} + // Merklize merklizes verifiable credential func (vc *W3CCredential) Merklize(ctx context.Context, opts ...merklize.MerklizeOption) (*merklize.Merklizer, error) { @@ -62,6 +335,9 @@ func (vc *W3CCredential) Merklize(ctx context.Context, // ErrProofNotFound is an error when specific proof is not found in the credential var ErrProofNotFound = errors.New("proof not found") +// ErrProofNotSupported is an error when specific proof is not supported for validation +var ErrProofNotSupported = errors.New("proof not supported") + // GetCoreClaimFromProof returns core claim from given proof func (vc *W3CCredential) GetCoreClaimFromProof(proofType ProofType) (*core.Claim, error) { for _, p := range vc.Proof { @@ -100,11 +376,29 @@ type CredentialStatusType string // RevocationStatus status of revocation nonce. Info required to check revocation state of claim in circuits type RevocationStatus struct { - Issuer struct { - State *string `json:"state,omitempty"` - RootOfRoots *string `json:"rootOfRoots,omitempty"` - ClaimsTreeRoot *string `json:"claimsTreeRoot,omitempty"` - RevocationTreeRoot *string `json:"revocationTreeRoot,omitempty"` - } `json:"issuer"` - MTP mt.Proof `json:"mtp"` + Issuer TreeState `json:"issuer"` + MTP merkletree.Proof `json:"mtp"` +} + +type TreeState struct { + State *string `json:"state"` + RootOfRoots *string `json:"rootOfRoots,omitempty"` + ClaimsTreeRoot *string `json:"claimsTreeRoot,omitempty"` + RevocationTreeRoot *string `json:"revocationTreeRoot,omitempty"` +} + +// WithStatusResolverRegistry return new options +func WithStatusResolverRegistry(registry *CredentialStatusResolverRegistry) W3CProofVerificationOpt { + return func(opts *w3CProofVerificationConfig) { + opts.credStatusValidationOpts = append(opts.credStatusValidationOpts, + WithValidationStatusResolverRegistry(registry)) + } +} + +// W3CProofVerificationOpt returns configuration options for W3C proof verification +type W3CProofVerificationOpt func(opts *w3CProofVerificationConfig) + +// w3CProofVerificationConfig options for W3C proof verification +type w3CProofVerificationConfig struct { + credStatusValidationOpts []CredentialStatusValidationOption } diff --git a/verifiable/credential_status.go b/verifiable/credential_status.go new file mode 100644 index 0000000..d9e2623 --- /dev/null +++ b/verifiable/credential_status.go @@ -0,0 +1,163 @@ +package verifiable + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + + "github.com/iden3/go-iden3-crypto/poseidon" + "github.com/iden3/go-merkletree-sql/v2" + "github.com/pkg/errors" +) + +var ErrCredentialIsRevoked = errors.New("credential is revoked") + +type credentialStatusValidationOpts struct { + statusResolverRegistry *CredentialStatusResolverRegistry +} + +type CredentialStatusValidationOption func(*credentialStatusValidationOpts) error + +func WithValidationStatusResolverRegistry( + registry *CredentialStatusResolverRegistry) CredentialStatusValidationOption { + return func(opts *credentialStatusValidationOpts) error { + opts.statusResolverRegistry = registry + return nil + } +} + +// ValidateCredentialStatus resolves the credential status (possibly download +// proofs from outer world) and validates the proof. May return +// ErrCredentialIsRevoked if the credential was revoked. +func ValidateCredentialStatus(ctx context.Context, credStatus CredentialStatus, + opts ...CredentialStatusValidationOption) (RevocationStatus, error) { + + o := &credentialStatusValidationOpts{ + statusResolverRegistry: DefaultCredentialStatusResolverRegistry, + } + for _, opt := range opts { + err := opt(o) + if err != nil { + return RevocationStatus{}, err + } + } + + revocationStatus, err := resolveRevStatus(ctx, credStatus, + o.statusResolverRegistry) + if err != nil { + return revocationStatus, err + } + + treeStateOk, err := validateTreeState(revocationStatus.Issuer) + if err != nil { + return revocationStatus, err + } + if !treeStateOk { + return revocationStatus, errors.New("signature proof: invalid tree state of the issuer while checking credential status of singing key") + } + + revocationRootHash := &merkletree.HashZero + if revocationStatus.Issuer.RevocationTreeRoot != nil { + revocationRootHash, err = merkletree.NewHashFromHex(*revocationStatus.Issuer.RevocationTreeRoot) + if err != nil { + return revocationStatus, err + } + } + + revNonce := new(big.Int).SetUint64(credStatus.RevocationNonce) + proofValid := merkletree.VerifyProof(revocationRootHash, + &revocationStatus.MTP, revNonce, big.NewInt(0)) + if !proofValid { + return revocationStatus, fmt.Errorf("proof validation failed. revNonce=%d", revNonce) + } + + if revocationStatus.MTP.Existence { + return revocationStatus, ErrCredentialIsRevoked + } + + return revocationStatus, nil +} + +func coerceCredentialStatus(credStatus any) (*CredentialStatus, error) { + switch credStatusT := credStatus.(type) { + case *CredentialStatus: + return credStatusT, nil + case CredentialStatus: + return &credStatusT, nil + case jsonObj: + var credStatusTyped CredentialStatus + err := remarshalObj(&credStatusTyped, credStatusT) + if err != nil { + return nil, err + } + if credStatusTyped.Type == "" { + return nil, errors.New("credential status doesn't contain type") + } + return &credStatusTyped, nil + default: + return nil, errors.New("unknown credential status format") + } +} + +func resolveRevStatus(ctx context.Context, credStatus CredentialStatus, + credStatusResolverRegistry *CredentialStatusResolverRegistry) (out RevocationStatus, err error) { + + resolver, err := credStatusResolverRegistry.Get(credStatus.Type) + if err != nil { + return out, err + } + + return resolver.Resolve(ctx, credStatus) +} + +// marshal/unmarshal object from one type to other +func remarshalObj(dst, src any) error { + objBytes, err := json.Marshal(src) + if err != nil { + return err + } + return json.Unmarshal(objBytes, dst) +} + +// check Issuer TreeState consistency +func validateTreeState(i TreeState) (bool, error) { + if i.State == nil { + return false, errors.New("state is nil") + } + + var err error + ctrHash := &merkletree.HashZero + if i.ClaimsTreeRoot != nil { + ctrHash, err = merkletree.NewHashFromHex(*i.ClaimsTreeRoot) + if err != nil { + return false, err + } + } + rtrHash := &merkletree.HashZero + if i.RevocationTreeRoot != nil { + rtrHash, err = merkletree.NewHashFromHex(*i.RevocationTreeRoot) + if err != nil { + return false, err + } + } + rorHash := &merkletree.HashZero + if i.RootOfRoots != nil { + rorHash, err = merkletree.NewHashFromHex(*i.RootOfRoots) + if err != nil { + return false, err + } + } + + wantState, err := poseidon.Hash([]*big.Int{ctrHash.BigInt(), + rtrHash.BigInt(), rorHash.BigInt()}) + if err != nil { + return false, err + } + + stateHash, err := merkletree.NewHashFromHex(*i.State) + if err != nil { + return false, err + } + return wantState.Cmp(stateHash.BigInt()) == 0, nil +} diff --git a/verifiable/credential_test.go b/verifiable/credential_test.go index 4bba09c..0f32cbe 100644 --- a/verifiable/credential_test.go +++ b/verifiable/credential_test.go @@ -12,6 +12,476 @@ import ( "github.com/stretchr/testify/require" ) +type test1Resolver struct{} + +func (test1Resolver) Resolve(context context.Context, status CredentialStatus) (out RevocationStatus, err error) { + statusJSON := `{"issuer":{"state":"34824a8e1defc326f935044e32e9f513377dbfc031d79475a0190830554d4409","rootOfRoots":"37eabc712cdaa64793561b16b8143f56f149ad1b0c35297a1b125c765d1c071e","claimsTreeRoot":"4436ea12d352ddb84d2ac7a27bbf7c9f1bfc7d3ff69f3e6cf4348f424317fd0b","revocationTreeRoot":"0000000000000000000000000000000000000000000000000000000000000000"},"mtp":{"existence":false,"siblings":[]}}` + var rs RevocationStatus + _ = json.Unmarshal([]byte(statusJSON), &rs) + return rs, nil +} +func TestW3CCredential_ValidateBJJSignatureProof(t *testing.T) { + in := `{ + "id": "urn:uuid:3a8d1822-a00e-11ee-8f57-a27b3ddbdc29", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.iden3.io/core/jsonld/iden3proofs.jsonld", + "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld" + ], + "type": [ + "VerifiableCredential", + "KYCAgeCredential" + ], + "expirationDate": "2361-03-21T21:14:48+02:00", + "issuanceDate": "2023-12-21T16:35:46.737547+02:00", + "credentialSubject": { + "birthday": 19960424, + "documentType": 2, + "id": "did:polygonid:polygon:mumbai:2qH2mPVRN7ZDCnEofjeh8Qd2Uo3YsEhTVhKhjB8xs4", + "type": "KYCAgeCredential" + }, + "credentialStatus": { + "id": "https://rhs-staging.polygonid.me/node?state=f9dd6aa4e1abef52b6c94ab7eb92faf1a283b371d263e25ac835c9c04894741e", + "revocationNonce": 74881362, + "statusIssuer": { + "id": "https://ad40-91-210-251-7.ngrok-free.app/api/v1/identities/did%3Apolygonid%3Apolygon%3Amumbai%3A2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf/claims/revocation/status/74881362", + "revocationNonce": 74881362, + "type": "SparseMerkleTreeProof" + }, + "type": "Iden3ReverseSparseMerkleTreeProof" + }, + "issuer": "did:polygonid:polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf", + "credentialSchema": { + "id": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCAgeCredential-v3.json", + "type": "JsonSchema2023" + }, + "proof": [ + { + "type": "BJJSignature2021", + "issuerData": { + "id": "did:polygonid:polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf", + "state": { + "claimsTreeRoot": "d946e9cb604bceb0721e4548c291b013647eb56a2cd755b965e6c3b840026517", + "value": "f9dd6aa4e1abef52b6c94ab7eb92faf1a283b371d263e25ac835c9c04894741e" + }, + "authCoreClaim": "cca3371a6cb1b715004407e325bd993c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d7d1691a4202c0a1e580da2a87118c26a399849c42e52c4d97506a5bf5985923e6ec8ef6caeb482daa0d7516a864ace8fba2854275781583934349b51ba70c190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "mtp": { + "existence": true, + "siblings": [] + }, + "credentialStatus": { + "id": "https://rhs-staging.polygonid.me/node?state=f9dd6aa4e1abef52b6c94ab7eb92faf1a283b371d263e25ac835c9c04894741e", + "revocationNonce": 0, + "statusIssuer": { + "id": "https://ad40-91-210-251-7.ngrok-free.app/api/v1/identities/did%3Apolygonid%3Apolygon%3Amumbai%3A2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf/claims/revocation/status/0", + "revocationNonce": 0, + "type": "SparseMerkleTreeProof" + }, + "type": "Iden3ReverseSparseMerkleTreeProof" + } + }, + "coreClaim": "c9b2370371b7fa8b3dab2a5ba81b68382a000000000000000000000000000000021264874acc807e8862077487500a0e9b550a84d667348fc936a4dd0e730b00d4bfb0b3fc0b67c4437ee22848e5de1a7a71748c428358625a5fbac1cebf982000000000000000000000000000000000000000000000000000000000000000005299760400000000281cdcdf0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "signature": "1783ff1c8207d3047a2ba6baa341dc8a6cb095e5683c6fb619ba4099d3332d2b209dca0a0676e41d4675154ea07662c7d9e14a7ee57259f85f3596493ac71a01" + } + ] +}` + var vc W3CCredential + err := json.Unmarshal([]byte(in), &vc) + require.NoError(t, err) + + resolverURL := "http://my-universal-resolver/1.0/identifiers" + + defer tst.MockHTTPClient(t, + map[string]string{ + "http://my-universal-resolver/1.0/identifiers/did%3Apolygonid%3Apolygon%3Amumbai%3A2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf?state=f9dd6aa4e1abef52b6c94ab7eb92faf1a283b371d263e25ac835c9c04894741e": `./testdata/verifycred//my-universal-resolver-1.json`, + })() + resolverRegisty := CredentialStatusResolverRegistry{} + rhsResolver := test1Resolver{} + resolverRegisty.Register(Iden3ReverseSparseMerkleTreeProof, rhsResolver) + verifyConfig := []W3CProofVerificationOpt{WithStatusResolverRegistry(&resolverRegisty)} + err = vc.VerifyProof(context.Background(), BJJSignatureProofType, + HTTPDIDResolver{resolverURL: resolverURL}, verifyConfig...) + require.NoError(t, err) +} + +type test2Resolver struct{} + +func (test2Resolver) Resolve(context context.Context, status CredentialStatus) (out RevocationStatus, err error) { + statusJSON := `{"issuer":{"state":"da6184809dbad90ccc52bb4dbfe2e8ff3f516d87c74d75bcc68a67101760b817","rootOfRoots":"0000000000000000000000000000000000000000000000000000000000000000","claimsTreeRoot":"aec50251fdc67959254c74ab4f2e746a7cd1c6f494c8ac028d655dfbccea430e","revocationTreeRoot":"0000000000000000000000000000000000000000000000000000000000000000"},"mtp":{"existence":false,"siblings":[]}}` + var rs RevocationStatus + _ = json.Unmarshal([]byte(statusJSON), &rs) + return rs, nil +} +func TestW3CCredential_ValidateBJJSignatureProofGenesis(t *testing.T) { + in := `{ + "id": "urn:uuid:b7a1e232-a0d3-11ee-bc8a-a27b3ddbdc29", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.iden3.io/core/jsonld/iden3proofs.jsonld", + "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld" + ], + "type": [ + "VerifiableCredential", + "KYCAgeCredential" + ], + "expirationDate": "2361-03-21T21:14:48+02:00", + "issuanceDate": "2023-12-22T16:09:27.444712+02:00", + "credentialSubject": { + "birthday": 19960424, + "documentType": 2, + "id": "did:polygonid:polygon:mumbai:2qJm6vBXtHWMqm9A9f5zihRNVGptHAHcK8oVxGUTg8", + "type": "KYCAgeCredential" + }, + "credentialStatus": { + "id": "https://rhs-staging.polygonid.me/node?state=da6184809dbad90ccc52bb4dbfe2e8ff3f516d87c74d75bcc68a67101760b817", + "revocationNonce": 1102174849, + "statusIssuer": { + "id": "https://ad40-91-210-251-7.ngrok-free.app/api/v1/identities/did%3Apolygonid%3Apolygon%3Amumbai%3A2qLx3hTJBV8REpNDK2RiG7eNBVzXMoZdPfi2uhF7Ks/claims/revocation/status/1102174849", + "revocationNonce": 1102174849, + "type": "SparseMerkleTreeProof" + }, + "type": "Iden3ReverseSparseMerkleTreeProof" + }, + "issuer": "did:polygonid:polygon:mumbai:2qLx3hTJBV8REpNDK2RiG7eNBVzXMoZdPfi2uhF7Ks", + "credentialSchema": { + "id": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCAgeCredential-v3.json", + "type": "JsonSchema2023" + }, + "proof": [ + { + "type": "BJJSignature2021", + "issuerData": { + "id": "did:polygonid:polygon:mumbai:2qLx3hTJBV8REpNDK2RiG7eNBVzXMoZdPfi2uhF7Ks", + "state": { + "claimsTreeRoot": "aec50251fdc67959254c74ab4f2e746a7cd1c6f494c8ac028d655dfbccea430e", + "value": "da6184809dbad90ccc52bb4dbfe2e8ff3f516d87c74d75bcc68a67101760b817" + }, + "authCoreClaim": "cca3371a6cb1b715004407e325bd993c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c08ac5cc7c5aa3e8190e188cf8d1737c92d16188541b582ef676c55b3a842c06c4985e9d4771ee6d033c2021a3d177f7dfa51859d99a9a476c2a910e887dc8240000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "mtp": { + "existence": true, + "siblings": [] + }, + "credentialStatus": { + "id": "https://rhs-staging.polygonid.me/node?state=da6184809dbad90ccc52bb4dbfe2e8ff3f516d87c74d75bcc68a67101760b817", + "revocationNonce": 0, + "statusIssuer": { + "id": "https://ad40-91-210-251-7.ngrok-free.app/api/v1/identities/did%3Apolygonid%3Apolygon%3Amumbai%3A2qLx3hTJBV8REpNDK2RiG7eNBVzXMoZdPfi2uhF7Ks/claims/revocation/status/0", + "revocationNonce": 0, + "type": "SparseMerkleTreeProof" + }, + "type": "Iden3ReverseSparseMerkleTreeProof" + } + }, + "coreClaim": "c9b2370371b7fa8b3dab2a5ba81b68382a00000000000000000000000000000002128aa2ae20d4f8f7b9d673e06498fa410f3c5a790194f3b9284a2018f30d0037d1e542f1b72c9d5ca4b46d93710fbfa23a7c9c36eb3ca0eb0f9548ad9c140c000000000000000000000000000000000000000000000000000000000000000081dab14100000000281cdcdf0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "signature": "2a2e4d79f3aa440154643252d1b9074f9651fffcd653fb2fcadc07f55cd1f9a20a812dd7df8ba8775653984cfb7120f999751f9c25473fd634c7f2d88419c102" + } + ] +}` + var vc W3CCredential + err := json.Unmarshal([]byte(in), &vc) + require.NoError(t, err) + + resolverURL := "http://my-universal-resolver/1.0/identifiers" + + defer tst.MockHTTPClient(t, + map[string]string{ + "http://my-universal-resolver/1.0/identifiers/did%3Apolygonid%3Apolygon%3Amumbai%3A2qLx3hTJBV8REpNDK2RiG7eNBVzXMoZdPfi2uhF7Ks?state=da6184809dbad90ccc52bb4dbfe2e8ff3f516d87c74d75bcc68a67101760b817": `./testdata/verifycred//my-universal-resolver-2.json`, + })() + + resolverRegisty := CredentialStatusResolverRegistry{} + rhsResolver := test2Resolver{} + resolverRegisty.Register(Iden3ReverseSparseMerkleTreeProof, rhsResolver) + verifyConfig := []W3CProofVerificationOpt{WithStatusResolverRegistry(&resolverRegisty)} + + err = vc.VerifyProof(context.Background(), BJJSignatureProofType, + HTTPDIDResolver{resolverURL: resolverURL}, verifyConfig...) + require.NoError(t, err) +} + +func TestW3CCredential_ValidateIden3SparseMerkleTreeProof(t *testing.T) { + in := `{ + "id": "urn:uuid:3a8d1822-a00e-11ee-8f57-a27b3ddbdc29", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.iden3.io/core/jsonld/iden3proofs.jsonld", + "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld" + ], + "type": [ + "VerifiableCredential", + "KYCAgeCredential" + ], + "expirationDate": "2361-03-21T21:14:48+02:00", + "issuanceDate": "2023-12-21T16:35:46.737547+02:00", + "credentialSubject": { + "birthday": 19960424, + "documentType": 2, + "id": "did:polygonid:polygon:mumbai:2qH2mPVRN7ZDCnEofjeh8Qd2Uo3YsEhTVhKhjB8xs4", + "type": "KYCAgeCredential" + }, + "credentialStatus": { + "id": "https://rhs-staging.polygonid.me/node?state=f9dd6aa4e1abef52b6c94ab7eb92faf1a283b371d263e25ac835c9c04894741e", + "revocationNonce": 74881362, + "statusIssuer": { + "id": "https://ad40-91-210-251-7.ngrok-free.app/api/v1/identities/did%3Apolygonid%3Apolygon%3Amumbai%3A2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf/claims/revocation/status/74881362", + "revocationNonce": 74881362, + "type": "SparseMerkleTreeProof" + }, + "type": "Iden3ReverseSparseMerkleTreeProof" + }, + "issuer": "did:polygonid:polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf", + "credentialSchema": { + "id": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCAgeCredential-v3.json", + "type": "JsonSchema2023" + }, + "proof": [ + { + "type": "BJJSignature2021", + "issuerData": { + "id": "did:polygonid:polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf", + "state": { + "claimsTreeRoot": "d946e9cb604bceb0721e4548c291b013647eb56a2cd755b965e6c3b840026517", + "value": "f9dd6aa4e1abef52b6c94ab7eb92faf1a283b371d263e25ac835c9c04894741e" + }, + "authCoreClaim": "cca3371a6cb1b715004407e325bd993c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d7d1691a4202c0a1e580da2a87118c26a399849c42e52c4d97506a5bf5985923e6ec8ef6caeb482daa0d7516a864ace8fba2854275781583934349b51ba70c190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "mtp": { + "existence": true, + "siblings": [] + }, + "credentialStatus": { + "id": "https://rhs-staging.polygonid.me/node?state=f9dd6aa4e1abef52b6c94ab7eb92faf1a283b371d263e25ac835c9c04894741e", + "revocationNonce": 0, + "statusIssuer": { + "id": "https://ad40-91-210-251-7.ngrok-free.app/api/v1/identities/did%3Apolygonid%3Apolygon%3Amumbai%3A2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf/claims/revocation/status/0", + "revocationNonce": 0, + "type": "SparseMerkleTreeProof" + }, + "type": "Iden3ReverseSparseMerkleTreeProof" + } + }, + "coreClaim": "c9b2370371b7fa8b3dab2a5ba81b68382a000000000000000000000000000000021264874acc807e8862077487500a0e9b550a84d667348fc936a4dd0e730b00d4bfb0b3fc0b67c4437ee22848e5de1a7a71748c428358625a5fbac1cebf982000000000000000000000000000000000000000000000000000000000000000005299760400000000281cdcdf0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "signature": "1783ff1c8207d3047a2ba6baa341dc8a6cb095e5683c6fb619ba4099d3332d2b209dca0a0676e41d4675154ea07662c7d9e14a7ee57259f85f3596493ac71a01" + }, + { + "type": "Iden3SparseMerkleTreeProof", + "issuerData": { + "id": "did:polygonid:polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf", + "state": { + "txId": "0x7ab71a8c5e91064e21beb586012f8b89932c255e243c496dec895a501a42e243", + "blockTimestamp": 1703174663, + "blockNumber": 43840767, + "rootOfRoots": "37eabc712cdaa64793561b16b8143f56f149ad1b0c35297a1b125c765d1c071e", + "claimsTreeRoot": "4436ea12d352ddb84d2ac7a27bbf7c9f1bfc7d3ff69f3e6cf4348f424317fd0b", + "revocationTreeRoot": "0000000000000000000000000000000000000000000000000000000000000000", + "value": "34824a8e1defc326f935044e32e9f513377dbfc031d79475a0190830554d4409" + } + }, + "coreClaim": "c9b2370371b7fa8b3dab2a5ba81b68382a000000000000000000000000000000021264874acc807e8862077487500a0e9b550a84d667348fc936a4dd0e730b00d4bfb0b3fc0b67c4437ee22848e5de1a7a71748c428358625a5fbac1cebf982000000000000000000000000000000000000000000000000000000000000000005299760400000000281cdcdf0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "mtp": { + "existence": true, + "siblings": [ + "0", + "10581662619345074277108685138429405012286849178024033034405862946888154171097" + ] + } + } + ] +}` + var vc W3CCredential + err := json.Unmarshal([]byte(in), &vc) + require.NoError(t, err) + + resolverURL := "http://my-universal-resolver/1.0/identifiers" + + defer tst.MockHTTPClient(t, + map[string]string{ + "http://my-universal-resolver/1.0/identifiers/did%3Apolygonid%3Apolygon%3Amumbai%3A2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf?state=34824a8e1defc326f935044e32e9f513377dbfc031d79475a0190830554d4409": `./testdata/verifycred//my-universal-resolver-3.json`, + })() + + err = vc.VerifyProof(context.Background(), Iden3SparseMerkleTreeProofType, + HTTPDIDResolver{resolverURL: resolverURL}) + require.NoError(t, err) +} + +type test3Resolver struct{} + +func (test3Resolver) Resolve(context context.Context, status CredentialStatus) (out RevocationStatus, err error) { + statusJSON := `{"issuer":{"state":"96161f3fbbdd68c72bc430dae474e27b157586b33b9fbf4a3f07d75ce275570f","rootOfRoots":"eaa48e4a7d3fe2fabbd939c7df1048c3f647a9a7c9dfadaae836ec78ba673229","claimsTreeRoot":"d9597e2fef206c9821f2425e513a68c8c793bc93c9216fb883fedaaf72abf51c","revocationTreeRoot":"0000000000000000000000000000000000000000000000000000000000000000"},"mtp":{"existence":false,"siblings":[]}}` + var rs RevocationStatus + _ = json.Unmarshal([]byte(statusJSON), &rs) + return rs, nil +} + +func TestW3CCredential_ValidateBJJSignatureProofAgentStatus(t *testing.T) { + in := `{ + "id": "urn:uuid:79d93584-ae2c-11ee-8050-a27b3ddbdc28", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.iden3.io/core/jsonld/iden3proofs.jsonld", + "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld" + ], + "type": [ + "VerifiableCredential", + "KYCAgeCredential" + ], + "expirationDate": "2361-03-21T21:14:48+02:00", + "issuanceDate": "2024-01-08T15:47:34.113565+02:00", + "credentialSubject": { + "birthday": 19960424, + "documentType": 2, + "id": "did:polygonid:polygon:mumbai:2qFDziX3k3h7To2jDJbQiXFtcozbgSNNvQpb6TgtPE", + "type": "KYCAgeCredential" + }, + "credentialStatus": { + "id": "http://localhost:8001/api/v1/agent", + "revocationNonce": 3262660310, + "type": "Iden3commRevocationStatusV1.0" + }, + "issuer": "did:polygonid:polygon:mumbai:2qJp131YoXVu8iLNGfL3TkQAWEr3pqimh2iaPgH3BJ", + "credentialSchema": { + "id": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCAgeCredential-v3.json", + "type": "JsonSchema2023" + }, + "proof": [ + { + "type": "BJJSignature2021", + "issuerData": { + "id": "did:polygonid:polygon:mumbai:2qJp131YoXVu8iLNGfL3TkQAWEr3pqimh2iaPgH3BJ", + "state": { + "claimsTreeRoot": "b35562873d9870f20e3d44dd94502f4156785a4b09d7906914758a7e0ed26829", + "value": "2de39210318bbc7fc79e24150c2790089c8385d7acffc0f0ebf1641b95087e0f" + }, + "authCoreClaim": "cca3371a6cb1b715004407e325bd993c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000167c1d2857ca6579d6e995198876cdfd4edb4fe2eeedeadbabaaed3008225205e7b8ab88a60b9ef0999be82625e0831872d8aca16b2932852c3731e9df69970a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "mtp": { + "existence": true, + "siblings": [] + }, + "credentialStatus": { + "id": "http://localhost:8001/api/v1/agent", + "revocationNonce": 0, + "type": "Iden3commRevocationStatusV1.0" + } + }, + "coreClaim": "c9b2370371b7fa8b3dab2a5ba81b68382a00000000000000000000000000000002123cbcd9d0f3a493561510c72b47afcb02e2f09b3855291c6b77d224260d0014f503c3ab03eebe757d5b50b570186a69d90c49904155f5fc71e0e7f5b8aa120000000000000000000000000000000000000000000000000000000000000000d63e78c200000000281cdcdf0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "signature": "56ab45ad828c4860d02e111b2732c969005046ee26dbc7d1e5bd6a6c6604ed81c3f55ffb9349f4d407f59e2e210f6d256a328d30edae2c7c95dd057240ee8902" + }, + { + "type": "Iden3SparseMerkleTreeProof", + "issuerData": { + "id": "did:polygonid:polygon:mumbai:2qJp131YoXVu8iLNGfL3TkQAWEr3pqimh2iaPgH3BJ", + "state": { + "txId": "0x02f1af6a616715ccb7511176ca53d39a28c55201effca0b43a343ee6e9dc8c97", + "blockTimestamp": 1704721690, + "blockNumber": 44542683, + "rootOfRoots": "eaa48e4a7d3fe2fabbd939c7df1048c3f647a9a7c9dfadaae836ec78ba673229", + "claimsTreeRoot": "d9597e2fef206c9821f2425e513a68c8c793bc93c9216fb883fedaaf72abf51c", + "revocationTreeRoot": "0000000000000000000000000000000000000000000000000000000000000000", + "value": "96161f3fbbdd68c72bc430dae474e27b157586b33b9fbf4a3f07d75ce275570f" + } + }, + "coreClaim": "c9b2370371b7fa8b3dab2a5ba81b68382a00000000000000000000000000000002123cbcd9d0f3a493561510c72b47afcb02e2f09b3855291c6b77d224260d0014f503c3ab03eebe757d5b50b570186a69d90c49904155f5fc71e0e7f5b8aa120000000000000000000000000000000000000000000000000000000000000000d63e78c200000000281cdcdf0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "mtp": { + "existence": true, + "siblings": [ + "18730028644149260049434737497088408840959357817865392043806470281178241979827" + ] + } + } + ] + }` + var vc W3CCredential + err := json.Unmarshal([]byte(in), &vc) + require.NoError(t, err) + + resolverURL := "http://my-universal-resolver/1.0/identifiers" + + defer tst.MockHTTPClient(t, + map[string]string{ + "http://my-universal-resolver/1.0/identifiers/did%3Apolygonid%3Apolygon%3Amumbai%3A2qJp131YoXVu8iLNGfL3TkQAWEr3pqimh2iaPgH3BJ?state=2de39210318bbc7fc79e24150c2790089c8385d7acffc0f0ebf1641b95087e0f": `./testdata/verifycred//my-universal-resolver-4.json`, + })() + + resolverRegisty := CredentialStatusResolverRegistry{} + resolverRegisty.Register(Iden3commRevocationStatusV1, test3Resolver{}) + verifyConfig := []W3CProofVerificationOpt{WithStatusResolverRegistry(&resolverRegisty)} + err = vc.VerifyProof(context.Background(), BJJSignatureProofType, HTTPDIDResolver{resolverURL: resolverURL}, + verifyConfig...) + require.NoError(t, err) +} + +func TestW3CCredential_ValidateBJJSignatureProofIssuerStatus(t *testing.T) { + in := `{ + "id": "urn:uuid:c784e54c-b14e-11ee-94df-a27b3ddbdc28", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.iden3.io/core/jsonld/iden3proofs.jsonld", + "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld" + ], + "type": [ + "VerifiableCredential", + "KYCAgeCredential" + ], + "expirationDate": "2361-03-21T21:14:48+02:00", + "issuanceDate": "2024-01-12T15:30:40.800436+02:00", + "credentialSubject": { + "birthday": 19960424, + "documentType": 2, + "id": "did:polygonid:polygon:mumbai:2qDwkysfn58urGGatGYsHKqzYPsy5p3mc9yxZZTeqh", + "type": "KYCAgeCredential" + }, + "credentialStatus": { + "id": "http://localhost:8001/api/v1/identities/did%3Apolygonid%3Apolygon%3Amumbai%3A2qNuE5Jxmvrx6EithQ5bMs4DcWN91SjxepUzdQtddn/claims/revocation/status/1737529009", + "revocationNonce": 1737529009, + "type": "SparseMerkleTreeProof" + }, + "issuer": "did:polygonid:polygon:mumbai:2qNuE5Jxmvrx6EithQ5bMs4DcWN91SjxepUzdQtddn", + "credentialSchema": { + "id": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCAgeCredential-v3.json", + "type": "JsonSchema2023" + }, + "proof": [ + { + "type": "BJJSignature2021", + "issuerData": { + "id": "did:polygonid:polygon:mumbai:2qNuE5Jxmvrx6EithQ5bMs4DcWN91SjxepUzdQtddn", + "state": { + "claimsTreeRoot": "9af7b27d7176f465dc9acfd7dc937bae5df1d1cd34d682692f1ea6bf7cedf514", + "value": "95e4f8437be5d50a569bb532713110e4f5d2ac97765fae54041dddae9638a119" + }, + "authCoreClaim": "cca3371a6cb1b715004407e325bd993c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d95ae65475a9b380ca6118927f741c06466e951c25bb7b03a1505d597fc078222fe8db4747e2bf9c847308b283a5c17eeba4e50ced3283d24cce665b35f701050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "mtp": { + "existence": true, + "siblings": [] + }, + "credentialStatus": { + "id": "http://localhost:8001/api/v1/identities/did%3Apolygonid%3Apolygon%3Amumbai%3A2qNuE5Jxmvrx6EithQ5bMs4DcWN91SjxepUzdQtddn/claims/revocation/status/0", + "revocationNonce": 0, + "type": "SparseMerkleTreeProof" + } + }, + "coreClaim": "c9b2370371b7fa8b3dab2a5ba81b68382a0000000000000000000000000000000212208b10849a2f9bbacd2a583d4177ec460ac4f599d8355cfc39d820d90c00c7f1c984807cf958a96b0850ee8e9f495902a87c3a8f11a2fbcabe10fdea702c0000000000000000000000000000000000000000000000000000000000000000b196906700000000281cdcdf0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "signature": "16a3e5cf7638bf843dbff803aeafa9c8735dde795cc9b8638c6b1963f290f890cad183481dee4f6376ed3496296f30170d1558f929486ec8ada00aa1d1104005" + } + ] + }` + var vc W3CCredential + err := json.Unmarshal([]byte(in), &vc) + require.NoError(t, err) + + resolverURL := "http://my-universal-resolver/1.0/identifiers" + + defer tst.MockHTTPClient(t, + map[string]string{ + "http://my-universal-resolver/1.0/identifiers/did%3Apolygonid%3Apolygon%3Amumbai%3A2qNuE5Jxmvrx6EithQ5bMs4DcWN91SjxepUzdQtddn?state=95e4f8437be5d50a569bb532713110e4f5d2ac97765fae54041dddae9638a119": `./testdata/verifycred/my-universal-resolver-5.json`, + "http://localhost:8001/api/v1/identities/did%3Apolygonid%3Apolygon%3Amumbai%3A2qNuE5Jxmvrx6EithQ5bMs4DcWN91SjxepUzdQtddn/claims/revocation/status/0": `./testdata/verifycred/issuer-state-response.json`, + })() + + resolverRegisty := CredentialStatusResolverRegistry{} + resolverRegisty.Register(SparseMerkleTreeProof, IssuerResolver{}) + verifyConfig := []W3CProofVerificationOpt{WithStatusResolverRegistry(&resolverRegisty)} + err = vc.VerifyProof(context.Background(), BJJSignatureProofType, + HTTPDIDResolver{resolverURL: resolverURL}, verifyConfig...) + require.NoError(t, err) +} + func TestW3CCredential_JSONUnmarshal(t *testing.T) { in := `{ "id": "http://ec2-34-247-165-109.eu-west-1.compute.amazonaws.com:8888/api/v1/claim/52cec4e3-7d1d-11ed-ade2-0242ac180007", diff --git a/verifiable/did_resolver.go b/verifiable/did_resolver.go new file mode 100644 index 0000000..6028c7a --- /dev/null +++ b/verifiable/did_resolver.go @@ -0,0 +1,67 @@ +package verifiable + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/iden3/go-iden3-core/v2/w3c" + "github.com/pkg/errors" +) + +type DIDResolver interface { + Resolve(ctx context.Context, did *w3c.DID) (DIDDocument, error) +} + +type HTTPDIDResolver struct { + resolverURL string + customHTTPClient *http.Client +} + +func (r HTTPDIDResolver) Resolve(ctx context.Context, did *w3c.DID) (out DIDDocument, err error) { + type didResolutionResult struct { + DIDDocument DIDDocument `json:"didDocument"` + } + res := &didResolutionResult{} + + var ( + resp *http.Response + httpClient *http.Client + ) + + httpClient = http.DefaultClient + if r.customHTTPClient != nil { + httpClient = r.customHTTPClient + } + didStr := did.String() + didParts := strings.Split(didStr, "?") + if len(didParts) == 2 { + didEscaped := url.QueryEscape(didParts[0]) + didStr = fmt.Sprintf("%s?%s", didEscaped, didParts[1]) + } + if err != nil { + return out, err + } + resp, err = httpClient.Get(fmt.Sprintf("%s/%s", strings.Trim(r.resolverURL, "/"), didStr)) + + if err != nil { + return out, err + } + + defer func() { + err2 := resp.Body.Close() + if err == nil { + err = errors.WithStack(err2) + } + }() + + err = json.NewDecoder(resp.Body).Decode(&res) + if err != nil { + return out, err + } + + return res.DIDDocument, nil +} diff --git a/verifiable/proof.go b/verifiable/proof.go index 626ce47..212143f 100644 --- a/verifiable/proof.go +++ b/verifiable/proof.go @@ -22,6 +22,12 @@ type IssuerData struct { CredentialStatus interface{} `json:"credentialStatus,omitempty"` } +func (id *IssuerData) authClaim() (*core.Claim, error) { + var claim core.Claim + err := claim.FromHex(id.AuthCoreClaim) + return &claim, err +} + // State represents the state of the issuer type State struct { TxID *string `json:"txId,omitempty"` diff --git a/verifiable/resolver.go b/verifiable/resolver.go new file mode 100644 index 0000000..f94833d --- /dev/null +++ b/verifiable/resolver.go @@ -0,0 +1,76 @@ +package verifiable + +import ( + "context" + "fmt" + + "github.com/iden3/go-iden3-core/v2/w3c" +) + +type ctxKeyIssuerDID struct{} + +// WithIssuerDID puts the issuer DID in the context +func WithIssuerDID(ctx context.Context, issuerDID *w3c.DID) context.Context { + return context.WithValue(ctx, ctxKeyIssuerDID{}, issuerDID) +} + +// GetIssuerDID extract the issuer DID from the context. +// Or nil if nothing is found. +func GetIssuerDID(ctx context.Context) *w3c.DID { + v := ctx.Value(ctxKeyIssuerDID{}) + if v == nil { + return nil + } + return v.(*w3c.DID) +} + +// CredentialStatusResolver is an interface that allows to interact with deifferent types of credential status to resolve revocation status +type CredentialStatusResolver interface { + Resolve(ctx context.Context, + credentialStatus CredentialStatus) (RevocationStatus, error) +} + +// CredentialStatusResolverRegistry is a registry of CredentialStatusResolver +type CredentialStatusResolverRegistry struct { + resolvers map[CredentialStatusType]CredentialStatusResolver +} + +func (r *CredentialStatusResolverRegistry) Register(resolverType CredentialStatusType, resolver CredentialStatusResolver) { + if r.resolvers == nil { + r.resolvers = make(map[CredentialStatusType]CredentialStatusResolver) + } + r.resolvers[resolverType] = resolver +} + +func (r *CredentialStatusResolverRegistry) Get(resolverType CredentialStatusType) (CredentialStatusResolver, error) { + resolver, ok := r.resolvers[resolverType] + if !ok { + return nil, fmt.Errorf("credential status type %s id not registered", resolverType) + } + return resolver, nil +} + +func (r *CredentialStatusResolverRegistry) Delete(resolverType CredentialStatusType) { + if r.resolvers == nil { + return + } + delete(r.resolvers, resolverType) +} + +var DefaultCredentialStatusResolverRegistry = &CredentialStatusResolverRegistry{} + +func RegisterStatusResolver(resolverType CredentialStatusType, + resolver CredentialStatusResolver) { + + DefaultCredentialStatusResolverRegistry.Register(resolverType, resolver) +} + +func GetStatusResolver( + resolverType CredentialStatusType) (CredentialStatusResolver, error) { + + return DefaultCredentialStatusResolverRegistry.Get(resolverType) +} + +func DeleteStatusResolver(resolverType CredentialStatusType) { + DefaultCredentialStatusResolverRegistry.Delete(resolverType) +} diff --git a/verifiable/status_direct.go b/verifiable/status_direct.go new file mode 100644 index 0000000..c706f82 --- /dev/null +++ b/verifiable/status_direct.go @@ -0,0 +1,59 @@ +package verifiable + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" +) + +type IssuerResolver struct { +} + +const limitReaderBytes = 16 * 1024 + +func (IssuerResolver) Resolve(ctx context.Context, + credentialStatus CredentialStatus) (out RevocationStatus, err error) { + + httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, + credentialStatus.ID, http.NoBody) + if err != nil { + return out, err + } + httpResp, err := http.DefaultClient.Do(httpReq) + if err != nil { + return out, err + } + defer func() { + err2 := httpResp.Body.Close() + if err2 != nil && err == nil { + err = err2 + } + }() + + statusOK := httpResp.StatusCode >= 200 && httpResp.StatusCode < 300 + if !statusOK { + return out, fmt.Errorf("unexpected status code: %d", + httpResp.StatusCode) + } + + limitReader := &io.LimitedReader{R: httpResp.Body, N: limitReaderBytes} + + respData, err := io.ReadAll(limitReader) + if err != nil { + return out, err + } + + // Check if the body size exceeds the limit + if limitReader.N <= 0 { + return out, fmt.Errorf("response body size exceeds the limit of %d", + limitReaderBytes) + } + + err = json.Unmarshal(respData, &out) + if err != nil { + return out, err + } + return out, nil +} diff --git a/verifiable/testdata/verifycred/issuer-state-response.json b/verifiable/testdata/verifycred/issuer-state-response.json new file mode 100644 index 0000000..8b9afff --- /dev/null +++ b/verifiable/testdata/verifycred/issuer-state-response.json @@ -0,0 +1 @@ +{"issuer":{"state":"95e4f8437be5d50a569bb532713110e4f5d2ac97765fae54041dddae9638a119","claimsTreeRoot":"9af7b27d7176f465dc9acfd7dc937bae5df1d1cd34d682692f1ea6bf7cedf514"},"mtp":{"existence":false,"siblings":[]}} \ No newline at end of file diff --git a/verifiable/testdata/verifycred/my-universal-resolver-1.json b/verifiable/testdata/verifycred/my-universal-resolver-1.json new file mode 100644 index 0000000..f106742 --- /dev/null +++ b/verifiable/testdata/verifycred/my-universal-resolver-1.json @@ -0,0 +1 @@ +{"@context":"https://w3id.org/did-resolution/v1","didDocument":{"@context":["https://www.w3.org/ns/did/v1","https://schema.iden3.io/core/jsonld/auth.jsonld"],"id":"did:polygonid:polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf","verificationMethod":[{"id":"did:polygonid:polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf#stateInfo","type":"Iden3StateInfo2023","controller":"did:polygonid:polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf","stateContractAddress":"80001:0x134B1BE34911E39A8397ec6289782989729807a4","published":true,"info":{"id":"did:polygonid:polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf","state":"34824a8e1defc326f935044e32e9f513377dbfc031d79475a0190830554d4409","replacedByState":"0000000000000000000000000000000000000000000000000000000000000000","createdAtTimestamp":"1703174663","replacedAtTimestamp":"0","createdAtBlock":"43840767","replacedAtBlock":"0"},"global":{"root":"92c4610a24247a4013ce6de4903452d164134a232a94fd1fe37178bce4937006","replacedByRoot":"0000000000000000000000000000000000000000000000000000000000000000","createdAtTimestamp":"1704439557","replacedAtTimestamp":"0","createdAtBlock":"44415346","replacedAtBlock":"0"}}]},"didResolutionMetadata":{"contentType":"application/did+ld+json","retrieved":"2024-01-05T08:05:13.413770024Z","pattern":"^(did:polygonid:.+)$","driverUrl":"http://driver-did-polygonid:8080/1.0/identifiers/","duration":429,"did":{"didString":"did:polygonid:polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf","methodSpecificId":"polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf","method":"polygonid"}},"didDocumentMetadata":{}} \ No newline at end of file diff --git a/verifiable/testdata/verifycred/my-universal-resolver-2.json b/verifiable/testdata/verifycred/my-universal-resolver-2.json new file mode 100644 index 0000000..b2bae2c --- /dev/null +++ b/verifiable/testdata/verifycred/my-universal-resolver-2.json @@ -0,0 +1 @@ +{"@context":"https://w3id.org/did-resolution/v1","didDocument":{"@context":["https://www.w3.org/ns/did/v1","https://schema.iden3.io/core/jsonld/auth.jsonld"],"id":"did:polygonid:polygon:mumbai:2qLx3hTJBV8REpNDK2RiG7eNBVzXMoZdPfi2uhF7Ks","verificationMethod":[{"id":"did:polygonid:polygon:mumbai:2qLx3hTJBV8REpNDK2RiG7eNBVzXMoZdPfi2uhF7Ks#stateInfo","type":"Iden3StateInfo2023","controller":"did:polygonid:polygon:mumbai:2qLx3hTJBV8REpNDK2RiG7eNBVzXMoZdPfi2uhF7Ks","stateContractAddress":"80001:0x134B1BE34911E39A8397ec6289782989729807a4","published":false,"global":{"root":"92c4610a24247a4013ce6de4903452d164134a232a94fd1fe37178bce4937006","replacedByRoot":"0000000000000000000000000000000000000000000000000000000000000000","createdAtTimestamp":"1704439557","replacedAtTimestamp":"0","createdAtBlock":"44415346","replacedAtBlock":"0"}}]},"didResolutionMetadata":{"contentType":"application/did+ld+json","retrieved":"2024-01-05T08:02:25.986085836Z","pattern":"^(did:polygonid:.+)$","driverUrl":"http://driver-did-polygonid:8080/1.0/identifiers/","duration":434,"did":{"didString":"did:polygonid:polygon:mumbai:2qLx3hTJBV8REpNDK2RiG7eNBVzXMoZdPfi2uhF7Ks","methodSpecificId":"polygon:mumbai:2qLx3hTJBV8REpNDK2RiG7eNBVzXMoZdPfi2uhF7Ks","method":"polygonid"}},"didDocumentMetadata":{}} \ No newline at end of file diff --git a/verifiable/testdata/verifycred/my-universal-resolver-3.json b/verifiable/testdata/verifycred/my-universal-resolver-3.json new file mode 100644 index 0000000..f9e7933 --- /dev/null +++ b/verifiable/testdata/verifycred/my-universal-resolver-3.json @@ -0,0 +1 @@ +{"@context":"https://w3id.org/did-resolution/v1","didDocument":{"@context":["https://www.w3.org/ns/did/v1","https://schema.iden3.io/core/jsonld/auth.jsonld"],"id":"did:polygonid:polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf","verificationMethod":[{"id":"did:polygonid:polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf#stateInfo","type":"Iden3StateInfo2023","controller":"did:polygonid:polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf","stateContractAddress":"80001:0x134B1BE34911E39A8397ec6289782989729807a4","published":true,"info":{"id":"did:polygonid:polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf","state":"34824a8e1defc326f935044e32e9f513377dbfc031d79475a0190830554d4409","replacedByState":"0000000000000000000000000000000000000000000000000000000000000000","createdAtTimestamp":"1703174663","replacedAtTimestamp":"0","createdAtBlock":"43840767","replacedAtBlock":"0"},"global":{"root":"92c4610a24247a4013ce6de4903452d164134a232a94fd1fe37178bce4937006","replacedByRoot":"0000000000000000000000000000000000000000000000000000000000000000","createdAtTimestamp":"1704439557","replacedAtTimestamp":"0","createdAtBlock":"44415346","replacedAtBlock":"0"}}]},"didResolutionMetadata":{"contentType":"application/did+ld+json","retrieved":"2024-01-05T07:53:42.67771172Z","pattern":"^(did:polygonid:.+)$","driverUrl":"http://driver-did-polygonid:8080/1.0/identifiers/","duration":442,"did":{"didString":"did:polygonid:polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf","methodSpecificId":"polygon:mumbai:2qLGnFZiHrhdNh5KwdkGvbCN1sR2pUaBpBahAXC3zf","method":"polygonid"}},"didDocumentMetadata":{}} \ No newline at end of file diff --git a/verifiable/testdata/verifycred/my-universal-resolver-4.json b/verifiable/testdata/verifycred/my-universal-resolver-4.json new file mode 100644 index 0000000..5012f68 --- /dev/null +++ b/verifiable/testdata/verifycred/my-universal-resolver-4.json @@ -0,0 +1 @@ +{"didDocument":{"@context":["https://www.w3.org/ns/did/v1","https://schema.iden3.io/core/jsonld/auth.jsonld"],"id":"did:polygonid:polygon:mumbai:2qEChbFATnamWnToMgNycnVi4W9Xw5772qX61qwki6","verificationMethod":[{"id":"did:polygonid:polygon:mumbai:2qEChbFATnamWnToMgNycnVi4W9Xw5772qX61qwki6#stateInfo","type":"Iden3StateInfo2023","controller":"did:polygonid:polygon:mumbai:2qEChbFATnamWnToMgNycnVi4W9Xw5772qX61qwki6","stateContractAddress":"80001:0x134B1BE34911E39A8397ec6289782989729807a4","published":false,"global":{"root":"ff3e987dc4c279af0e77ac2b1983ed8cf627bfeebbc6d5d56be2526cc7286621","replacedByRoot":"0000000000000000000000000000000000000000000000000000000000000000","createdAtTimestamp":"1704719148","replacedAtTimestamp":"0","createdAtBlock":"44541667","replacedAtBlock":"0"}}]}} \ No newline at end of file diff --git a/verifiable/testdata/verifycred/my-universal-resolver-5.json b/verifiable/testdata/verifycred/my-universal-resolver-5.json new file mode 100644 index 0000000..8120423 --- /dev/null +++ b/verifiable/testdata/verifycred/my-universal-resolver-5.json @@ -0,0 +1 @@ +{"didDocument":{"@context":["https://www.w3.org/ns/did/v1","https://schema.iden3.io/core/jsonld/auth.jsonld"],"id":"did:polygonid:polygon:mumbai:2qNuE5Jxmvrx6EithQ5bMs4DcWN91SjxepUzdQtddn","verificationMethod":[{"id":"did:polygonid:polygon:mumbai:2qNuE5Jxmvrx6EithQ5bMs4DcWN91SjxepUzdQtddn#stateInfo","type":"Iden3StateInfo2023","controller":"did:polygonid:polygon:mumbai:2qNuE5Jxmvrx6EithQ5bMs4DcWN91SjxepUzdQtddn","stateContractAddress":"80001:0x134B1BE34911E39A8397ec6289782989729807a4","published":false,"global":{"root":"40c30e53dc6649842d8f1297f8b4267e7097b6941c413ac032ce53726f826229","replacedByRoot":"0000000000000000000000000000000000000000000000000000000000000000","createdAtTimestamp":"1705061221","replacedAtTimestamp":"0","createdAtBlock":"44690848","replacedAtBlock":"0"}}]}} \ No newline at end of file