in src/ccf/recovery/ccf-recovery-agent/Controllers/RecoveryMembersController.cs [231:328]
public async Task<IActionResult> SubmitRecoveryShare(
[FromRoute] string memberId,
[FromBody] byte[] content)
{
// Verify caller is an active member of the consortium.
(var err, var payload) = await this.VerifyMemberAuthentication(memberId, content);
if (err != null)
{
return this.BadRequest(err);
}
var input = JsonSerializer.Deserialize<SubmitRecoveryShareInput>(payload)!;
err = ValidateInput(input);
if (err != null)
{
return this.BadRequest(err);
}
this.logger.LogInformation(
$"Requesting recovery service for recovery share for member {input.MemberName}.");
var wsConfig = await this.clientManager.GetWsConfig();
// To complete recovery share submission the steps are:
// - Get encrypted share from CCF
// - Request recovery service to decrypt and return the recovery_share message
// which would then be submitted to CCF to get the state digest.
// - Submit recovery_share message to CCF.
var svcClient = await this.GetRecoverySvcClient(input.AgentConfig.RecoveryService);
var recoveryMember = (await svcClient.GetFromJsonAsync<JsonObject>(
$"members/{input.MemberName}"))!;
var signingCert = recoveryMember["signingCert"]!.ToString();
using var cert = X509Certificate2.CreateFromPem(signingCert);
var recoveryMemberId = cert.GetCertHashString(HashAlgorithmName.SHA256).ToLower();
var ccfClient = await this.clientManager.GetCcfClient();
var encryptedShare = await ccfClient.GetFromJsonAsync<JsonObject>(
$"/gov/recovery/encrypted-shares/{recoveryMemberId}" +
$"?api-version={this.clientManager.GetGovApiVersion()}");
// Get attestation report and encrypted share content to send in the request.
var dataContent = new JsonObject
{
["memberName"] = input.MemberName,
["encryptedShare"] = encryptedShare
};
(var data, var signature) =
this.PrepareSignedData(dataContent, wsConfig.Attestation.PrivateKey);
var svcContent = Attestation.PrepareSignedDataRequestContent(
data,
signature,
wsConfig.Attestation.PublicKey,
wsConfig.Attestation.Report);
byte[] recoveryShareMessage;
using (var genRecoveryShareResponse = await svcClient.PostAsync(
$"members/generateRecoveryShareMessage",
JsonContent.Create(svcContent)))
{
await genRecoveryShareResponse.ValidateStatusCodeAsync(this.logger);
var body = (await genRecoveryShareResponse.Content.ReadFromJsonAsync<JsonObject>())!;
var wrappedValue = Convert.FromBase64String(body["message"]!.ToString());
recoveryShareMessage = Attestation.UnwrapRsaOaepAesKwpValue(
wrappedValue,
wsConfig.Attestation.PrivateKey);
}
JsonObject? recoveryShareResponse;
using (HttpRequestMessage request = Cose.CreateHttpRequestMessage(
$"gov/recovery/members/{recoveryMemberId}:recover" +
$"?api-version={this.clientManager.GetGovApiVersion()}",
recoveryShareMessage))
{
using HttpResponseMessage response = await ccfClient.SendAsync(request);
await response.ValidateStatusCodeAsync(this.logger);
await response.WaitGovTransactionCommittedAsync(this.logger, ccfClient);
recoveryShareResponse = await response.Content.ReadFromJsonAsync<JsonObject>();
}
return this.Ok(recoveryShareResponse);
static ODataError? ValidateInput(SubmitRecoveryShareInput input)
{
if (string.IsNullOrEmpty(input.MemberName))
{
return new ODataError("InputMissing", "name input is required.");
}
if (input.AgentConfig == null)
{
return new ODataError("InputMissing", "agentConfig input is required.");
}
return null;
}
}