public void CanFormatStringCredentialValues()
        {
            var attributeValues = new List <CredentialPreviewAttribute>
            {
                new CredentialPreviewAttribute("first_name", "Test"),
                new CredentialPreviewAttribute("last_name", "holder")
            };

            var expectedResult = new
            {
                first_name = new
                {
                    raw     = "Test",
                    encoded = CredentialUtils.GetEncoded("Test")
                },
                last_name = new
                {
                    raw     = "holder",
                    encoded = CredentialUtils.GetEncoded("holder")
                }
            }.ToJson();

            var formatedCredentialUtils = CredentialUtils.FormatCredentialValues(attributeValues);

            var expectedResultObj = JObject.Parse(expectedResult);
            var formatedCredObj   = JObject.Parse(formatedCredentialUtils);

            Assert.Equal(expectedResultObj, formatedCredObj);
        }
        private async Task <(IssuerCreateCredentialResult, RevocationRegistryRecord)> IssueCredentialSafeAsync(
            IAgentContext agentContext,
            DefinitionRecord definitionRecord,
            CredentialRecord credentialRecord)
        {
            BlobStorageReader        tailsReader      = null;
            RevocationRegistryRecord revocationRecord = null;

            if (definitionRecord.SupportsRevocation)
            {
                revocationRecord = await RecordService.GetAsync <RevocationRegistryRecord>(agentContext.Wallet, definitionRecord.CurrentRevocationRegistryId);

                tailsReader = await TailsService.OpenTailsAsync(revocationRecord.TailsFile);
            }

            try
            {
                return(await AnonCreds.IssuerCreateCredentialAsync(
                           agentContext.Wallet,
                           credentialRecord.OfferJson,
                           credentialRecord.RequestJson,
                           CredentialUtils.FormatCredentialValues(credentialRecord.CredentialAttributesValues),
                           definitionRecord.CurrentRevocationRegistryId,
                           tailsReader), revocationRecord);
            }
            catch (RevocationRegistryFullException)
            {
                if (!definitionRecord.RevocationAutoScale)
                {
                    throw;
                }
            }

            var    registryIndex = definitionRecord.CurrentRevocationRegistryId.Split(':').LastOrDefault()?.Split('-').FirstOrDefault();
            string registryTag;

            if (int.TryParse(registryIndex, out var currentIndex))
            {
                registryTag = $"{currentIndex + 1}-{definitionRecord.MaxCredentialCount}";
            }
            else
            {
                registryTag = $"1-{definitionRecord.MaxCredentialCount}";
            }

            var(_, nextRevocationRecord) = await SchemaService.CreateRevocationRegistryAsync(agentContext, registryTag, definitionRecord);

            definitionRecord.CurrentRevocationRegistryId = nextRevocationRecord.Id;
            await RecordService.UpdateAsync(agentContext.Wallet, definitionRecord);

            tailsReader = await TailsService.OpenTailsAsync(nextRevocationRecord.TailsFile);

            return(await AnonCreds.IssuerCreateCredentialAsync(
                       agentContext.Wallet,
                       credentialRecord.OfferJson,
                       credentialRecord.RequestJson,
                       CredentialUtils.FormatCredentialValues(credentialRecord.CredentialAttributesValues),
                       nextRevocationRecord.Id,
                       tailsReader), nextRevocationRecord);
        }
