Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions crates/rite-model/src/params/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,22 @@ pub struct EncryptDataParams {
#[serde(default)]
#[schemars(schema_with = "content_schemes")]
pub scheme: Option<WrapScheme>,
/// The expected fingerprint of the recipient's public key, in the form
/// `sha256:<64 lowercase hex digits>`, for a step that reads `recipient`.
/// The step fails if the recipient key does not match.
//
// Content sealed to the wrong recipient is readable by whoever holds that
// key and by nobody the ceremony meant. Lowercase only, for the reason
// given at `WrapKeyParams`.
#[serde(default)]
#[schemars(
regex(pattern = r"^sha256:[0-9a-f]{64}$"),
extend(
"x-message" = "'expect_recipient' must be a fingerprint of the form \
'sha256:<64 lowercase hex digits>'"
)
)]
pub expect_recipient: Option<String>,
}

/// The schemes that carry content, which are the ones `encrypt_data` writes.
Expand Down
33 changes: 19 additions & 14 deletions crates/rite-model/src/transcript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1253,11 +1253,12 @@ pub enum StepFact {
)]
fingerprint: Option<String>,
},
/// A key held outside the ceremony was used as the recipient of a wrap.
/// A key held outside the ceremony was used as the recipient of a wrap or
/// of encrypted content.
///
/// Its own fact rather than a field on the wrap operation: the method of a
/// wrap is publishable and the identity of a custodian may not be, and a
/// fact carries one confidentiality level.
/// Its own fact rather than a field on the operation: the method of a wrap
/// or an encryption is publishable and the identity of a custodian may not
/// be, and a fact carries one confidentiality level.
///
/// The fingerprint records the bytes that were present. Nothing in the
/// ceremony corroborates that they belong to the intended recipient, and
Expand All @@ -1266,14 +1267,17 @@ pub enum StepFact {
#[cfg_attr(
test,
schemars(
description = "A key held outside the ceremony received a wrapped key. A separate \
fact from the operation, so the method of a wrap can be published while the recipient \
stays withheld."
description = "A key held outside the ceremony received a wrapped key or encrypted \
content. A separate fact from the operation, so the method can be published while the \
recipient stays withheld."
)
)]
WrapRecipientRecorded {
/// Step that wrapped to this recipient.
#[cfg_attr(test, schemars(description = "The step that wrapped the key."))]
/// Step that wrapped or encrypted to this recipient.
#[cfg_attr(
test,
schemars(description = "The step that wrapped the key or encrypted the content.")
)]
step: StepId,
/// The artifact or material the recipient key came from, as named in
/// the ceremony.
Expand All @@ -1285,7 +1289,7 @@ pub enum StepFact {
)
)]
source: String,
/// `sha256:<hex>` over the recipient's SPKI DER.
/// The recipient's fingerprint.
#[cfg_attr(
test,
schemars(
Expand All @@ -1294,7 +1298,7 @@ pub enum StepFact {
with = "Sha256Digest"
)
)]
fingerprint: String,
fingerprint: rite_sdk::KeyFingerprint,
/// Whether the ceremony declared this fingerprint in advance, so the
/// runtime checked the key it received against the definition rather
/// than recording whatever arrived.
Expand Down Expand Up @@ -2219,19 +2223,20 @@ mod schema_snapshot_tests {

#[test]
fn wrap_recipient_recorded() {
let fingerprint = Sha256Digest::of(b"escrow spki").to_string();
let fingerprint: rite_sdk::KeyFingerprint =
Sha256Digest::of(b"escrow spki").as_str().parse().unwrap();
assert_json(
&StepFact::WrapRecipientRecorded {
step: StepId::new("s1"),
source: "escrow_pubkey".to_string(),
fingerprint: fingerprint.clone(),
fingerprint,
declared: true,
},
&json!({
"type": "wrap_recipient_recorded",
"step": "s1",
"source": "escrow_pubkey",
"fingerprint": fingerprint,
"fingerprint": fingerprint.to_string(),
"declared": true,
}),
);
Expand Down
27 changes: 20 additions & 7 deletions crates/rite-model/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,20 @@ pub enum ActionType {
/// The wrapped data identifies its scheme. The step checks the recovered
/// key against `algorithm` when one is given.
UnwrapKey,
/// Encrypt data using a key held by a backend.
/// Encrypt data for a key held by a backend or for a public key.
///
/// The backend generates a content-encryption key, encrypts the data with
/// it, and protects it with the step's encryption key. The data itself is
/// not sent to the backend.
/// Reads `data` and exactly one of `encryption_key` or `recipient`. With
/// `encryption_key`, the backend generates a content-encryption key and
/// protects it with that key. With `recipient`, an RSA-2048 or RSA-4096
/// public key held elsewhere, the backend supplies the content-encryption
/// key's random bytes, which are protected with RSAES-OAEP over SHA-256.
/// The data itself is not sent to the backend.
EncryptData,
/// Decrypt data encrypted by `encrypt_data`.
/// Decrypt data encrypted by `encrypt_data`, or a CMS file sealed
/// elsewhere and supplied as a material.
///
/// The result is a byte artifact available to later steps and erased from
/// The step checks that the container names the decryption key. The
/// result is a byte artifact available to later steps and erased from
/// memory when the run ends.
DecryptData,
/// Split a secret into shares so that `threshold` of them can recover it.
Expand Down Expand Up @@ -719,7 +724,15 @@ impl ActionType {
with_field_requires: &[],
lists: &[],
},
ActionType::EncryptData => ReadsContract::required(&["data", "encryption_key"]),
ActionType::EncryptData => ReadsContract {
required: &["data"],
exactly_one_of: &[&["encryption_key", "recipient"]],
optional: &[],
// As at `wrap_key`: only the public-key path has a recipient
// to compare.
with_field_requires: &[("expect_recipient", "recipient")],
lists: &[],
},
ActionType::DecryptData => {
ReadsContract::required(&["encrypted_data", "decryption_key"])
}
Expand Down
Loading