示例#3
0
        /// <inheritdoc />
        public virtual async Task <CredentialOfferMessage> CreateOfferAsync(Wallet wallet, DefaultCreateOfferConfiguration config)
        {
            Logger.LogInformation(LoggingEvents.CreateCredentialOffer, "DefinitionId {0}, ConnectionId {1}, IssuerDid {2}",
                                  config.CredentialDefinitionId, config.ConnectionId, config.IssuerDid);

            var connection = await ConnectionService.GetAsync(wallet, config.ConnectionId);

            var offerJson = await AnonCreds.IssuerCreateCredentialOfferAsync(wallet, config.CredentialDefinitionId);

            var nonce = JObject.Parse(offerJson)["nonce"].ToObject <string>();

            // Write offer record to local wallet
            var credentialRecord = new CredentialRecord
            {
                Id = Guid.NewGuid().ToString(),
                CredentialDefinitionId = config.CredentialDefinitionId,
                OfferJson    = offerJson,
                ValuesJson   = CredentialUtils.FormatCredentialValues(config.CredentialAttributeValues),
                State        = CredentialState.Offered,
                ConnectionId = connection.GetId(),
            };

            credentialRecord.Tags.Add(TagConstants.Nonce, nonce);
            credentialRecord.Tags.Add(TagConstants.Role, TagConstants.Issuer);
            credentialRecord.Tags.Add(TagConstants.ConnectionId, connection.GetId());

            if (!string.IsNullOrEmpty(config.IssuerDid))
            {
                credentialRecord.Tags.Add(TagConstants.IssuerDid, config.IssuerDid);
            }

            if (config.Tags != null)
            {
                foreach (var tag in config.Tags)
                {
                    if (!credentialRecord.Tags.Keys.Contains(tag.Key))
                    {
                        credentialRecord.Tags.Add(tag.Key, tag.Value);
                    }
                }
            }

            await RecordService.AddAsync(wallet, credentialRecord);

            var credentialOffer = await MessageSerializer.PackSealedAsync <CredentialOfferMessage>(
                new CredentialOfferDetails { OfferJson = offerJson },
                wallet,
                connection.MyVk,
                connection.TheirVk);

            credentialOffer.Type = MessageUtils.FormatDidMessageType(connection.TheirDid, MessageTypes.CredentialOffer);

            return(credentialOffer);
        }
        /// <inheritdoc />
        public virtual async Task AcceptOfferAsync(IAgentContext agentContext, string credentialId,
                                                   Dictionary <string, string> attributeValues = null)
        {
            var credential = await GetAsync(agentContext, credentialId);

            if (credential.State != CredentialState.Offered)
            {
                throw new AgentFrameworkException(ErrorCode.RecordInInvalidState,
                                                  $"Credential state was invalid. Expected '{CredentialState.Offered}', found '{credential.State}'");
            }

            var credentialCopy = credential.DeepCopy();

            var connection = await ConnectionService.GetAsync(agentContext, credential.ConnectionId);

            var definition = await LedgerService.LookupDefinitionAsync(agentContext.Pool, credential.CredentialDefinitionId);

            var provisioning = await ProvisioningService.GetProvisioningAsync(agentContext.Wallet);

            var request = await AnonCreds.ProverCreateCredentialReqAsync(agentContext.Wallet, connection.MyDid, credential.OfferJson,
                                                                         definition.ObjectJson, provisioning.MasterSecretId);

            // Update local credential record with new info
            credential.CredentialRequestMetadataJson = request.CredentialRequestMetadataJson;

            await credential.TriggerAsync(CredentialTrigger.Request);

            await RecordService.UpdateAsync(agentContext.Wallet, credential);

            var msg = new CredentialRequestMessage
            {
                OfferJson             = credential.OfferJson,
                CredentialRequestJson = request.CredentialRequestJson,
                CredentialValuesJson  = CredentialUtils.FormatCredentialValues(attributeValues)
            };

            try
            {
                await MessageService.SendAsync(agentContext.Wallet, msg, connection);
            }
            catch (Exception e)
            {
                await RecordService.UpdateAsync(agentContext.Wallet, credentialCopy);

                throw new AgentFrameworkException(ErrorCode.A2AMessageTransmissionError, "Failed to send credential request message", e);
            }
        }
        public void CanFormatCredentialValues()
        {
            Dictionary <string, string> attributeValues = new Dictionary <string, string>()
            {
                { "first_name", "Test" },
                { "last_name", "holder" }
            };

            var expectedResult =
                "{\n  \"first_name\" : {\n    \"raw\" : \"Test\",\n    \"encoded\" : \"1234567890\"\n  },\n  \"last_name\" : {\n    \"raw\" : \"holder\",\n    \"encoded\" : \"1234567890\"\n  }\n}";

            var formatedCredentialUtils = CredentialUtils.FormatCredentialValues(attributeValues);

            var expectedResultObj = JObject.Parse(expectedResult);
            var formatedCredObj   = JObject.Parse(formatedCredentialUtils);

            Assert.Equal(expectedResultObj, formatedCredObj);
        }
        public void CanFormatStringCredentialValues()
        {
            var attributeValues = new List <CredentialPreviewAttribute>
            {
                new CredentialPreviewAttribute("first_name", "Test"),
                new CredentialPreviewAttribute("last_name", "holder")
            };

            var expectedResult =
                "{\n  \"first_name\" : {\n    \"raw\" : \"Test\",\n    \"encoded\" : \"1234567890\"\n  },\n  \"last_name\" : {\n    \"raw\" : \"holder\",\n    \"encoded\" : \"1234567890\"\n  }\n}";

            var formatedCredentialUtils = CredentialUtils.FormatCredentialValues(attributeValues);

            var expectedResultObj = JObject.Parse(expectedResult);
            var formatedCredObj   = JObject.Parse(formatedCredentialUtils);

            Assert.Equal(expectedResultObj, formatedCredObj);
        }
示例#7
0
        /// <inheritdoc />
        public virtual async Task AcceptOfferAsync(Wallet wallet, Pool pool, string credentialId,
                                                   Dictionary <string, string> attributeValues = null)
        {
            var credential = await RecordService.GetAsync <CredentialRecord>(wallet, credentialId);

            var connection = await ConnectionService.GetAsync(wallet, credential.ConnectionId);

            var definition =
                await LedgerService.LookupDefinitionAsync(pool, connection.MyDid, credential.CredentialDefinitionId);

            var provisioning = await ProvisioningService.GetProvisioningAsync(wallet);

            var request = await AnonCreds.ProverCreateCredentialReqAsync(wallet, connection.MyDid, credential.OfferJson,
                                                                         definition.ObjectJson, provisioning.MasterSecretId);

            // Update local credential record with new info
            credential.CredentialRequestMetadataJson = request.CredentialRequestMetadataJson;

            var details = new CredentialRequestDetails
            {
                OfferJson             = credential.OfferJson,
                CredentialRequestJson = request.CredentialRequestJson,
                CredentialValuesJson  = CredentialUtils.FormatCredentialValues(attributeValues)
            };

            var requestMessage =
                await MessageSerializer.PackSealedAsync <CredentialRequestMessage>(details, wallet, connection.MyVk,
                                                                                   connection.TheirVk);

            requestMessage.Type =
                MessageUtils.FormatDidMessageType(connection.TheirDid, MessageTypes.CredentialRequest);

            await credential.TriggerAsync(CredentialTrigger.Request);

            await RecordService.UpdateAsync(wallet, credential);

            //TODO we need roll back, i.e if we fail to send the A2A message the credential record should revert to Offer phase
            //so the user can resend
            await RouterService.ForwardAsync(new ForwardEnvelopeMessage
            {
                Content = requestMessage.ToJson(),
                Type    = MessageUtils.FormatDidMessageType(connection.TheirDid, MessageTypes.Forward)
            }, connection.Endpoint);
        }
示例#8
0
        public async Task <(CredentialIssueMessage, CredentialRecord)> CreateCredentialAsync(IAgentContext agentContext, string credentialId, IEnumerable <CredentialPreviewAttribute> values)
        {
            var credential = await GetAsync(agentContext, credentialId);

            if (credential.State != CredentialState.Requested)
            {
                throw new AgentFrameworkException(ErrorCode.RecordInInvalidState,
                                                  $"Credential state was invalid. Expected '{CredentialState.Requested}', found '{credential.State}'");
            }

            if (values != null && values.Any())
            {
                credential.CredentialAttributesValues = values;
            }

            var definitionRecord =
                await SchemaService.GetCredentialDefinitionAsync(agentContext.Wallet, credential.CredentialDefinitionId);

            var connection = await ConnectionService.GetAsync(agentContext, credential.ConnectionId);

            var provisioning = await ProvisioningService.GetProvisioningAsync(agentContext.Wallet);

            if (connection.State != ConnectionState.Connected)
            {
                throw new AgentFrameworkException(ErrorCode.RecordInInvalidState,
                                                  $"Connection state was invalid. Expected '{ConnectionState.Connected}', found '{connection.State}'");
            }

            string            revocationRegistryId = null;
            BlobStorageReader tailsReader          = null;

            if (definitionRecord.SupportsRevocation)
            {
                var revocationRecordSearch = await RecordService.SearchAsync <RevocationRegistryRecord>(
                    agentContext.Wallet, SearchQuery.Equal(nameof(RevocationRegistryRecord.CredentialDefinitionId), definitionRecord.Id), null, 5);

                var revocationRecord = revocationRecordSearch.Single(); // TODO: Credential definition can have multiple revocation registries

                revocationRegistryId = revocationRecord.Id;
                tailsReader          = await TailsService.OpenTailsAsync(revocationRecord.TailsFile);
            }

            var issuedCredential = await AnonCreds.IssuerCreateCredentialAsync(agentContext.Wallet, credential.OfferJson,
                                                                               credential.RequestJson, CredentialUtils.FormatCredentialValues(credential.CredentialAttributesValues), revocationRegistryId, tailsReader);

            if (definitionRecord.SupportsRevocation)
            {
                var paymentInfo = await PaymentService.GetTransactionCostAsync(agentContext, TransactionTypes.REVOC_REG_ENTRY);

                await LedgerService.SendRevocationRegistryEntryAsync(
                    wallet : agentContext.Wallet,
                    pool : await agentContext.Pool,
                    issuerDid : provisioning.IssuerDid,
                    revocationRegistryDefinitionId : revocationRegistryId,
                    revocationDefinitionType : "CL_ACCUM",
                    value : issuedCredential.RevocRegDeltaJson,
                    paymentInfo : paymentInfo);

                credential.CredentialRevocationId = issuedCredential.RevocId;

                if (paymentInfo != null)
                {
                    await RecordService.UpdateAsync(agentContext.Wallet, paymentInfo.PaymentAddress);
                }
            }

            await credential.TriggerAsync(CredentialTrigger.Issue);

            await RecordService.UpdateAsync(agentContext.Wallet, credential);

            var threadId = credential.GetTag(TagConstants.LastThreadId);

            var credentialMsg = new CredentialIssueMessage
            {
                Credentials = new []
                {
                    new Attachment
                    {
                        Id       = "libindy-cred-0",
                        MimeType = CredentialMimeTypes.ApplicationJsonMimeType,
                        Data     = new AttachmentContent
                        {
                            Base64 = issuedCredential.CredentialJson
                                     .GetUTF8Bytes()
                                     .ToBase64String()
                        }
                    }
                }
            };

            credentialMsg.ThreadFrom(threadId);

            return(credentialMsg, credential);
        }
        /// <inheritdoc />
        public virtual async Task IssueCredentialAsync(IAgentContext agentContext, string issuerDid, string credentialId,
                                                       Dictionary <string, string> values)
        {
            var credential = await GetAsync(agentContext, credentialId);

            if (credential.State != CredentialState.Requested)
            {
                throw new AgentFrameworkException(ErrorCode.RecordInInvalidState,
                                                  $"Credential state was invalid. Expected '{CredentialState.Requested}', found '{credential.State}'");
            }

            var credentialCopy = credential.DeepCopy();

            if (values != null && values.Count > 0)
            {
                credential.ValuesJson = CredentialUtils.FormatCredentialValues(values);
            }

            var definitionRecord =
                await SchemaService.GetCredentialDefinitionAsync(agentContext.Wallet, credential.CredentialDefinitionId);

            var connection = await ConnectionService.GetAsync(agentContext, credential.ConnectionId);

            if (connection.State != ConnectionState.Connected)
            {
                throw new AgentFrameworkException(ErrorCode.RecordInInvalidState,
                                                  $"Connection state was invalid. Expected '{ConnectionState.Connected}', found '{connection.State}'");
            }

            string            revocationRegistryId = null;
            BlobStorageReader tailsReader          = null;

            if (definitionRecord.SupportsRevocation)
            {
                var revocationRecordSearch = await RecordService.SearchAsync <RevocationRegistryRecord>(
                    agentContext.Wallet, SearchQuery.Equal(nameof(RevocationRegistryRecord.CredentialDefinitionId), definitionRecord.Id), null, 5);

                var revocationRecord = revocationRecordSearch.Single(); // TODO: Credential definition can have multiple revocation registries

                revocationRegistryId = revocationRecord.Id;
                tailsReader          = await TailsService.OpenTailsAsync(revocationRecord.TailsFile);
            }

            var issuedCredential = await AnonCreds.IssuerCreateCredentialAsync(agentContext.Wallet, credential.OfferJson,
                                                                               credential.RequestJson, credential.ValuesJson, revocationRegistryId, tailsReader);

            if (definitionRecord.SupportsRevocation)
            {
                await LedgerService.SendRevocationRegistryEntryAsync(agentContext.Wallet, agentContext.Pool, issuerDid,
                                                                     revocationRegistryId,
                                                                     "CL_ACCUM", issuedCredential.RevocRegDeltaJson);

                credential.CredentialRevocationId = issuedCredential.RevocId;
            }

            var msg = new CredentialMessage
            {
                CredentialJson       = issuedCredential.CredentialJson,
                RevocationRegistryId = revocationRegistryId
            };

            await credential.TriggerAsync(CredentialTrigger.Issue);

            await RecordService.UpdateAsync(agentContext.Wallet, credential);

            try
            {
                await MessageService.SendAsync(agentContext.Wallet, msg, connection);
            }
            catch (Exception e)
            {
                await RecordService.UpdateAsync(agentContext.Wallet, credentialCopy);

                throw new AgentFrameworkException(ErrorCode.A2AMessageTransmissionError, "Failed to send credential request message", e);
            }
        }
        /// <inheritdoc />
        public virtual async Task <(CredentialOfferMessage, string)> CreateOfferAsync(IAgentContext agentContext, OfferConfiguration config, string connectionId = null)
        {
            Logger.LogInformation(LoggingEvents.CreateCredentialOffer, "DefinitionId {0}, ConnectionId {1}, IssuerDid {2}",
                                  config.CredentialDefinitionId, config.IssuerDid);

            if (!config.MultiPartyOffer && !string.IsNullOrEmpty(connectionId))
            {
                var connection = await ConnectionService.GetAsync(agentContext, connectionId);

                if (connection.State != ConnectionState.Connected)
                {
                    throw new AgentFrameworkException(ErrorCode.RecordInInvalidState,
                                                      $"Connection state was invalid. Expected '{ConnectionState.Connected}', found '{connection.State}'");
                }
            }

            var offerJson = await AnonCreds.IssuerCreateCredentialOfferAsync(agentContext.Wallet, config.CredentialDefinitionId);

            var nonce = JObject.Parse(offerJson)["nonce"].ToObject <string>();

            // Write offer record to local wallet
            var credentialRecord = new CredentialRecord
            {
                Id = Guid.NewGuid().ToString(),
                CredentialDefinitionId = config.CredentialDefinitionId,
                OfferJson       = offerJson,
                MultiPartyOffer = config.MultiPartyOffer,
                ValuesJson      = CredentialUtils.FormatCredentialValues(config.CredentialAttributeValues),
                State           = CredentialState.Offered,
            };

            if (!config.MultiPartyOffer)
            {
                credentialRecord.ConnectionId = connectionId;
            }

            credentialRecord.SetTag(TagConstants.Nonce, nonce);
            credentialRecord.SetTag(TagConstants.Role, TagConstants.Issuer);

            if (!string.IsNullOrEmpty(config.IssuerDid))
            {
                credentialRecord.Tags.Add(TagConstants.IssuerDid, config.IssuerDid);
            }

            if (config.Tags != null)
            {
                foreach (var tag in config.Tags)
                {
                    if (!credentialRecord.Tags.Keys.Contains(tag.Key))
                    {
                        credentialRecord.Tags.Add(tag.Key, tag.Value);
                    }
                }
            }

            await RecordService.AddAsync(agentContext.Wallet, credentialRecord);

            return(new CredentialOfferMessage {
                OfferJson = offerJson
            }, credentialRecord.Id);
        }
示例#11
0
        /// <inheritdoc />
        public virtual async Task <(CredentialMessage, CredentialRecord)> CreateCredentialAsync(IAgentContext agentContext, string issuerDid, string credentialId,
                                                                                                IEnumerable <CredentialPreviewAttribute> values)
        {
            var credential = await GetAsync(agentContext, credentialId);

            if (credential.State != CredentialState.Requested)
            {
                throw new AgentFrameworkException(ErrorCode.RecordInInvalidState,
                                                  $"Credential state was invalid. Expected '{CredentialState.Requested}', found '{credential.State}'");
            }

            if (values != null && values.Any())
            {
                credential.CredentialAttributesValues = values;
            }

            var definitionRecord =
                await SchemaService.GetCredentialDefinitionAsync(agentContext.Wallet, credential.CredentialDefinitionId);

            var connection = await ConnectionService.GetAsync(agentContext, credential.ConnectionId);

            if (connection.State != ConnectionState.Connected)
            {
                throw new AgentFrameworkException(ErrorCode.RecordInInvalidState,
                                                  $"Connection state was invalid. Expected '{ConnectionState.Connected}', found '{connection.State}'");
            }

            string            revocationRegistryId = null;
            BlobStorageReader tailsReader          = null;

            if (definitionRecord.SupportsRevocation)
            {
                var revocationRecordSearch = await RecordService.SearchAsync <RevocationRegistryRecord>(
                    agentContext.Wallet, SearchQuery.Equal(nameof(RevocationRegistryRecord.CredentialDefinitionId), definitionRecord.Id), null, 5);

                var revocationRecord = revocationRecordSearch.Single(); // TODO: Credential definition can have multiple revocation registries

                revocationRegistryId = revocationRecord.Id;
                tailsReader          = await TailsService.OpenTailsAsync(revocationRecord.TailsFile);
            }

            var issuedCredential = await AnonCreds.IssuerCreateCredentialAsync(agentContext.Wallet, credential.OfferJson,
                                                                               credential.RequestJson, CredentialUtils.FormatCredentialValues(credential.CredentialAttributesValues), revocationRegistryId, tailsReader);

            if (definitionRecord.SupportsRevocation)
            {
                await LedgerService.SendRevocationRegistryEntryAsync(agentContext.Wallet, await agentContext.Pool, issuerDid,
                                                                     revocationRegistryId,
                                                                     "CL_ACCUM", issuedCredential.RevocRegDeltaJson);

                credential.CredentialRevocationId = issuedCredential.RevocId;
            }

            await credential.TriggerAsync(CredentialTrigger.Issue);

            await RecordService.UpdateAsync(agentContext.Wallet, credential);

            var threadId = credential.GetTag(TagConstants.LastThreadId);

            var credentialMsg = new CredentialMessage
            {
                CredentialJson       = issuedCredential.CredentialJson,
                RevocationRegistryId = revocationRegistryId
            };

            credentialMsg.ThreadFrom(threadId);

            return(credentialMsg, credential);
        }
示例#12
0
        /// <inheritdoc />
        public virtual async Task IssueCredentialAsync(Pool pool, Wallet wallet, string issuerDid, string credentialId,
                                                       Dictionary <string, string> values)
        {
            var credentialRecord = await RecordService.GetAsync <CredentialRecord>(wallet, credentialId);

            if (values != null && values.Count > 0)
            {
                credentialRecord.ValuesJson = CredentialUtils.FormatCredentialValues(values);
            }

            var definitionRecord =
                await SchemaService.GetCredentialDefinitionAsync(wallet, credentialRecord.CredentialDefinitionId);

            var connection = await ConnectionService.GetAsync(wallet, credentialRecord.ConnectionId);

            if (credentialRecord.State != CredentialState.Requested)
            {
                throw new Exception(
                          $"Credential sate was invalid. Expected '{CredentialState.Requested}', found '{credentialRecord.State}'");
            }

            string            revocationRegistryId = null;
            BlobStorageReader tailsReader          = null;

            if (definitionRecord.SupportsRevocation)
            {
                var revocationRecordSearch = await RecordService.SearchAsync <RevocationRegistryRecord>(
                    wallet, new SearchRecordQuery { { TagConstants.CredentialDefinitionId, definitionRecord.DefinitionId } }, null, 1);

                var revocationRecord = revocationRecordSearch.First();

                revocationRegistryId = revocationRecord.RevocationRegistryId;
                tailsReader          = await TailsService.OpenTailsAsync(revocationRecord.TailsFile);
            }

            var issuedCredential = await AnonCreds.IssuerCreateCredentialAsync(wallet, credentialRecord.OfferJson,
                                                                               credentialRecord.RequestJson, credentialRecord.ValuesJson, revocationRegistryId, tailsReader);

            if (definitionRecord.SupportsRevocation)
            {
                await LedgerService.SendRevocationRegistryEntryAsync(wallet, pool, issuerDid,
                                                                     revocationRegistryId,
                                                                     "CL_ACCUM", issuedCredential.RevocRegDeltaJson);

                credentialRecord.CredentialRevocationId = issuedCredential.RevocId;
            }

            var credentialDetails = new CredentialDetails
            {
                CredentialJson       = issuedCredential.CredentialJson,
                RevocationRegistryId = revocationRegistryId
            };

            await credentialRecord.TriggerAsync(CredentialTrigger.Issue);

            await RecordService.UpdateAsync(wallet, credentialRecord);

            var credential = await MessageSerializer.PackSealedAsync <CredentialMessage>(credentialDetails, wallet,
                                                                                         connection.MyVk,
                                                                                         connection.TheirVk);

            credential.Type = MessageUtils.FormatDidMessageType(connection.TheirDid, MessageTypes.Credential);

            await RouterService.ForwardAsync(new ForwardEnvelopeMessage
            {
                Content = credential.ToJson(),
                Type    = MessageUtils.FormatDidMessageType(connection.TheirDid, MessageTypes.Forward)
            }, connection.Endpoint);
        }